From d5a9490815d0e4fefc7d7ff3d218047ddd19eea8 Mon Sep 17 00:00:00 2001 From: Alvin Castro Date: Fri, 4 Jun 2021 08:27:31 -0700 Subject: [PATCH 1/2] Major update for v2. Restructure of libraries to use objects and factories. Several files migrated to a different naming schema or removed until supported in v2. See RELEASE-NOTES.md for more information. --- README.md | 12 +- RELEASE-NOTES.md | 39 + docs/source/pyaoscx.nae.rst | 7 + docs/source/pyaoscx.rst | 1 + pyaoscx/CONTRIBUTING.md | 131 + pyaoscx/DESIGN.md | 180 ++ pyaoscx/LICENSE | 201 ++ pyaoscx/README.md | 118 + pyaoscx/access_security.py | 1822 ----------- pyaoscx/acl.py | 1445 +++------ pyaoscx/acl_entry.py | 515 ++++ pyaoscx/aggregate_address.py | 453 +++ pyaoscx/api.py | 49 + pyaoscx/arp.py | 49 - pyaoscx/bgp.py | 294 -- pyaoscx/bgp_neighbor.py | 450 +++ pyaoscx/bgp_router.py | 540 ++++ pyaoscx/common_ops.py | 123 - pyaoscx/config.py | 103 - pyaoscx/configuration.py | 503 ++++ pyaoscx/device.py | 454 +++ pyaoscx/dhcp.py | 328 -- pyaoscx/dhcp_relay.py | 456 +++ pyaoscx/dns.py | 342 +++ pyaoscx/error.py | 11 + pyaoscx/evpn.py | 164 - pyaoscx/exceptions/__init__.py | 0 pyaoscx/exceptions/generic_op_error.py | 33 + pyaoscx/exceptions/login_error.py | 13 + pyaoscx/exceptions/pyaoscx_error.py | 12 + pyaoscx/exceptions/response_error.py | 24 + pyaoscx/exceptions/verification_error.py | 24 + pyaoscx/firmware.py | 33 + pyaoscx/interface.py | 3510 ++++++++++++--------- pyaoscx/ipv6.py | 442 +++ pyaoscx/lag.py | 482 --- pyaoscx/lldp.py | 231 -- pyaoscx/loop_protect.py | 246 -- pyaoscx/mac.py | 78 - pyaoscx/ospf.py | 1050 ------- pyaoscx/ospf_area.py | 435 +++ pyaoscx/ospf_interface.py | 436 +++ pyaoscx/ospf_router.py | 487 +++ pyaoscx/port.py | 998 ------ pyaoscx/pyaoscx_factory.py | 1144 +++++++ pyaoscx/pyaoscx_module.py | 120 + pyaoscx/qos.py | 3511 ---------------------- pyaoscx/rest/__init__.py | 0 pyaoscx/rest/v1/__init__.py | 0 pyaoscx/rest/v1/api.py | 163 + pyaoscx/rest/v1/interface.py | 974 ++++++ pyaoscx/rest/v10_04/__init__.py | 0 pyaoscx/rest/v10_04/api.py | 186 ++ pyaoscx/rest/v10_04/interface.py | 13 + pyaoscx/session.py | 330 +- pyaoscx/static_nexthop.py | 506 ++++ pyaoscx/static_route.py | 531 ++++ pyaoscx/system.py | 89 - pyaoscx/utils/__init__.py | 0 pyaoscx/utils/connection.py | 20 + pyaoscx/utils/list_attributes.py | 127 + pyaoscx/utils/util.py | 291 ++ pyaoscx/vlan.py | 1357 ++++----- pyaoscx/vrf.py | 929 ++++-- pyaoscx/vrf_address_family.py | 419 +++ pyaoscx/vsx.py | 838 +++--- pyaoscx/vxlan.py | 220 -- setup.py | 8 +- workflows/cleanup_l2_l3_vlans.py | 88 - workflows/configure_l2_l3_vlans.py | 109 - workflows/print_system_info.py | 64 - workflows/workflow.py | 116 + 72 files changed, 15340 insertions(+), 14137 deletions(-) create mode 100644 docs/source/pyaoscx.nae.rst create mode 100644 pyaoscx/CONTRIBUTING.md create mode 100644 pyaoscx/DESIGN.md create mode 100644 pyaoscx/LICENSE create mode 100644 pyaoscx/README.md delete mode 100644 pyaoscx/access_security.py create mode 100644 pyaoscx/acl_entry.py create mode 100644 pyaoscx/aggregate_address.py create mode 100644 pyaoscx/api.py delete mode 100644 pyaoscx/arp.py delete mode 100644 pyaoscx/bgp.py create mode 100644 pyaoscx/bgp_neighbor.py create mode 100644 pyaoscx/bgp_router.py delete mode 100644 pyaoscx/common_ops.py delete mode 100644 pyaoscx/config.py create mode 100644 pyaoscx/configuration.py create mode 100644 pyaoscx/device.py delete mode 100644 pyaoscx/dhcp.py create mode 100644 pyaoscx/dhcp_relay.py create mode 100644 pyaoscx/dns.py create mode 100644 pyaoscx/error.py delete mode 100644 pyaoscx/evpn.py create mode 100644 pyaoscx/exceptions/__init__.py create mode 100644 pyaoscx/exceptions/generic_op_error.py create mode 100644 pyaoscx/exceptions/login_error.py create mode 100644 pyaoscx/exceptions/pyaoscx_error.py create mode 100644 pyaoscx/exceptions/response_error.py create mode 100644 pyaoscx/exceptions/verification_error.py create mode 100644 pyaoscx/firmware.py create mode 100644 pyaoscx/ipv6.py delete mode 100644 pyaoscx/lag.py delete mode 100644 pyaoscx/lldp.py delete mode 100644 pyaoscx/loop_protect.py delete mode 100644 pyaoscx/mac.py delete mode 100644 pyaoscx/ospf.py create mode 100644 pyaoscx/ospf_area.py create mode 100644 pyaoscx/ospf_interface.py create mode 100644 pyaoscx/ospf_router.py delete mode 100644 pyaoscx/port.py create mode 100644 pyaoscx/pyaoscx_factory.py create mode 100644 pyaoscx/pyaoscx_module.py delete mode 100644 pyaoscx/qos.py create mode 100644 pyaoscx/rest/__init__.py create mode 100644 pyaoscx/rest/v1/__init__.py create mode 100644 pyaoscx/rest/v1/api.py create mode 100644 pyaoscx/rest/v1/interface.py create mode 100644 pyaoscx/rest/v10_04/__init__.py create mode 100644 pyaoscx/rest/v10_04/api.py create mode 100644 pyaoscx/rest/v10_04/interface.py create mode 100644 pyaoscx/static_nexthop.py create mode 100644 pyaoscx/static_route.py delete mode 100644 pyaoscx/system.py create mode 100644 pyaoscx/utils/__init__.py create mode 100644 pyaoscx/utils/connection.py create mode 100644 pyaoscx/utils/list_attributes.py create mode 100644 pyaoscx/utils/util.py create mode 100644 pyaoscx/vrf_address_family.py delete mode 100644 pyaoscx/vxlan.py delete mode 100644 workflows/cleanup_l2_l3_vlans.py delete mode 100644 workflows/configure_l2_l3_vlans.py delete mode 100644 workflows/print_system_info.py create mode 100644 workflows/workflow.py diff --git a/README.md b/README.md index d4ff95a..18631b4 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,24 @@ # pyaoscx -These modules are written for AOS-CX API v1 and v10.04. These scripts are written for devices running AOS-CX firmware version 10.04. +These modules are written for AOS-CX API v1 and v10.04. These scripts are written for devices running AOS-CX firmware +version 10.04 or greater. -For this initial release, it is recommended to use the v1 AOS-CX API. See the Release Notes for more information. +See the [Release Notes](RELEASE-NOTES.md) for more information. + +Please note that pyaoscx v2 is **not** backwards compatible for pyaoscx v1 and earlier, so please specify the correct +version when using pyaoscx in requirements.txt files ## Structure +Detailed information about the structure and design can be found in the [Design document](pyaoscx/DESIGN.md). * REST API call functions are found in the modules in /pyaoscx. * REST API call functions are combined into other functions that emulate low-level processes. These low-level process functions are also placed in files in /pyaoscx. * Functions from the /pyaoscx files (API functions and low-level functions) are combined to emulate larger network configuration processes (workflows). These workflow scripts stored in the /workflows folder. + ## How to contribute -Please see the accompanying CONTRIBUTING.md file for guidelines on how to contribute to this repository. +Please see the accompanying [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines on how to contribute to this repository. ## Git Workflow diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 4708248..1158d1a 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,4 +1,43 @@ +# 2.0.0 + +## Notable Changes +**WARNING: V2 is NOT backwards compatible with v1 and earlier** +* Huge overhaul of the design - the libraries now use factories in order for the code to be more object oriented. + Please read the [Design document](pyaoscx/DESIGN.md) file for more information. +* Removed previous workflow examples (defunct) and added /workflows/workflow.py as an example for using the new design +* Added directories for supporting files in /rest/v1 and /rest/v10_04 +* Most of the previous libraries have been updated, but there are unsupported modules that are pending updates below: + * ARP + * Common_ops + * EVPN + * LLDP + * Loop Protect + * MAC + * NAE + * QoS + * System + * VXLAN +* Additionally, a few libraries have been migrated into other libraries: + * bgp.py is now split between bgp_router.py and bgp_neighbor.py + * config. py is now configuration.py + * dhcp.py is now dhcp_relay.py + * lag.py is now integrated into interface.py + * ospf.py is now split between ospf_area.py, ospf_interface.py, and ospf_router.py + + +# 1.0.0 + +## Notable Changes +* Made changes to setup.py to update the PyPi information + +# 0.3.0 + +## Notable Changes +* Added nae.py, a new module to provide functionality for interacting with NAE scripts and agents. +* Added a function 'create_first_password' to setup.py to support logging into a factory default switch and handling the mandatory password creation. + # 0.2.2 + ## Notable Changes * Minor bug fix in system.py module diff --git a/docs/source/pyaoscx.nae.rst b/docs/source/pyaoscx.nae.rst new file mode 100644 index 0000000..0d3a325 --- /dev/null +++ b/docs/source/pyaoscx.nae.rst @@ -0,0 +1,7 @@ +pyaoscx.nae module +================== + +.. automodule:: pyaoscx.nae + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/pyaoscx.rst b/docs/source/pyaoscx.rst index a03c68f..e0773a4 100644 --- a/docs/source/pyaoscx.rst +++ b/docs/source/pyaoscx.rst @@ -19,6 +19,7 @@ Submodules pyaoscx.lldp pyaoscx.loop_protect pyaoscx.mac + pyaoscx.nae pyaoscx.ospf pyaoscx.port pyaoscx.qos diff --git a/pyaoscx/CONTRIBUTING.md b/pyaoscx/CONTRIBUTING.md new file mode 100644 index 0000000..fdafff4 --- /dev/null +++ b/pyaoscx/CONTRIBUTING.md @@ -0,0 +1,131 @@ +# Contribution Guidelines + +If you're reading this, you're probably thinking about contributing to this repository. We really appreciate that--thank you! + +This document provides guidelines on contributing to this repository. Please follow these guidelines when creating issues, making commits, and submitting pull requests. The repository maintainers review all pull requests and verify that they conform to these guidelines before approving and merging. + +#### Table Of Contents +[How Can I Contribute?](#how-can-i-contribute) + * [Contribution Ideas](#contribution-ideas) + * [What should I know before I get started?](#what-should-i-know-before-i-get-started) + +[Licensing](#licensing) + * [Developer's Certificate of Origin](#developers-certificate-of-origin) + * [Sign Your Work](#sign-your-work) + +[Coding Conventions](#coding-conventions) + +[Additional Notes](#additional-notes) + * [Resources](#resources) + +## How Can I Contribute? + +### Contribution Ideas + +1. Raise issues for bugs, features, and enhancements. +1. Submit updates and improvements to the documentation. +1. Submit articles and guides, which are also part of the documentation. +1. Help out repo maintainers by answering questions in [Airheads Developer Community][airheads-link]. +1. Share feedback and let us know about interesting use cases in [Airheads Developer Community][airheads-link]. + +### What should I know before I get started? + +The best way to directly collaborate with the project contributors is through GitHub. + +* If you want to raise an issue such as a defect, an enhancement request, feature request, or a general issue, please open a GitHub issue. +* If you want to contribute to our code by either fixing a problem, enhancing some code, or creating a new feature, please open a GitHub pull request against the development branch. +> **Note:** All pull requests require an associated issue number, must be made against the **development** branch, and require acknowledgement of the DCO. See the [Licensing](#licensing) section below. + +Before you start to code, we recommend discussing your plans through a GitHub issue, especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. + +It is your responsibility to test and verify, prior to submitting a pull request, that your updated code doesn't introduce any bugs. Please write a clear commit message for each commit. Brief messages are fine for small changes, but bigger changes warrant a little more detail (at least a few sentences). +Note that all patches from all contributors get reviewed. +After a pull request is made, other contributors will offer feedback. If the patch passes review, a maintainer will accept it with a comment. +When a pull request fails review, the author is expected to update the pull request to address the issue until it passes review and the pull request merges successfully. + +At least one review from a maintainer is required for all patches. + +### Contribution Guidelines +This repo is maintained on a best-effort basis. The burden is on the submitter and not the repo maintainers to ensure the following criteria are met when code is submitted. +1. All code submissions must adhere to the structure of the repo: + * Lower-level functions and API calls must be saved in the /pyaoscx folder. + * High-level process-focused functions must be saved in the /workflows folder. + * Do not create new separate folders for submitted projects. + * Do not make copies of existing files to be saved in different folders. + * The objective is that all submissions build on the repo as a whole, rather than creating multiple sub-projects housed in the repo. +2. All Python code should conform to PEP-8 standards. The maintainers use Pycharm IDE to perform this check. That does not require submitters to use Pycharm, but regardless of the code editor used, the PEP-8 check must be successful. +3. All functions should have explanatory docstrings using the reStructuredText format. +4. All workflows should have a comment at the top explaining the configuration steps the workflow performs, and any preconditions that need to be met before running the script. +5. All git commits should have clear, concise messages which explain the changes made in the commit. All Pull Requests (PRs) should contain a title and comments that explain the impact of the PR. +6. All code submitted for merge consideration must be tested by the submitter. + +## Licensing + +All contributions must include acceptance of the DCO: + +### Developer’s Certificate of Origin + +> Developer Certificate of Origin Version 1.1 +> +> Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 +> York Street, Suite 102, San Francisco, CA 94110 USA +> +> Everyone is permitted to copy and distribute verbatim copies of this +> license document, but changing it is not allowed. +> +> Developer's Certificate of Origin 1.1 +> +> By making a contribution to this project, I certify that: +> +> \(a) The contribution was created in whole or in part by me and I have +> the right to submit it under the open source license indicated in the +> file; or +> +> \(b) The contribution is based upon previous work that, to the best of my +> knowledge, is covered under an appropriate open source license and I +> have the right under that license to submit that work with +> modifications, whether created in whole or in part by me, under the same +> open source license (unless I am permitted to submit under a different +> license), as indicated in the file; or +> +> \(c) The contribution was provided directly to me by some other person +> who certified (a), (b) or (c) and I have not modified it. +> +> \(d) I understand and agree that this project and the contribution are +> public and that a record of the contribution (including all personal +> information I submit with it, including my sign-off) is maintained +> indefinitely and may be redistributed consistent with this project or +> the open source license(s) involved. + +### Sign Your Work + +To accept the DCO, simply add this line to each commit message with your +name and email address (`git commit -s` will do this for you): + + Signed-off-by: Jane Example + +For legal reasons, no anonymous or pseudonymous contributions are +accepted. + +## Coding Conventions + +1. Python code should conform to PEP-8. PyCharm editor has a built-in PEP-8 checker. +1. Since this is a collaborative project, document your code with comments that will help other contributors understand the code you write. +1. When in doubt, follow conventions you see used in the source already. + +## Additional Notes + +> **Note:** Please don't file an issue to ask a question. Please reach out to us via email or disucssion forums. + +### Resources + +| Resource | Description | +| --- | --- | +| [Airheads Developer Community][airheads-link] | Aruba Airheads forum to discuss all things network automation. | +| [Aruba Bots Automate Videos][aruba-bots-playlist-link]| YouTube playlist containing instructional videos for Ansible and Python automation repositories. | +| [aruba-switching-automation@hpe.com][email-link] | Distribution list email to contact the switching automation technical marketing engineering team. | + + +[airheads-link]: https://community.arubanetworks.com/community-home?CommunityKey=ea467413-8db4-4c49-b5f8-1a12f193e959 +[aruba-bots-playlist-link]: https://www.youtube.com/watch?v=_QxwUiXkJpA&list=PLsYGHuNuBZcY02FUh95ZpOB5VFkPurVaX +[email-link]: mailto:aruba-automation@hpe.com \ No newline at end of file diff --git a/pyaoscx/DESIGN.md b/pyaoscx/DESIGN.md new file mode 100644 index 0000000..ac149a3 --- /dev/null +++ b/pyaoscx/DESIGN.md @@ -0,0 +1,180 @@ +# pyaoscx + +## Overview + +The pyaoscx (a.k.a. AOS-CX Python SDK) is a framework created to ease the access to +the Aruba switches running AOS-CX, using the REST API interface. The framework is +not intended to be a Network Management System (NMS). Instead, it is meant to +provide functions and classes to perform basic operations on the devices, +including the following: + +1. Handling the session and remote connection. +1. Handling the allowed operations depending on their capabilities. +1. Handling the responses depending on the API version. +1. Basic data validation. +1. Raising meaningful errors. + +And, also, if needed: + +1. Caching data. +1. Handling notifications. + +## Design drivers + +pyaoscx uses the object-oriented approach. Instead of having separate modules to +perform specific operations, it connect several features together to represent +the operational state of the switch. Such pattern allows the client to keep track +of the switch configuration in its internal data structures. + + +Every single feature must be represented in a class. Such a class may or may not +be contained in another class or a collection. For example, the `Interface` +represents a specific Interface, and it may contain references to other `VLANs`, `VRFs`, +and depending on the use case, the `VSF` or `VSX` configuration. Also, an +`Interface` may be represented as a `LAG`, meaning that it may contain +references to more than a single physical port. + +### pyaoscx Modules +Each pyaoscx module is defined as a Python Class, in which it contains method +definitions to generate and manage a module through REST API call or multiple +REST API calls. All module creation is managed through a flag attribute materialized. +This attribute will be True if the module exists within the Switch Device, False otherwise. +This will be updated whenever a POST or GET called is done. + +Each pyaoscx Module has a list of important attributes besides materialized: +* session: use to make the HTTP requests, among other things. +* config_attrs: list of attributes writable to each specific object +* original_attributes: dictionary with object information and attributes right + after performing a GET request. +* modified: Flag to verify if object was modified, in case it was not the PUT + request is not made. + +### Materialized objects + +The attributes for a local object in the SDK may not match the current configuration on the switch. +Creating a local object in the SDK does not mean the object was created in the device. +The internal attributes can be filled with _artificial_ data, which cannot be +considered _materialized_ unless the object was retrieved from the device with +a __get__ operation, or the object was created with a __create__ operation. + +### Operations + +REST API allows a well-known list of HTTP verbs. They must be mapped to class- +specific operations to keep the same behavior along this SDK. + +#### Get + +This method maps to the HTTP GET verb. It allows to retrieve the data from the +device, converting the JSON response into the corresponding internal attributes +of the object. Object attributes must match 1:1 with the device information. + +#### Create + +This method maps to the HTTP POST verb. It retrieves a list of attributes from +the object known as writable attributes, creating a body for the +POST request. + +#### Update + +This method maps to the HTTP PUT verb. Just as the CREATE, retrieves a list of +attributes from the object known as writable attributes. The difference +is the UPDATE methods makes a PUT Request. It's important to know that this method +is executed only if the object is materialized. + +##### NOTIFICATIONS + +#### Set + +Given that the REST API does not provide support to the HTTP PATCH verb yet, all +the operations to change the device configuration must use the HTTP PUT verb. +It means that the the original attributes must be retrieved first, then change +the values required, and finally perform the change. This operation cannot be +performed on an unmaterialized object. + + +### Session management + +In order to perform any operation in the device, a connection must be +established. The session is an object representing it. It keeps the credential +information, such as `username` and `password`, and, once established, it also +keeps the session cookie. There is an assumption that all the operations +performed should be done with the same REST API version. Therefore, the session + also contains the version the client wants to use. + +The session state must be validated every time an operation must be performed. +Therefore, using a Python decorator is the suggested approach. + +However, in order to prevent using a _global accessible object_ representing +the session, and passing it as a parameter in every call, the SDK objects may +receive the session object as _mandatory parameter_ in the constructor. + +### Factory Pattern + +The REST API request and responses vary depending on the version. It means that +the internal structure of the payload changes, and the SDK must take care of it. +A Factory class is used to create new objects -- both python objects and modules +inside a switch AOS-CX Device. + +If a module is created using the pyaoscx Factory, it's going to be set with the given +state in each creation method. + +## Main classes + +### Session + +The session class keeps the connection parameters. When a connection is established, +it also records the session cookie and the last time the connection was used, which +can help to identify whether the session is still valid because of the idle time. +The following fields will be required to store the main session information: + +* Username +* Password +* Cookie +* API version +* Last operation timestamp + +### API + +Represents the API version used. Keeps all the important information +for the version and the methods related to it: + +* release date +* version number +* default selector +* default depth + + +### Device + +This class identifies the switch. It keeps all the basic internal parameters +which defines the device identity, like serial number and name, but, most importantly, +the device's capacities and capabilities. The latter parameters allow to decide +what kind of operations can be performed on a switch, given some features are +available on a certain family of devices. + +* Name +* Serial +* Capacities +* Capabilities + +### Configuration + +This class represents a Device's configuration and all of its attributes. It's used to +configure the device, get full config structure, backup configuration, among other things. + + +### Error handling + +Leveraging the Python infrastructure, the exceptions are the best +mechanism to handle the errors. An exception may report internal details +about the issue found, by returning error codes and descriptive error strings. + +The following hierarchical organization will help classify the possible errors, +and, furthermore, let the SDK client know what exactly happened when calling +a function. + +#### Exceptions + +1. GenericOperationError +2. ResponseError +3. VerificationError diff --git a/pyaoscx/LICENSE b/pyaoscx/LICENSE new file mode 100644 index 0000000..8dada3e --- /dev/null +++ b/pyaoscx/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pyaoscx/README.md b/pyaoscx/README.md new file mode 100644 index 0000000..e4bfca4 --- /dev/null +++ b/pyaoscx/README.md @@ -0,0 +1,118 @@ +# pyaoscx + +These modules are written for AOS-CX API v1 and v10.04. These scripts are written for devices running AOS-CX firmware +version 10.04 or greater. + +See the [Release Notes](../RELEASE-NOTES.md) for more information. + +Please Note that pyaoscx v2 is not backwards compatible for pyaoscx v1 and earlier, so please specify the correct +version when using pyaoscx in requirements.txt files + + +## Structure +Detailed information about the structure and design can be found in the [Design document](DESIGN.md). + +* Classes are found in the modules in /pyaoscx. +* REST API call functions are combined into other functions that emulate + low-level processes. These low-level process functions are also placed + in files in /pyaoscx. +* Functions from the /pyaoscx files (API functions and low-level functions) + are combined to emulate larger network configuration processes (workflows). + These workflow scripts stored in the /workflows folder. + +## How to contribute + +Please, see the accompanying [CONTRIBUTING.md](CONTRIBUTING.md) file for +guidelines on how to contribute to this repository. + +## Git Workflow + +This repo adheres to the 'shared repo' git workflow: + +1. Clone the repo to a local machine: + + ```git clone ``` + +1. Checkout a local working branch: + + ```git checkout -b ``` + +1. Add and amend files in the local working branch: + + ```git add ``` + +1. Commit regularly. Each commit should encompass a single logical change to + the repo (e.g. adding a new function in /pyaoscx is one commit; writing + docstrings for all functions in a module is another commit). Include an + explanatory message with each commit: + + ```git commit -m ""``` + +1. Push commits to github.hpe.com: + + ```git push origin ``` + +1. Merge changes using a Pull Request on github.hpe.com. Ensure the request has + a relevant title and additional comments if necessary. PRs should be raised + regularly once code is tested and the user satisfied that it is ready for + submission. Do not put off creaing a PR until a whole project is complete. + The larger the PR, the difficult it is to successfully merge. + +## Setup + +Before starting ensure the switch REST API is enabled. Instructions for +checking and changing whether or not the REST API is enabled status are +available in the *ArubaOS-CX Rest API Guide*. + +This includes making sure each device has an administrator account with a +password, and each device has https-server rest access-mode read-write and +enabled on the reachable VRF. + +### How to run this code + +In order to run the workflow scripts, please complete the steps below: + +1. Install `virtual env` (refer to [Virtual Environment documentation](#1). + Make sure python version 3 is installed in system. + + ```bash + $ python3 -m venv switchenv + $ + ``` + +1. Activate the virtual env + + ```bash + $ source switchenv/bin/activate + $ + ``` + + In Windows: + + ```bash + > venv/Scripts/activate.bat + ``` + +1. Install the pyaoscx package + + ```bash + (switchenv)$ pip3 install pyaoscx + ``` + +1. Now you can run different workflows from pyaoscx/workflows + (e.g. `print_system_info.py`) + +1. Keep in mind that the workflows perform high-level configuration processes; + they are highly dependent on the configuration already on the switch prior + to running the workflows. For this reason, the comment at the top of each + workflow script describes any necessary preconditions. + +## Troubleshooting Issues + +1. If you encounter module import errors, make sure that the package has been + installed correctly. + +Additionally, please read the [Release notes](RELEASE-NOTES.md) for the current +release information and known issues. + +[#1]: https://docs.python.org/3/library/venv.html \ No newline at end of file diff --git a/pyaoscx/access_security.py b/pyaoscx/access_security.py deleted file mode 100644 index 4d670a7..0000000 --- a/pyaoscx/access_security.py +++ /dev/null @@ -1,1822 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops -from pyaoscx import system -from pyaoscx import interface -from pyaoscx import port -from pyaoscx import vrf - -import json -import logging - - -def create_radius_host_config(vrf_name, host, default_group_priority=1, groups=[], passkey=None, **kwargs): - """ - Perform a POST call to set the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param default_group_priority: Integer priority within the default RADIUS server group. All RADIUS servers will be - added to this default group. The priority must be at least 1, and defaults to 1 if not specified. - :param groups: Optional list of additional RADIUS server groups to which this server will be added. Defaults to - empty list if not specified. - :param passkey: Optional passkey to be used between RADIUS client and server for authentication. - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_radius_host_config_v1(vrf_name, host, default_group_priority, groups, passkey, **kwargs) - else: # Updated else for when version is v10.04 - return _create_radius_host_config(vrf_name, host, default_group_priority, groups, passkey, **kwargs) - - -def _create_radius_host_config_v1(vrf_name, host, default_group_priority=1, groups=[], passkey=None, **kwargs): - """ - Perform a POST call to set the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param default_group_priority: Integer priority within the default RADIUS server group. All RADIUS servers will be - added to this default group. The priority must be at least 1, and defaults to 1 if not specified. - :param groups: Optional list of additional RADIUS server groups to which this server will be added. Defaults to - empty list if not specified. - :param passkey: Optional passkey to be used between RADIUS client and server for authentication. - :return: True if successful, False otherwise - """ - - if default_group_priority < 1: - raise Exception("Default group priority must be at least 1!") - - radius_server_data = {"address": host, - "vrf": "/rest/v1/system/vrfs/%s" % vrf_name, - "default_group_priority": default_group_priority, - "group": ["/rest/v1/system/aaa_server_groups/radius"] + ["/rest/v1/system/aaa_server_groups/%s" % group for group in groups], - } - - if passkey is not None: - radius_server_data['passkey'] = passkey - - target_url = kwargs["url"] + "system/vrfs/default/radius_servers" - post_data = json.dumps(radius_server_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring RADIUS server host to '%s' failed with status code %d: %s" - % (host, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring RADIUS server host to '%s' succeeded" % host) - return True - - -def _create_radius_host_config(vrf_name, host, default_group_priority=1, groups=[], passkey=None, **kwargs): - """ - Perform a POST call to set the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param default_group_priority: Integer priority within the default RADIUS server group. All RADIUS servers will be - added to this default group. The priority must be at least 1, and defaults to 1 if not specified. - :param groups: Optional list of additional RADIUS server groups to which this server will be added. Defaults to - empty list if not specified. - :param passkey: Optional passkey to be used between RADIUS client and server for authentication. - :return: True if successful, False otherwise - """ - - if default_group_priority < 1: - raise Exception("Default group priority must be at least 1!") - - radius_server_data = {"address": host, - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf_name, - "default_group_priority": default_group_priority, - "group": ["/rest/v10.04/system/aaa_server_groups/radius"] + [ - "/rest/v10.04/system/aaa_server_groups/%s" % group for group in groups], - } - - if passkey is not None: - radius_server_data['passkey'] = passkey - - target_url = kwargs["url"] + "system/vrfs/default/radius_servers" - post_data = json.dumps(radius_server_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring RADIUS server host to '%s' failed with status code %d: %s" - % (host, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring RADIUS server host to '%s' succeeded" % host) - return True - - -def delete_radius_host_config(vrf_name, host, udp_port=1812, **kwargs): - """ - Perform a DELETE call to remove the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param udp_port: UDP port number used for authentication. Defaults to 1812 if not specified. - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_radius_host_config_v1(vrf_name, host, udp_port, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_radius_host_config(vrf_name, host, udp_port, **kwargs) - - -def _delete_radius_host_config_v1(vrf_name, host, udp_port=1812, **kwargs): - """ - Perform a DELETE call to remove the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param udp_port: UDP port number used for authentication. Defaults to 1812 if not specified. - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "system/vrfs/%s/radius_servers/%s/%d" % (vrf_name, host, udp_port) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing RADIUS server host failed with status code %d: %s" % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing RADIUS server host succeeded") - return True - - -def _delete_radius_host_config(vrf_name, host, udp_port=1812, **kwargs): - """ - Perform a DELETE call to remove the RADIUS server host. - - :param vrf_name: Alphanumeric name of VRF through which the RADIUS server is reachable - :param host: IPv4/IPv6 address or FQDN of the RADIUS server - :param udp_port: UDP port number used for authentication. Defaults to 1812 if not specified. - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "system/vrfs/%s/radius_servers/%s,%d" % (vrf_name, host, udp_port) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing RADIUS server host failed with status code %d: %s" % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing RADIUS server host succeeded") - return True - - -def enable_disable_dot1x_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable 802.1X globally - - :param enable: True if 802.1x to be enabled globally, False if 802.1x to be disabled globally. Defaults to True - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _enable_disable_dot1x_globally_v1(enable, **kwargs) - else: # Updated else for when version is v10.04 - return _enable_disable_dot1x_globally(enable, **kwargs) - - -def _enable_disable_dot1x_globally_v1(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable 802.1X globally - - :param enable: True if 802.1x to be enabled globally, False if 802.1x to be disabled globally. Defaults to True - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['aaa']['dot1x_auth_enable'] = enable - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting 802.1x authentication enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting 802.1x authentication enabled globally to '%s' succeeded" % enable) - return True - - -def _enable_disable_dot1x_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable 802.1X globally - - :param enable: True if 802.1x to be enabled globally, False if 802.1x to be disabled globally. Defaults to True - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['aaa']['dot1x_auth_enable'] = enable - - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=2) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting 802.1x authentication enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting 802.1x authentication enabled globally to '%s' succeeded" % enable) - return True - - -def configure_dot1x_interface(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, eapol_timeout=None, max_requests=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set 802.1x authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if 802.1x is to be enabled on the port, false otherwise. Defaults to True if not specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. - Defaults to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param eapol_timeout: Time period(in seconds) to wait for a response from a client before retransmitting an - EAPOL PDU. If the value is not set the time period is calculated as per RFC 2988. Defaults to nothing if not - specified. - :param max_requests: Number of EAPOL requests to supplicant before authentication fails. Applicable for 802.1X only. - Defaults to nothing if not specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _configure_dot1x_interface_v1(port_name, auth_enable, cached_reauth_enable, cached_reauth_period, - discovery_period, eapol_timeout, max_requests, max_retries, - quiet_period, reauth_enable, reauth_period, **kwargs) - else: # Updated else for when version is v10.04 - return _configure_dot1x_interface(port_name, auth_enable, cached_reauth_enable, cached_reauth_period, - discovery_period, eapol_timeout, max_requests, max_retries, - quiet_period, reauth_enable, reauth_period, **kwargs) - - -def _configure_dot1x_interface_v1(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, eapol_timeout=None, max_requests=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set 802.1x authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if 802.1x is to be enabled on the port, false otherwise. Defaults to True if not specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. - Defaults to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param eapol_timeout: Time period(in seconds) to wait for a response from a client before retransmitting an - EAPOL PDU. If the value is not set the time period is calculated as per RFC 2988. Defaults to nothing if not - specified. - :param max_requests: Number of EAPOL requests to supplicant before authentication fails. Applicable for 802.1X only. - Defaults to nothing if not specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_access_auth_data = { - "authentication_method": "dot1x", - "auth_enable": auth_enable, - "cached_reauth_enable": cached_reauth_enable, - "reauth_enable": reauth_enable - } - - if cached_reauth_period is not None: - port_access_auth_data['cached_reauth_period'] = cached_reauth_period - - if discovery_period is not None: - port_access_auth_data['discovery_period'] = discovery_period - - if eapol_timeout is not None: - port_access_auth_data['eapol_timeout'] = eapol_timeout - - if max_requests is not None: - port_access_auth_data['max_requests'] = max_requests - - if max_retries is not None: - port_access_auth_data['max_retries'] = max_retries - - if quiet_period is not None: - port_access_auth_data['quiet_period'] = quiet_period - - if reauth_period is not None: - port_access_auth_data['reauth_period'] = reauth_period - - target_url = kwargs["url"] + "system/ports/%s/port_access_auth_configurations" % port_name_percents - post_data = json.dumps(port_access_auth_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring 802.1x for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring 802.1x for Port '%s' succeeded" % port_name) - return True - - -def _configure_dot1x_interface(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, eapol_timeout=None, max_requests=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set 802.1x authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if 802.1x is to be enabled on the port, false otherwise. Defaults to True if not specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. - Defaults to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param eapol_timeout: Time period(in seconds) to wait for a response from a client before retransmitting an - EAPOL PDU. If the value is not set the time period is calculated as per RFC 2988. Defaults to nothing if not - specified. - :param max_requests: Number of EAPOL requests to supplicant before authentication fails. Applicable for 802.1X only. - Defaults to nothing if not specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_access_auth_data = { - "authentication_method": "dot1x", - "auth_enable": auth_enable, - "cached_reauth_enable": cached_reauth_enable, - "reauth_enable": reauth_enable - } - - if cached_reauth_period is not None: - port_access_auth_data['cached_reauth_period'] = cached_reauth_period - - if discovery_period is not None: - port_access_auth_data['discovery_period'] = discovery_period - - if eapol_timeout is not None: - port_access_auth_data['eapol_timeout'] = eapol_timeout - - if max_requests is not None: - port_access_auth_data['max_requests'] = max_requests - - if max_retries is not None: - port_access_auth_data['max_retries'] = max_retries - - if quiet_period is not None: - port_access_auth_data['quiet_period'] = quiet_period - - if reauth_period is not None: - port_access_auth_data['reauth_period'] = reauth_period - - target_url = kwargs["url"] + "system/interfaces/%s/port_access_auth_configurations" % port_name_percents - post_data = json.dumps(port_access_auth_data) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring 802.1x for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring 802.1x for Port '%s' succeeded" % port_name) - return True - - -def enable_disable_mac_auth_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable MAC authentication globally - - :param enable: True if MAC authentication to be enabled globally, False if MAC authentication to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _enable_disable_mac_auth_globally_v1(enable, **kwargs) - else: # Updated else for when version is v10.04 - return _enable_disable_mac_auth_globally(enable, **kwargs) - - -def _enable_disable_mac_auth_globally_v1(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable MAC authentication globally - - :param enable: True if MAC authentication to be enabled globally, False if MAC authentication to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['aaa']['mac_auth_enable'] = enable - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting MAC authentication enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting MAC authentication enabled globally to '%s' succeeded" % enable) - return True - - -def _enable_disable_mac_auth_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable MAC authentication globally - - :param enable: True if MAC authentication to be enabled globally, False if MAC authentication to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['aaa']['mac_auth_enable'] = enable - - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting MAC authentication enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting MAC authentication enabled globally to '%s' succeeded" % enable) - return True - - -def configure_mac_auth_interface(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set MAC authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if authentication is to be enabled on the port, false otherwise. Defaults to True if not - specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. Defaults - to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _configure_mac_auth_interface_v1(port_name, auth_enable, cached_reauth_enable, cached_reauth_period, - discovery_period, max_retries, - quiet_period, reauth_enable, reauth_period, **kwargs) - else: # Updated else for when version is v10.04 - return _configure_mac_auth_interface(port_name, auth_enable, cached_reauth_enable, cached_reauth_period, - discovery_period, max_retries, - quiet_period, reauth_enable, reauth_period, **kwargs) - - -def _configure_mac_auth_interface_v1(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set MAC authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if authentication is to be enabled on the port, false otherwise. Defaults to True if not - specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. Defaults - to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_access_auth_data = { - "authentication_method": "mac-auth", - "auth_enable": auth_enable, - "cached_reauth_enable": cached_reauth_enable, - "reauth_enable": reauth_enable - } - - if cached_reauth_period is not None: - port_access_auth_data['cached_reauth_period'] = cached_reauth_period - - if discovery_period is not None: - port_access_auth_data['discovery_period'] = discovery_period - - if max_retries is not None: - port_access_auth_data['max_retries'] = max_retries - - if quiet_period is not None: - port_access_auth_data['quiet_period'] = quiet_period - - if reauth_period is not None: - port_access_auth_data['reauth_period'] = reauth_period - - target_url = kwargs["url"] + "system/ports/%s/port_access_auth_configurations" % port_name_percents - post_data = json.dumps(port_access_auth_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring MAC authentication for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring MAC authentication for Port '%s' succeeded" % port_name) - return True - - -def _configure_mac_auth_interface(port_name, auth_enable=True, cached_reauth_enable=True, cached_reauth_period=None, - discovery_period=None, max_retries=None, - quiet_period=None, reauth_enable=True, reauth_period=None, **kwargs): - """ - Perform a POST call to set MAC authentication on a port. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param auth_enable: True if authentication is to be enabled on the port, false otherwise. Defaults to True if not - specified. - :param cached_reauth_enable: True if cached reauthentication is to be enabled on the port, false otherwise. - Defaults to True if not specified. - :param cached_reauth_period: Time in seconds during which cached reauthentication is allowed on the port. Defaults - to nothing if not specified. - :param discovery_period: Time period(in seconds) to wait before an EAPOL request identity frame re-transmission on - an 802.1X enabled port with no authenticated client. Applicable for 802.1X only. Defaults to nothing if not - specified. - :param max_retries: Number of authentication attempts before authentication fails. Defaults to nothing if not - specified. - :param quiet_period: Time period(in seconds) to wait before processing an authentication request from a client - that failed authentication. Defaults to nothing if not specified. - :param reauth_enable: True if periodic reauthentication is to be enabled on the port, false otherwise. Defaults to - True if not specified. - :param reauth_period: Time period(in seconds) to enforce periodic re-authentication of clients. Defaults to nothing - if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_access_auth_data = { - "authentication_method": "mac-auth", - "auth_enable": auth_enable, - "cached_reauth_enable": cached_reauth_enable, - "reauth_enable": reauth_enable - } - - if cached_reauth_period is not None: - port_access_auth_data['cached_reauth_period'] = cached_reauth_period - - if discovery_period is not None: - port_access_auth_data['discovery_period'] = discovery_period - - if max_retries is not None: - port_access_auth_data['max_retries'] = max_retries - - if quiet_period is not None: - port_access_auth_data['quiet_period'] = quiet_period - - if reauth_period is not None: - port_access_auth_data['reauth_period'] = reauth_period - - target_url = kwargs["url"] + "system/interfaces/%s/port_access_auth_configurations" % port_name_percents - post_data = json.dumps(port_access_auth_data) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Configuring MAC authentication for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring MAC authentication for Port '%s' succeeded" % port_name) - return True - - -def enable_disable_port_security_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable port security globally - - :param enable: True if port security to be enabled globally, False if port security to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _enable_disable_port_security_globally_v1(enable, **kwargs) - else: # Updated else for when version is v10.04 - return _enable_disable_port_security_globally(enable, **kwargs) - - -def _enable_disable_port_security_globally_v1(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable port security globally - - :param enable: True if port security to be enabled globally, False if port security to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['port_security_enable'] = enable - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting port security enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting port security enabled globally to '%s' succeeded" % enable) - return True - - -def _enable_disable_port_security_globally(enable=True, **kwargs): - """ - Perform GET and PUT calls to either enable or disable port security globally - - :param enable: True if port security to be enabled globally, False if port security to be disabled globally. Defaults to True if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['port_security_enable'] = enable - - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting port security enabled globally to '%s' failed with status code %d: %s" - % (enable, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting port security enabled globally to '%s' succeeded" % enable) - return True - - -def get_all_auth_methods_interface(port_name, **kwargs): - """ - Perform a GET call to get a list/dict of all authentication methods on a port - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dictionary containing all authentication methods on the port - """ - if kwargs["url"].endswith("/v1/"): - return _get_all_auth_methods_interface_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _get_all_auth_methods_interface(port_name, **kwargs) - - -def _get_all_auth_methods_interface_v1(port_name, **kwargs): - """ - Perform a GET call to get a list/dict of all authentication methods on a port - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dictionary containing all authentication methods on the port - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - target_url = kwargs["url"] + "system/ports/%s/port_access_auth_configurations" % port_name_percents - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dict of all authentication methods on port %s failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - auth_methods = [] - else: - logging.info("SUCCESS: Getting list/dict of all authentication methods on port %s succeeded" % port_name) - auth_methods = response.json() - - return auth_methods - - -def _get_all_auth_methods_interface(port_name, **kwargs): - """ - Perform a GET call to get a dictionary containing all authentication methods on a port - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing all authentication methods on the port - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - target_url = kwargs["url"] + "system/interfaces/%s/port_access_auth_configurations" % port_name_percents - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dict of all authentication methods on port %s failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - auth_methods = [] - else: - logging.info("SUCCESS: Getting dict of all authentication methods on port %s succeeded" % port_name) - auth_methods = response.json() - - return auth_methods - - -def remove_auth_method_interface(port_name, auth_method, **kwargs): - """ - Perform a DELETE call to remove an authentication method from a port - - :param port_name: Alphanumeric name of the Port on which the authentication method is to be removed - :param auth_method: Authentication method to be removed from the Port. Should be either "802.1x" or "mac-auth" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _remove_auth_method_interface_v1(port_name, auth_method, **kwargs) - else: # Updated else for when version is v10.04 - return _remove_auth_method_interface(port_name, auth_method, **kwargs) - - -def _remove_auth_method_interface_v1(port_name, auth_method, **kwargs): - """ - Perform a DELETE call to remove an authentication method from a port - - :param port_name: Alphanumeric name of the Port on which the authentication method is to be removed - :param auth_method: Authentication method to be removed from the Port. Should be either "802.1x" or "mac-auth" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - port_name_percents = common_ops._replace_special_characters(port_name) - - auth_methods = get_all_auth_methods_interface(port_name, **kwargs) - - if auth_method in auth_methods: - - target_url = kwargs["url"] + "system/ports/%s/port_access_auth_configurations/%s" \ - % (port_name_percents, auth_method) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing authentication method '%s' from Port '%s' failed with status code %d: %s" - % (auth_method, port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing authentication method '%s' from Port '%s' succeeded" % (auth_method, port_name)) - return True - else: - logging.info("SUCCESS: No need to remove authentication method '%s' from Port '%s' since it doesn't exist" - % (auth_method, port_name)) - return True - - -def _remove_auth_method_interface(port_name, auth_method, **kwargs): - """ - Perform a DELETE call to remove an authentication method from a port - - :param port_name: Alphanumeric name of the Port on which the authentication method is to be removed - :param auth_method: Authentication method to be removed from the Port. Should be either "802.1x" or "mac-auth" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - port_name_percents = common_ops._replace_special_characters(port_name) - - auth_methods = get_all_auth_methods_interface(port_name, **kwargs) - - if auth_method in auth_methods: - - target_url = kwargs["url"] + "system/interfaces/%s/port_access_auth_configurations/%s" \ - % (port_name_percents, auth_method) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing authentication method '%s' from Port '%s' failed with status code %d: %s" - % (auth_method, port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing authentication method '%s' from Port '%s' succeeded" % (auth_method, port_name)) - return True - else: - logging.info("SUCCESS: No need to remove authentication method '%s' from Port '%s' since it doesn't exist" - % (auth_method, port_name)) - return True - - -def set_ubt_client_vlan(vlan_id, **kwargs): - """ - Perform GET and PUT calls to set the reserved VLAN for tunneled clients. - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _set_ubt_client_vlan_v1(vlan_id, **kwargs) - else: # Updated else for when version is v10.04 - return _set_ubt_client_vlan(vlan_id, **kwargs) - - -def _set_ubt_client_vlan_v1(vlan_id, **kwargs): - """ - Perform GET and PUT calls to set the reserved VLAN for tunneled clients. - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['ubt_client_vid'] = "/rest/v1/system/vlans/%d" % vlan_id - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting the VLAN reserved for tunneled clients to '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting the VLAN reserved for tunneled clients to '%d' succeeded" % vlan_id) - return True - - -def _set_ubt_client_vlan(vlan_id, **kwargs): - """ - Perform GET and PUT calls to set the reserved VLAN for tunneled clients. - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['ubt_client_vid'] = "/rest/v10.04/system/vlans/%d" % vlan_id - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting the VLAN reserved for tunneled clients to '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting the VLAN reserved for tunneled clients to '%d' succeeded" % vlan_id) - return True - - -def create_ubt_zone(zone_name, vrf_name, enable=True, pri_ctrlr_ip_addr=None, backup_ctrlr_ip_addr=None, - sac_heartbeat_interval=1, uac_keepalive_interval=60, papi_security_key=None, **kwargs): - """ - Perform a POST call to create User-Based-Tunneling (UBT) zone on a VRF - - :param zone_name: Alphanumeric name of UBT zone - :param vrf_name: Alphanumeric name of VRF - :param enable: True if UBT functionality to be enabled on this zone, False otherwise. Default to True if not - specified. - :param pri_ctrlr_ip_addr: IP address of primary controller node. Defaults to nothing if not specified. - :param backup_ctrlr_ip_addr: IP address of backup controller node. Defaults to nothing if not specified. - :param sac_heartbeat_interval: Time interval (in seconds) between successive heartbeat messages to the switch - anchor node. Defaults to 1 if not specified. - :param uac_keepalive_interval: Time interval (in seconds) between successive keep-alive messages sent to the user - anchor node. Defaults to 60 if not specified. - :param papi_security_key: Shared security key used to encrypt UBT PAPI messages exchanged between the switch and the - controller cluster corresponding to this zone. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_ubt_zone_v1(zone_name, vrf_name, enable, pri_ctrlr_ip_addr, backup_ctrlr_ip_addr, - sac_heartbeat_interval, uac_keepalive_interval, papi_security_key, **kwargs) - else: # Updated else for when version is v10.04 - return _create_ubt_zone(zone_name, vrf_name, enable, pri_ctrlr_ip_addr, backup_ctrlr_ip_addr, - sac_heartbeat_interval, uac_keepalive_interval, papi_security_key, **kwargs) - - -def _create_ubt_zone_v1(zone_name, vrf_name, enable=True, pri_ctrlr_ip_addr=None, backup_ctrlr_ip_addr=None, - sac_heartbeat_interval=1, uac_keepalive_interval=60, papi_security_key=None, **kwargs): - """ - Perform a POST call to create User-Based-Tunneling (UBT) zone on a VRF - - :param zone_name: Alphanumeric name of UBT zone - :param vrf_name: Alphanumeric name of VRF - :param enable: True if UBT functionality to be enabled on this zone, False otherwise. Default to True if not - specified. - :param pri_ctrlr_ip_addr: IP address of primary controller node. Defaults to nothing if not specified. - :param backup_ctrlr_ip_addr: IP address of backup controller node. Defaults to nothing if not specified. - :param sac_heartbeat_interval: Time interval (in seconds) between successive heartbeat messages to the switch - anchor node. Defaults to 1 if not specified. - :param uac_keepalive_interval: Time interval (in seconds) between successive keep-alive messages sent to the user - anchor node. Defaults to 60 if not specified. - :param papi_security_key: Shared security key used to encrypt UBT PAPI messages exchanged between the switch and the - controller cluster corresponding to this zone. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ubt_zone_data = { - "enable": enable, - "name": zone_name, - "vrf": "/rest/v1/system/vrfs/%s" % vrf_name, - "controller_nodes": {} - } - - if pri_ctrlr_ip_addr is not None: - ubt_zone_data['controller_nodes']['primary'] = pri_ctrlr_ip_addr - - if backup_ctrlr_ip_addr is not None: - ubt_zone_data['controller_nodes']['backup'] = backup_ctrlr_ip_addr - - if sac_heartbeat_interval is not None: - ubt_zone_data['sac_heartbeat_interval'] = sac_heartbeat_interval - - if uac_keepalive_interval is not None: - ubt_zone_data['uac_keepalive_interval'] = uac_keepalive_interval - - if papi_security_key is not None: - ubt_zone_data['papi_security_key'] = papi_security_key - - target_url = kwargs["url"] + "system/vrfs/%s/ubt_zone" % vrf_name - post_data = json.dumps(ubt_zone_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating UBT zone '%s' on VRF '%s' failed with status code %d: %s" - % (zone_name, vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating UBT zone '%s' on VRF '%s' succeeded" % (zone_name, vrf_name)) - return True - - -def _create_ubt_zone(zone_name, vrf_name, enable=True, pri_ctrlr_ip_addr=None, backup_ctrlr_ip_addr=None, - sac_heartbeat_interval=1, uac_keepalive_interval=60, papi_security_key=None, **kwargs): - """ - Perform a POST call to create User-Based-Tunneling (UBT) zone on a VRF - - :param zone_name: Alphanumeric name of UBT zone - :param vrf_name: Alphanumeric name of VRF - :param enable: True if UBT functionality to be enabled on this zone, False otherwise. Default to True if not - specified. - :param pri_ctrlr_ip_addr: IP address of primary controller node. Defaults to nothing if not specified. - :param backup_ctrlr_ip_addr: IP address of backup controller node. Defaults to nothing if not specified. - :param sac_heartbeat_interval: Time interval (in seconds) between successive heartbeat messages to the switch - anchor node. Defaults to 1 if not specified. - :param uac_keepalive_interval: Time interval (in seconds) between successive keep-alive messages sent to the user - anchor node. Defaults to 60 if not specified. - :param papi_security_key: Shared security key used to encrypt UBT PAPI messages exchanged between the switch and the - controller cluster corresponding to this zone. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ubt_zone_data = { - "enable": enable, - "name": zone_name, - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf_name, - "controller_nodes": {} - } - - if pri_ctrlr_ip_addr is not None: - ubt_zone_data['controller_nodes']['primary'] = pri_ctrlr_ip_addr - - if backup_ctrlr_ip_addr is not None: - ubt_zone_data['controller_nodes']['backup'] = backup_ctrlr_ip_addr - - if sac_heartbeat_interval is not None: - ubt_zone_data['sac_heartbeat_interval'] = sac_heartbeat_interval - - if uac_keepalive_interval is not None: - ubt_zone_data['uac_keepalive_interval'] = uac_keepalive_interval - - if papi_security_key is not None: - ubt_zone_data['papi_security_key'] = papi_security_key - - target_url = kwargs["url"] + "system/vrfs/%s/ubt_zone" % vrf_name - post_data = json.dumps(ubt_zone_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating UBT zone '%s' on VRF '%s' failed with status code %d: %s" - % (zone_name, vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating UBT zone '%s' on VRF '%s' succeeded" % (zone_name, vrf_name)) - return True - - -def create_port_access_role(role_name, desc=None, gateway_zone=None, ubt_gateway_role=None, vlan_mode=None, - vlan_tag=None, vlan_trunks=None, **kwargs): - """ - Perform a POST call to create a port access role - - :param role_name: Alphanumeric name of port access role - :param desc: Optional description for role. Defaults to nothing if not specified. - :param gateway_zone: Gateway zone associated with this role. Defaults to nothing if not specified. - :param ubt_gateway_role: Role to be assigned to tunneled clients on the UBT cluster side. Defaults to nothing if not - specified. - :param vlan_mode: VLAN mode should be one of "access," "native-tagged," "native-untagged," or "trunk." Defaults to - nothing if not specified. - :param vlan_tag: The untagged VLAN to which users of this access role has to be assigned to. - :param vlan_trunks: The tagged VLAN(s) to which users of this access role has to be assigned to. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_port_access_role_v1(role_name, desc, gateway_zone, ubt_gateway_role, vlan_mode, - vlan_tag, vlan_trunks, **kwargs) - else: # Updated else for when version is v10.04 - return _create_port_access_role(role_name, desc, gateway_zone, ubt_gateway_role, vlan_mode, - vlan_tag, vlan_trunks, **kwargs) - - -def _create_port_access_role_v1(role_name, desc=None, gateway_zone=None, ubt_gateway_role=None, vlan_mode=None, - vlan_tag=None, vlan_trunks=None, **kwargs): - """ - Perform a POST call to create a port access role - - :param role_name: Alphanumeric name of port access role - :param desc: Optional description for role. Defaults to nothing if not specified. - :param gateway_zone: Gateway zone associated with this role. Defaults to nothing if not specified. - :param ubt_gateway_role: Role to be assigned to tunneled clients on the UBT cluster side. Defaults to nothing if not - specified. - :param vlan_mode: VLAN mode should be one of "access," "native-tagged," "native-untagged," or "trunk." Defaults to - nothing if not specified. - :param vlan_tag: The untagged VLAN to which users of this access role has to be assigned to. - :param vlan_trunks: The tagged VLAN(s) to which users of this access role has to be assigned to. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - - role_data = { - "name": role_name, - } - - if desc is not None: - role_data['desc'] = desc - - if gateway_zone is not None: - role_data['gateway_zone'] = gateway_zone - - if ubt_gateway_role is not None: - role_data['ubt_gateway_role'] = ubt_gateway_role - - if vlan_mode is not None: - role_data['vlan_mode'] = vlan_mode - - if vlan_tag is not None: - role_data['vlan_tag'] = vlan_tag - - if vlan_trunks is not None: - role_data['vlan_trunks'] = vlan_trunks - - target_url = kwargs["url"] + "system/port_access_roles" - post_data = json.dumps(role_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating port access role '%s' failed with status code %d: %s" - % (role_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating port access role '%s' succeeded" % role_name) - return True - - -def _create_port_access_role(role_name, desc=None, gateway_zone=None, ubt_gateway_role=None, vlan_mode=None, - vlan_tag=None, vlan_trunks=None, **kwargs): - """ - Perform a POST call to create a port access role - - :param role_name: Alphanumeric name of port access role - :param desc: Optional description for role. Defaults to nothing if not specified. - :param gateway_zone: Gateway zone associated with this role. Defaults to nothing if not specified. - :param ubt_gateway_role: Role to be assigned to tunneled clients on the UBT cluster side. Defaults to nothing if not - specified. - :param vlan_mode: VLAN mode should be one of "access," "native-tagged," "native-untagged," or "trunk." Defaults to - nothing if not specified. - :param vlan_tag: The untagged VLAN to which users of this access role has to be assigned to. - :param vlan_trunks: The tagged VLAN(s) to which users of this access role has to be assigned to. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - role_data = { - "name": role_name, - } - - if desc is not None: - role_data['desc'] = desc - - if gateway_zone is not None: - role_data['gateway_zone'] = gateway_zone - - if ubt_gateway_role is not None: - role_data['ubt_gateway_role'] = ubt_gateway_role - - if vlan_mode is not None: - role_data['vlan_mode'] = vlan_mode - - if vlan_tag is not None: - role_data['vlan_tag'] = vlan_tag - - if vlan_trunks is not None: - role_data['vlan_trunks'] = vlan_trunks - - target_url = kwargs["url"] + "system/port_access_roles" - post_data = json.dumps(role_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating port access role '%s' failed with status code %d: %s" - % (role_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating port access role '%s' succeeded" % role_name) - return True - - -def set_port_access_clients_limit(port_name, clients_limit, **kwargs): - """ - Perform GET and PUT calls to set a port's maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param clients_limit: Numeric ID of VLAN to add to trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _set_port_access_clients_limit_v1(port_name, clients_limit, **kwargs) - else: # Updated else for when version is v10.04 - return _set_port_access_clients_limit(port_name, clients_limit, **kwargs) - - -def _set_port_access_clients_limit_v1(port_name, clients_limit, **kwargs): - """ - Perform GET and PUT calls to set a port's maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param clients_limit: Numeric ID of VLAN to add to trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - port_data['port_access_clients_limit'] = clients_limit - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting maximum allowable clients limit on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting maximum allowable clients limit on Port '%s' succeeded" - % port_name) - return True - - -def _set_port_access_clients_limit(port_name, clients_limit, **kwargs): - """ - Perform GET and PUT calls to set a port's maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param clients_limit: Numeric ID of VLAN to add to trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name_percents, depth=2, selector="writable", **kwargs) - - int_data.pop('portfilter', None) # Have to remove this because of bug? - - int_data['port_access_clients_limit'] = clients_limit - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting maximum allowable clients limit on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting maximum allowable clients limit on Port '%s' succeeded" - % port_name) - return True - - -def set_source_ip_ubt(vrf_name, source_ip, **kwargs): - """ - Perform GET and PUT calls to set the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param source_ip: IP address for UBT - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _set_source_ip_ubt_v1(vrf_name, source_ip, **kwargs) - else: # Updated else for when version is v10.04 - return _set_source_ip_ubt(vrf_name, source_ip, **kwargs) - - -def _set_source_ip_ubt_v1(vrf_name, source_ip, **kwargs): - """ - Perform GET and PUT calls to set the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param source_ip: IP address for UBT - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - vrf_data = vrf.get_vrf(vrf_name, depth=0, selector="configuration", **kwargs) - - vrf_data['source_ip']['ubt'] = source_ip - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - put_data = json.dumps(vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating UBT source IP address on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating UBT source IP address on VRF '%s' succeeded" - % vrf_name) - return True - - -def _set_source_ip_ubt(vrf_name, source_ip, **kwargs): - """ - Perform GET and PUT calls to set the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param source_ip: IP address for UBT - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - vrf_data = vrf.get_vrf(vrf_name, depth=1, selector="writable", **kwargs) - - vrf_data['source_ip']['ubt'] = source_ip - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - put_data = json.dumps(vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating UBT source IP address on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating UBT source IP address on VRF '%s' succeeded" - % vrf_name) - return True - - -def clear_ubt_client_vlan(**kwargs): - """ - Perform GET and PUT calls to clear the reserved VLAN for tunneled clients. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _clear_ubt_client_vlan_v1(**kwargs) - else: # Updated else for when version is v10.04 - return _clear_ubt_client_vlan(**kwargs) - - -def _clear_ubt_client_vlan_v1(**kwargs): - """ - Perform GET and PUT calls to clear the reserved VLAN for tunneled clients. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data.pop('ubt_client_vid', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing the VLAN reserved for tunneled clients failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing the VLAN reserved for tunneled clients succeeded") - return True - - -def _clear_ubt_client_vlan(**kwargs): - """ - Perform GET and PUT calls to clear the reserved VLAN for tunneled clients. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data.pop('ubt_client_vid', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing the VLAN reserved for tunneled clients failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing the VLAN reserved for tunneled clients succeeded") - return True - - -def remove_ubt_zone(vrf_name, **kwargs): - """ - Perform a DELETE call to delete the User-Based-Tunneling (UBT) zone on a VRF - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _remove_ubt_zone_v1(vrf_name, **kwargs) - else: # Updated else for when version is v10.04 - return _remove_ubt_zone(vrf_name, **kwargs) - - -def _remove_ubt_zone_v1(vrf_name, **kwargs): - """ - Perform a DELETE call to delete the User-Based-Tunneling (UBT) zone on a VRF - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "system/vrfs/%s/ubt_zone" % vrf_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting UBT zone on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting UBT zone on VRF '%s' succeeded" % vrf_name) - return True - - -# same as _remove_ubt_zone_v1 -def _remove_ubt_zone(vrf_name, **kwargs): - """ - Perform a DELETE call to delete the User-Based-Tunneling (UBT) zone on a VRF - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "system/vrfs/%s/ubt_zone" % vrf_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting UBT zone on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting UBT zone on VRF '%s' succeeded" % vrf_name) - return True - - -def remove_port_access_role(role_name, **kwargs): - """ - Perform a DELETE call to delete a port access role - - :param role_name: Alphanumeric name of port access role - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _remove_port_access_role_v1(role_name, **kwargs) - else: # Updated else for when version is v10.04 - return _remove_port_access_role(role_name, **kwargs) - - -def _remove_port_access_role_v1(role_name, **kwargs): - """ - Perform a DELETE call to delete a port access role - - :param role_name: Alphanumeric name of port access role - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "system/port_access_roles/%s" % role_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing port access role '%s' failed with status code %d: %s" - % (role_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing port access role '%s' succeeded" % role_name) - return True - - -# same as _remove_port_access_role_v1 -def _remove_port_access_role(role_name, **kwargs): - """ - Perform a DELETE call to delete a port access role - - :param role_name: Alphanumeric name of port access role - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - target_url = kwargs["url"] + "system/port_access_roles/%s" % role_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Removing port access role '%s' failed with status code %d: %s" - % (role_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing port access role '%s' succeeded" % role_name) - return True - - -def clear_port_access_clients_limit(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a port's limit of maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _clear_port_access_clients_limit_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _clear_port_access_clients_limit(port_name, **kwargs) - - -def _clear_port_access_clients_limit_v1(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a port's limit of maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('port_access_clients_limit', None) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing maximum allowable clients limit on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing maximum allowable clients limit on Port '%s' succeeded" - % port_name) - return True - - -def _clear_port_access_clients_limit(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a port's limit of maximum allowed number of authorized clients. - - :param port_name: Alphanumeric name of Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name_percents, depth=2, selector="writable", **kwargs) - - int_data.pop('port_access_clients_limit', None) - - int_data.pop('portfilter', None) # Have to remove this because of bug - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing maximum allowable clients limit on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing maximum allowable clients limit on Port '%s' succeeded" - % port_name) - return True - - -def remove_source_ip_ubt(vrf_name, **kwargs): - """ - Perform GET and PUT calls to remove the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _remove_source_ip_ubt_v1(vrf_name, **kwargs) - else: # Updated else for when version is v10.04 - return _remove_source_ip_ubt(vrf_name, **kwargs) - - -def _remove_source_ip_ubt_v1(vrf_name, **kwargs): - """ - Perform GET and PUT calls to remove the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrf_data = vrf.get_vrf(vrf_name, depth=0, selector="configuration", **kwargs) - - vrf_data['source_ip'].pop('ubt', None) - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - put_data = json.dumps(vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing UBT source IP address on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing UBT source IP address on VRF '%s' succeeded" - % vrf_name) - return True - - -def _remove_source_ip_ubt(vrf_name, **kwargs): - """ - Perform GET and PUT calls to remove the source IP address for UBT on a VRF. - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - vrf_data = vrf.get_vrf(vrf_name, depth=1, selector="writable", **kwargs) - - vrf_data['source_ip'].pop('ubt', None) - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - put_data = json.dumps(vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing UBT source IP address on VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing UBT source IP address on VRF '%s' succeeded" - % vrf_name) - return True diff --git a/pyaoscx/acl.py b/pyaoscx/acl.py index 50c94e8..16f9564 100644 --- a/pyaoscx/acl.py +++ b/pyaoscx/acl.py @@ -1,977 +1,526 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import common_ops, interface, port +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.utils.connection import connected +from pyaoscx.pyaoscx_module import PyaoscxModule import json -import random import logging - - -def get_all_acls(**kwargs): - """ - Perform a GET call to get a list of all ACLs - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all ACLs in the table - """ - target_url = kwargs["url"] + "system/acls" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all ACLs failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all ACLs succeeded") - - acls_list = response.json() - return acls_list - - -def create_acl(list_name, list_type, **kwargs): - """ - Perform a POST call to create an ACL with no entries - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_acl_v1(list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _create_acl(list_name, list_type, **kwargs) - - -def _create_acl_v1(list_name, list_type, **kwargs): - """ - Perform a POST call to create an ACL with no entries - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - acls_list = get_all_acls(**kwargs) - - # ACL doesn't exist; create it - if "/rest/v1/system/acls/%s/%s" % (list_name, list_type) not in acls_list: - - acl_data = { - "name": list_name, - "list_type": list_type +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class ACL(PyaoscxModule): + ''' + Provide configuration management for ACL on AOS-CX devices. + ''' + + base_uri = "system/acls" + resource_uri_name = 'acls' + + indices = ['name', 'list_type'] + + cfg_aces = ListDescriptor('cfg_aces') + + def __init__(self, session, name, list_type, uri=None, **kwargs): + self.session = session + # Assign IDs + self.name = name + self.list_type = list_type + self._uri = uri + # List used to determine attributes related to the ACL configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Use to manage ACL Entries + self.cfg_aces = [] + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for an ACL table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch ACLs") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector } - target_url = kwargs["url"] + "system/acls" - post_data = json.dumps(acl_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating %s ACL '%s' succeeded" % (list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to create %s ACL '%s' since it already exists" - % (list_type, list_name)) + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=ACL.base_uri, + id1=self.name, + separator=self.session.api_version.compound_index_separator, + id2=self.list_type + ) + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Remove fields because they are not needed for the PUT request + if 'name' in data: + data.pop('name') + if 'list_type' in data: + data.pop('list_type') + # Delete unwanted data + if 'cfg_aces' in data: + data.pop('cfg_aces') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the ACL is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['name', 'list_type']) + + # Set original attributes + self.__original_attributes = data + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + # Clean ACL Entries settings + if self.cfg_aces == []: + # Set ACL Entries if any + # Adds ACL Entries to parent ACL already + from pyaoscx.acl_entry import AclEntry + AclEntry.get_all(self.session, self) return True - -def _create_acl(list_name, list_type, **kwargs): - """ - Perform a POST call to create an ACL with no entries - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - acls_list = get_all_acls(**kwargs) - - acl_key = "{},{}".format(list_name, list_type) - acl_value = "/rest/v10.04/system/acls/" + acl_key - - # ACL doesn't exist; create it - if acl_value not in acls_list.values(): - acl_data = { - "name": list_name, - "list_type": list_type - } - - target_url = kwargs["url"] + "system/acls" - post_data = json.dumps(acl_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system ACLs, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing ACLs IDs as keys and a + Acl objects as values + ''' + + logging.info("Retrieving the switch ACL") + + uri = "{base_url}{class_uri}".format( + base_url=session.base_url, + class_uri=ACL.base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + acl_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a Acl object + indices, acl = ACL.from_uri( + session, uri) + acl_dict[indices] = acl + + return acl_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + ACL table entry. + Checks whether the ACL exists in the switch + Calls self.update() if ACL being updated + Calls self.create() if a new ACL is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + modified = False + if self.materialized: + modified = self.update() else: - logging.info("SUCCESS: Creating %s ACL '%s' succeeded" % (list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to create %s ACL '%s' since it already exists" - % (list_type, list_name)) - return True - - -def get_all_acl_entries(list_name, list_type, **kwargs): - """ - Perform a GET call to get all entries of an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing ACL entry URIs - """ - if kwargs["url"].endswith("/v1/"): - acl_entries = _get_all_acl_entries_v1(list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - acl_entries = _get_all_acl_entries(list_name, list_type, **kwargs) - - return acl_entries - - -def _get_all_acl_entries_v1(list_name, list_type, **kwargs): - """ - Perform a GET call to get all entries of an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing ACL entry URIs - """ - target_url = kwargs["url"] + "system/acls/%s/%s/cfg_aces" % (list_name, list_type) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIS of entries in %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - acl_entries = {} - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in %s ACL '%s' succeeded" % (list_type, list_name)) - acl_entries = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary, - if not acl_entries: - return {} - else: - return acl_entries - - -def _get_all_acl_entries(list_name, list_type, **kwargs): - """ - Perform a GET call to get all entries of an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing ACL entry URIs - """ - - target_url = kwargs["url"] + "system/acls/%s,%s/cfg_aces" % (list_name, list_type) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIS of entries in %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in %s ACL '%s' succeeded" % (list_type, list_name)) - - acl_entries = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary, - if not acl_entries: - return {} - else: - return acl_entries - - -def create_acl_entry(list_name, list_type, sequence_num, action, count=None, ip_protocol=None, src_ip=None, dst_ip=None, - dst_l4_port_min=None, dst_l4_port_max=None, src_mac=None, dst_mac=None, ethertype=None, **kwargs): - """ - Perform a POST call to create an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer number of the sequence - :param action: Action should be either "permit" or "deny" - :param count: Optional boolean flag that when true, will make entry increment hit count for matched packets - :param ip_protocol: Optional integer IP protocol number - :param src_ip: Optional source IP address - :param dst_ip: Optional destination IP address - :param dst_l4_port_min: Optional minimum L4 port number in range; used in conjunction with dst_l4_port_max. - :param dst_l4_port_max: Optional maximum L4 port number in range; used in conjunction with dst_l4_port_min. - :param src_mac: Optional source MAC address - :param dst_mac: Optional destination MAC address - :param ethertype: Optional integer EtherType number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_acl_entry_v1(list_name, list_type, sequence_num, action, count, ip_protocol, src_ip, dst_ip, - dst_l4_port_min, dst_l4_port_max, src_mac, dst_mac, ethertype, **kwargs) - else: # Updated else for when version is v10.04 - return _create_acl_entry(list_name, list_type, sequence_num, action, count, ip_protocol, src_ip, dst_ip, - dst_l4_port_min, dst_l4_port_max, src_mac, dst_mac, ethertype, **kwargs) - - -def _create_acl_entry_v1(list_name, list_type, sequence_num, action, count=None, ip_protocol=None, src_ip=None, dst_ip=None, - dst_l4_port_min=None, dst_l4_port_max=None, src_mac=None, dst_mac=None, ethertype=None, **kwargs): - """ - Perform a POST call to create an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer number of the sequence - :param action: Action should be either "permit" or "deny" - :param count: Optional boolean flag that when true, will make entry increment hit count for matched packets - :param ip_protocol: Optional integer IP protocol number - :param src_ip: Optional source IP address - :param dst_ip: Optional destination IP address - :param dst_l4_port_min: Optional minimum L4 port number in range; used in conjunction with dst_l4_port_max. - :param dst_l4_port_max: Optional maximum L4 port number in range; used in conjunction with dst_l4_port_min. - :param src_mac: Optional source MAC address - :param dst_mac: Optional destination MAC address - :param ethertype: Optional integer EtherType number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - acl_entries_dict = get_all_acl_entries(list_name, list_type, **kwargs) - - if "/rest/v1/system/acls/%s/%s/cfg_aces/%d" % (list_name, list_type, sequence_num) not in acl_entries_dict.values(): - acl_entry_data = { - "sequence_number": sequence_num, - "action": action - } - - if count is not None: - acl_entry_data["count"] = count - - if ip_protocol is not None: - acl_entry_data["protocol"] = ip_protocol - - if src_ip is not None: - acl_entry_data["src_ip"] = src_ip - - if dst_ip is not None: - acl_entry_data["dst_ip"] = dst_ip - - if dst_l4_port_min is not None: - acl_entry_data["dst_l4_port_min"] = dst_l4_port_min - - if dst_l4_port_max is not None: - acl_entry_data["dst_l4_port_max"] = dst_l4_port_max - - if src_mac is not None: - acl_entry_data["src_mac"] = src_mac - - if dst_mac is not None: - acl_entry_data["dst_mac"] = dst_mac - - if ethertype is not None: - acl_entry_data["ethertype"] = ethertype - - target_url = kwargs["url"] + "system/acls/%s/%s/cfg_aces" % (list_name, list_type) - post_data = json.dumps(acl_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for %s ACL '%s' failed with status code %d: %s" - % (sequence_num, list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for %s ACL '%s' succeeded" % (sequence_num, list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for %s ACL '%s' since it already exists" - % (sequence_num, list_type, list_name)) - return True - - -def _create_acl_entry(list_name, list_type, sequence_num, action, count=None, ip_protocol=None, src_ip=None, dst_ip=None, - dst_l4_port_min=None, dst_l4_port_max=None, src_mac=None, dst_mac=None, ethertype=None, **kwargs): - """ - Perform a POST call to create an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer number of the sequence - :param action: Action should be either "permit" or "deny" - :param count: Optional boolean flag that when true, will make entry increment hit count for matched packets - :param ip_protocol: Optional integer IP protocol number - :param src_ip: Optional source IP address - :param dst_ip: Optional destination IP address - :param dst_l4_port_min: Optional minimum L4 port number in range; used in conjunction with dst_l4_port_max. - :param dst_l4_port_max: Optional maximum L4 port number in range; used in conjunction with dst_l4_port_min. - :param src_mac: Optional source MAC address - :param dst_mac: Optional destination MAC address - :param ethertype: Optional integer EtherType number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - acl_entries_dict = get_all_acl_entries(list_name, list_type, **kwargs) - - ace_key = "{},{}".format(list_name, list_type) - ace_value = "/rest/v10.04/system/acls/%s/cfg_aces/%d" % (ace_key, sequence_num) - - if ace_value not in acl_entries_dict.values(): - acl_entry_data = { - "sequence_number": sequence_num, - "action": action, - } - - if count is not None: - acl_entry_data["count"] = count - - if ip_protocol is not None: - acl_entry_data["protocol"] = ip_protocol - - if src_ip is not None: - acl_entry_data["src_ip"] = src_ip - - if dst_ip is not None: - acl_entry_data["dst_ip"] = dst_ip - - if dst_l4_port_min is not None: - acl_entry_data["dst_l4_port_min"] = dst_l4_port_min - - if dst_l4_port_max is not None: - acl_entry_data["dst_l4_port_max"] = dst_l4_port_max - - if src_mac is not None: - acl_entry_data["src_mac"] = src_mac - - if dst_mac is not None: - acl_entry_data["dst_mac"] = dst_mac - - if ethertype is not None: - acl_entry_data["ethertype"] = ethertype - - target_url = kwargs["url"] + "system/acls/%s/cfg_aces" % ace_key - post_data = json.dumps(acl_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for %s ACL '%s' failed with status code %d: %s" - % (sequence_num, list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for %s ACL '%s' succeeded" % (sequence_num, list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for %s ACL '%s' since it already exists" - % (sequence_num, list_type, list_name)) - return True - + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing ACL table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + acl_data = {} + + acl_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=ACL.base_uri, + id1=self.name, + separator=self.session.api_version.compound_index_separator, + id2=self.list_type + ) + + # Compare dictionaries + if acl_data == self.__original_attributes: + # Object was not modified + modified = False -def get_acl(list_name, list_type, **kwargs): - """ - Perform a GET call to get details of a particular ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular ACL - """ - - if kwargs["url"].endswith("/v1/"): - acl = _get_acl_v1(list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - acl = _get_acl(list_name, list_type, **kwargs) - return acl - - -def _get_acl_v1(list_name, list_type, **kwargs): - """ - Perform a GET call to get details of a particular ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular ACL - """ - target_url = kwargs["url"] + "system/acls/%s/%s" % (list_name, list_type) - - payload = {"selector": "configuration"} - - response = kwargs["s"].get(target_url, params=payload, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting %s ACL '%s' succeeded" % (list_type, list_name)) - - acl = response.json() - return acl - - -def _get_acl(list_name, list_type, **kwargs): - """ - Perform a GET call to get details of a particular ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular ACL - """ - acl_key = "{},{}".format(list_name, list_type) - target_url = kwargs["url"] + "system/acls/%s?depth=2&selector=writable" % acl_key - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting %s ACL '%s' succeeded" % (list_type, list_name)) - - acl = response.json() - return acl - - -def update_acl(list_name, list_type, **kwargs): - """ - Perform a PUT call to version-up an ACL. This is required whenever entries of an ACL are changed - in any way. - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_acl_v1(list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _update_acl(list_name, list_type, **kwargs) - - -def _update_acl_v1(list_name, list_type, **kwargs): - """ - Perform a PUT call to version-up an ACL. This is required whenever entries of an ACL are changed - in any way. - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - acl_data = get_acl(list_name, list_type, **kwargs) - - # must remove these fields from the data since they can't be modified - acl_data.pop('name', None) - acl_data.pop('list_type', None) - - acl_data['cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - - target_url = kwargs["url"] + "system/acls/%s/%s" % (list_name, list_type) - put_data = json.dumps(acl_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s ACL '%s' succeeded" % (list_type, list_name)) - return True - - -def _update_acl(list_name, list_type, **kwargs): - """ - Perform a PUT call to version-up an ACL. This is required whenever entries of an ACL are changed - in any way. - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - acl_data = get_acl(list_name, list_type, **kwargs) - acl_key = "{},{}".format(list_name, list_type) - - acl_data['cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - target_url = kwargs["url"] + "system/acls/%s" % acl_key - put_data = json.dumps(acl_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s ACL '%s' succeeded" % (list_type, list_name)) - return True - - -def delete_acl(list_name, list_type, **kwargs): - """ - Perform a DELETE call to delete an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_acl_v1(list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_acl(list_name, list_type, **kwargs) - - -def _delete_acl_v1(list_name, list_type, **kwargs): - """ - Perform a DELETE call to delete an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - acls_list = get_all_acls(**kwargs) - - if "/rest/v1/system/acls/%s/%s" % (list_name, list_type) in acls_list: - - target_url = kwargs["url"] + "system/acls/%s/%s" % (list_name, list_type) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting %s ACL '%s' succeeded" % (list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to delete %s ACL '%s' since it doesn't exist" - % (list_type, list_name)) - return True - - -def _delete_acl(list_name, list_type, **kwargs): - """ - Perform a DELETE call to delete an ACL - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - acls_list = get_all_acls(**kwargs) - - acl_key = "{},{}".format(list_name, list_type) - acl_value = "/rest/v10.04/system/acls/" + acl_key + post_data = json.dumps(acl_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update ACL table entry {} succeeded") + # Set new original attributes + self.__original_attributes = acl_data + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new ACL table entry + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created. + ''' + + acl_data = {} + + acl_data = utils.get_attrs(self, self.config_attrs) + acl_data['name'] = self.name + acl_data['list_type'] = self.list_type + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=ACL.base_uri + ) + post_data = json.dumps(acl_data, sort_keys=True, indent=4) - # ACL exists; delete it - if acl_value in acls_list.values(): + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) - target_url = kwargs["url"] + "system/acls/%s,%s" % (list_name, list_type) + except Exception as e: + raise ResponseError('POST', e) - response = kwargs["s"].delete(target_url, verify=False) + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting %s ACL '%s' failed with status code %d: %s" - % (list_type, list_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting %s ACL '%s' succeeded" % (list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to delete %s ACL '%s' since it doesn't exist" - % (list_type, list_name)) - return True - - -def delete_acl_entry(list_name, list_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_acl_entry_v1(list_name, list_type, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_acl_entry(list_name, list_type, sequence_num, **kwargs) - - -def _delete_acl_entry_v1(list_name, list_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ + logging.info( + "SUCCESS: Adding ACL table entry {} succeeded\ + ".format(self.name)) - acl_entries_dict = get_all_acl_entries(list_name, list_type, **kwargs) + # Get all object's data + self.get() - if "/rest/v1/system/acls/%s/%s/cfg_aces/%d" % (list_name, list_type, sequence_num) in acl_entries_dict.values(): - - target_url = kwargs["url"] + "system/acls/%s/%s/cfg_aces/%d" % (list_name, list_type, sequence_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in %s ACL '%s' failed with status code %d: %s" - % (sequence_num, list_type, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in %s ACL '%s' succeeded" - % (sequence_num, list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in %s ACL '%s' since it doesn't exist" - % (sequence_num, list_type, list_name)) + # Object was modified, as it was created return True + @connected + def delete(self): + ''' + Perform DELETE call to delete ACL table entry. -def _delete_acl_entry(list_name, list_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete an ACL entry - - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ + ''' - acl_entries_dict = get_all_acl_entries(list_name, list_type, **kwargs) + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=ACL.base_uri, + id1=self.name, + separator=self.session.api_version.compound_index_separator, + id2=self.list_type + ) - ace_key = "{},{}".format(list_name, list_type) - ace_value = "/rest/v10.04/system/acls/%s/cfg_aces/%d" % (ace_key, sequence_num) + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) - if ace_value in acl_entries_dict.values(): - target_url = kwargs["url"] + "system/acls/%s,%s/cfg_aces/%d" % (list_name, list_type, sequence_num) + except Exception as e: + raise ResponseError('DELETE', e) - response = kwargs["s"].delete(target_url, verify=False) + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in %s ACL '%s' failed with status code %d: %s" - % (sequence_num, list_type, list_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting entry %d in %s ACL '%s' succeeded" - % (sequence_num, list_type, list_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in %s ACL '%s' since it doesn't exist" - % (sequence_num, list_type, list_name)) - return True - - -def update_port_acl_in(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an interface. This function specifically applies an ACL - to Ingress traffic of the interface - - :param interface_name: Alphanumeric String that is the name of the interface on which the ACL - is applied to - :param acl_name: Alphanumeric String that is the name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _update_port_acl_in_v1(interface_name, acl_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _update_port_acl_in(interface_name, acl_name, list_type, **kwargs) - - -def _update_port_acl_in_v1(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an interface. This function specifically applies an ACL - to Ingress traffic of the interface - - :param interface_name: Alphanumeric String that is the name of the interface on which the ACL - is applied to - :param acl_name: Alphanumeric String that is the name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - acl_url = "/rest/v1/system/acls/%s/%s" % (acl_name, list_type) - - if list_type is "ipv6": - port_data['aclv6_in_cfg'] = acl_url - port_data['aclv6_in_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - elif list_type is "ipv4": - port_data['aclv4_in_cfg'] = acl_url - port_data['aclv4_in_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying ACL '%s' to Ingress of Interface '%s' failed with status code %d: %s" - % (acl_name, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying ACL '%s' to Ingress of Interface '%s' succeeded" - % (acl_name, interface_name)) + logging.info( + "SUCCESS: Delete ACL table entry {} succeeded\ + ".format(self.name)) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, response_data): + ''' + Create a Acl object given a response_data + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/ + /acls/{name},{list_type}" + } + or a + string: "/rest/v10.04/system/acls/{name},{list_type}" + :return: Acl object + ''' + acl_arr = session.api_version.get_keys( + response_data, ACL.resource_uri_name) + list_type = acl_arr[1] + name = acl_arr[0] + + return ACL( + session, name, list_type) + + @classmethod + def from_uri(cls, session, uri): + ''' + Create a Acl object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return indices, acl: tuple containing both the indices and + Acl object + ''' + # Obtain ID from URI + index_pattern = \ + re.compile( + r'(.*)acls/(?P.+)[,./-](?P.+)') + name = index_pattern.match(uri).group('index1') + list_type = index_pattern.match(uri).group('index2') + + # Create Acl object + acl = ACL(session, name, list_type) + indices = "{},{}".format(name, list_type) + + return indices, acl + + def __str__(self): + return "ACL name:{}, list_type:{}".format(self.name, self.list_type) + + def get_uri(self): + ''' + Method used to obtain the specific ACL URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = \ + '{resource_prefix}{class_uri}/{id1}{separator}{id2}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=ACL.base_uri, + id1=self.name, + separator=self.session.api_version.compound_index_separator, + id2=self.list_type + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def add_acl_entry(self, sequence_num, action, count=None, + protocol=None, src_ip=None, dst_ip=None, + dst_l4_port_min=None, dst_l4_port_max=None, + src_mac=None, dst_mac=None, ethertype=None): + """ + Create an AclEntry object, ACL Entry already exists, value passed + won't update the entry + + :param sequence_num: Integer number of the sequence + :param action: Action should be either "permit" or "deny" + :param count: Optional boolean flag that when true, will make entry + increment hit count for matched packets + :param protocol: Optional integer IP protocol number + :param src_ip: Optional source IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_ip: Optional destination IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_l4_port_min: Optional minimum L4 port number in range; used + in conjunction with dst_l4_port_max. + :param dst_l4_port_max: Optional maximum L4 port number in range; used + in conjunction with dst_l4_port_min. + :param src_mac: Optional source MAC address + Example: + '01:02:03:04:05:06' + :param dst_mac: Optional destination MAC address + Example: + '01:02:03:04:05:06' + :param ethertype: Optional integer EtherType number + + :return acl_entry: A AclEntry object + + """ + + # Create ACL Entry + acl_entry_obj = self.session.api_version.get_module( + self.session, 'AclEntry', sequence_num, parent_acl=self, + action=action, count=count, protocol=protocol, src_ip=src_ip, + dst_ip=dst_ip, dst_l4_port_min=dst_l4_port_min, + dst_l4_port_max=dst_l4_port_max, src_mac=src_mac, + dst_mac=dst_mac, ethertype=ethertype) + + # Try to obtain data; if not, create + try: + acl_entry_obj.get() + except GenericOperationError: + # Create object inside switch + acl_entry_obj.apply() + + return acl_entry_obj + + def modify_acl_entry(self, sequence_num, action, count=None, + src_ip=None, dst_ip=None, + dst_l4_port_min=None, dst_l4_port_max=None, + src_mac=None, dst_mac=None, ethertype=None): + """ + Modify an existing ACL Entry + + :param sequence_num: Integer number of the sequence + :param action: Action should be either "permit" or "deny" + :param count: Optional boolean flag that when true, will make entry + increment hit count for matched packets + :param src_ip: Optional source IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_ip: Optional destination IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_l4_port_min: Optional minimum L4 port number in range; used + in conjunction with dst_l4_port_max. + :param dst_l4_port_max: Optional maximum L4 port number in range; used + in conjunction with dst_l4_port_min. + :param src_mac: Optional source MAC address + Example: + '01:02:03:04:05:06' + :param dst_mac: Optional destination MAC address + Example: + '01:02:03:04:05:06' + :param ethertype: Optional integer EtherType number + + :return acl_entry: A AclEntry object + + """ + + # Create ACL Entry + acl_entry_obj = self.session.api_version.get_module( + self.session, 'AclEntry', sequence_num, parent_acl=self) + # Get AclEntry object data + acl_entry_obj.get() + + # Modify data + acl_entry_obj.modify( + action, count, + src_ip, dst_ip, + dst_l4_port_min, dst_l4_port_max, + src_mac, dst_mac, ethertype) + + return acl_entry_obj + + def delete_all_acl_entries(self): + """ + Delete all ACL Entries within an ACL + :return: True if object was changed + """ + # Verify ACL has the latest data + self.get() + + # Delete all entries + self.cfg_aces = [] + + # ACL Entries deleted + # Object modified return True - - -def _update_port_acl_in(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an interface. This function specifically applies an ACL - to Ingress traffic of the interface. This function's minimum supported version is v10.04 and later - - :param interface_name: Alphanumeric name of the interface on which the ACL is applied to - :param acl_name: Alphanumeric name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(interface_name) - int_data = interface.get_interface(int_name_percents, depth=1, selector="writable", **kwargs) - - acl_key = "{},{}".format(acl_name, list_type) - acl_value = "/rest/v10.04/system/acls/" + acl_key - - if interface_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - if list_type is "ipv6": - int_data['aclv6_in_cfg'] = acl_value - int_data['aclv6_in_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - elif list_type is "ipv4": - int_data['aclv4_in_cfg'] = acl_value - int_data['aclv4_in_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating ACL %s on Ingress for Port '%s' failed with status code %d: %s" - % (acl_name, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating ACL %s on Ingress for Port '%s' succeeded" - % (acl_name, interface_name)) - return True - - -def update_port_acl_out(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an L3 interface. This function specifically applies an ACL - to Egress traffic of the interface, which must be a routing interface - - :param interface_name: Alphanumeric String that is the name of the interface on which the ACL - is applied to - :param acl_name: Alphanumeric String that is the name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _update_port_acl_out_v1(interface_name, acl_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _update_port_acl_out(interface_name, acl_name, list_type, **kwargs) - - -def _update_port_acl_out_v1(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an L3 interface. This function specifically applies an ACL - to Egress traffic of the interface, which must be a routing interface. This function will set the interface - to enable routing. - - :param interface_name: Alphanumeric String that is the name of the interface on which the ACL - is applied to - :param acl_name: Alphanumeric String that is the name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - acl_url = "/rest/v1/system/acls/%s/%s" % (acl_name, list_type) - - if list_type is "ipv6": - port_data['aclv6_out_cfg'] = acl_url - port_data['aclv6_out_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - elif list_type is "ipv4": - port_data['aclv4_out_cfg'] = acl_url - port_data['aclv4_out_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - - port_data['routing'] = True - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying ACL '%s' to Egress on Interface '%s' failed with status code %d: %s" - % (acl_name, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying ACL '%s' to Egress on Interface '%s' succeeded" - % (acl_name, interface_name)) - return True - - -def _update_port_acl_out(interface_name, acl_name, list_type, **kwargs): - """ - Perform GET and PUT calls to apply ACL on an interface. This function specifically applies an ACL - to Egress traffic of the interface, which must be a routing interface. This function will set the interface - to enable routing. - - :param interface_name: Alphanumeric name of the interface on which the ACL is applied to - :param acl_name: Alphanumeric name of the ACL - :param list_type: Alphanumeric String of ipv4 or ipv6 to specify the type of ACL - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - int_data = interface.get_interface(port_name_percents, depth=1, selector="writable", **kwargs) - - acl_key = "{},{}".format(acl_name, list_type) - acl_value = "/rest/v10.04/system/acls/" + acl_key - - if interface_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - if list_type is "ipv6": - int_data['aclv6_out_cfg'] = acl_value - int_data['aclv6_out_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - elif list_type is "ipv4": - int_data['aclv4_out_cfg'] = acl_value - int_data['aclv4_out_cfg_version'] = random.randint(-9007199254740991, 9007199254740991) - - int_data['routing'] = True - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying ACL '%s' to Egress on Interface '%s' failed with status code %d: %s" - % (acl_name, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying ACL '%s' to Egress on Interface '%s' succeeded" - % (acl_name, interface_name)) - return True - - -def clear_interface_acl(interface_name, acl_type, **kwargs): - """ - Perform GET and PUT calls to clear an interface's ACL - - :param interface_name: Alphanumeric name of the interface - :param acl_type: Type of ACL: options are 'aclv4_out', 'aclv4_in', 'aclv6_in', or 'aclv6_out' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._clear_port_acl(interface_name, acl_type, **kwargs) - else: # Updated else for when version is v10.04 - return interface._clear_interface_acl(interface_name, acl_type, **kwargs) diff --git a/pyaoscx/acl_entry.py b/pyaoscx/acl_entry.py new file mode 100644 index 0000000..b3183fd --- /dev/null +++ b/pyaoscx/acl_entry.py @@ -0,0 +1,515 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class AclEntry(PyaoscxModule): + ''' + Provide configuration management for ACL Entry on AOS-CX devices. + ''' + + indices = ['sequence_number'] + resource_uri_name = 'cfg_aces' + + protocol_dict = { + "ah": 51, + "esp": 50, + "gre": 47, + "icmp": 1, + "icmpv6": 58, + "igmp": 2, + "ospf": 89, + "pim": 103, + "sctp": 132, + "tcp": 6, + "udp": 17 + } + + def __init__(self, session, sequence_number, parent_acl, + uri=None, **kwargs): + + self.session = session + # Assign ID + self.sequence_number = sequence_number + # Assign parent Acl object + self.__set_acl(parent_acl) + self._uri = uri + # List used to determine attributes related to the acl_entry + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_acl(self, parent_acl): + ''' + Set parent Acl object as an attribute for the AclEntry object + :param parent_acl: a Acl object + ''' + + # Set parent acl + self.__parent_acl = parent_acl + + # Set URI + self.base_uri = '{base_acl_uri}/{id1}{separator}{id2}/cfg_aces'.format( + base_acl_uri=self.__parent_acl.base_uri, + id1=self.__parent_acl.name, + separator=self.session.api_version.compound_index_separator, + id2=self.__parent_acl.list_type) + + # Verify acl_entry doesn't exist already inside acl + for acl_entry in self.__parent_acl.cfg_aces: + if acl_entry.sequence_number == self.sequence_number: + # Make list element point to current object + acl_entry = self + else: + # Add self to cfg_aces list in parent acl + self.__parent_acl.cfg_aces.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for an ACL Entry table entry and + fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch ACL Entries") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{sequence_number}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + sequence_number=self.sequence_number + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the ACL Entry is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['sequence_number']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'sequence_number' in self.__original_attributes: + self.__original_attributes.pop('sequence_number') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_acl): + ''' + Perform a GET call to retrieve all system ACL Entries inside an ACL, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_acl: parent Acl object where ACL Entry is stored + :return acl_entry_dict: Dictionary containing ACL Entry IDs as keys and an ACL Entry + objects as values + ''' + + logging.info("Retrieving all ACL entries within switch for ACL") + # Set URI + base_uri = '{base_acl_uri}/{id1}{separator}{id2}/cfg_aces'.format( + base_acl_uri=parent_acl.base_uri, + id1=parent_acl.name, + separator=session.api_version.compound_index_separator, + id2=parent_acl.list_type) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + acl_entry_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a AclEntry object and adds it to parent acl list + sequence_number, acl_entry = AclEntry.from_uri( + session, parent_acl, uri) + # Load all acl_entry data from within the Switch + acl_entry.get() + acl_entry_dict[sequence_number] = acl_entry + + return acl_entry_dict + + @connected + def apply(self): + ''' + Main method used to either create a new ACL Entry or update an existing + AclEntry. + Checks whether the ACL Entry exists in the switch + Calls self.update() if ACL Entry being updated + Calls self.create() if a new ACL Entry is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + if not self.__parent_acl.materialized: + self.__parent_acl.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing ACL Entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + acl_entry_data = {} + acl_entry_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{sequence_number}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + sequence_number=self.sequence_number + ) + + # Compare dictionaries + if acl_entry_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(acl_entry_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update ACL Entry table entry {} succeeded\ + ".format( + self.sequence_number)) + # Set new original attributes + self.__original_attributes = acl_entry_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new ACL Entry. + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + + ''' + + acl_entry_data = {} + + acl_entry_data = utils.get_attrs(self, self.config_attrs) + acl_entry_data['sequence_number'] = self.sequence_number + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + + # Try to get protocol number + try: + if isinstance(self.protocol, str): + if self.protocol == 'any' or self.protocol == '': + acl_entry_data.pop('protocol') + else: + protocol_num = self.protocol_dict[self.protocol] + acl_entry_data['protocol'] = protocol_num + elif isinstance(self.protocol, int): + acl_entry_data['protocol'] = self.protocol + except Exception: + pass + post_data = json.dumps(acl_entry_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Adding ACL Entry table entry {} succeeded\ + ".format(self.sequence_number)) + + # Get all object's data + self.get() + + # Object was created, means modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete ACL Entry from parent ACL on the switch. + + ''' + + uri = "{base_url}{class_uri}/{sequence_number}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + sequence_number=self.sequence_number + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete ACL Entry table entry {} succeeded".format( + self.sequence_number)) + + # Delete back reference from ACL + for acl_entry in self.__parent_acl.cfg_aces: + if acl_entry.sequence_number == self.sequence_number: + self.__parent_acl.cfg_aces.remove(acl_entry) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_acl, response_data): + ''' + Create a AclEntry object given a response_data related to the ACL Entry + sequence_number object + :param cls: Class calling the method + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_acl: parent Acl object where ACL Entry is stored + :param response_data: The response can be either a + dictionary: { + sequence_number: "/rest/v10.04/system/acls/cfg_aces/ + sequence_number" + } + or a + string: "/rest/v10.04/system/acls/cfg_aces/sequence_number" + :return: AclEntry object + ''' + acl_entry_arr = session.api_version.get_keys( + response_data, AclEntry.resource_uri_name) + sequence_number = acl_entry_arr[0] + return AclEntry(session, sequence_number, parent_acl) + + @classmethod + def from_uri(cls, session, parent_acl, uri): + ''' + Create a AclEntry object given a URI + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_acl: parent Acl object where ACL Entry is stored + :param uri: a String with a URI + + :return index, acl_entry_obj: tuple containing both the AclEntry object and + the acl_entry's sequence_number + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)cfg_aces/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create AclEntry object + acl_entry_obj = AclEntry(session, index, parent_acl, uri=uri) + + return index, acl_entry_obj + + def __str__(self): + return "ACL Entry ID {}".format(self.sequence_number) + + def get_uri(self): + ''' + Method used to obtain the specific ACL Entry URI + return: AclEntry object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{sequence_number}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + sequence_number=self.sequence_number) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: AclEntry object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def modify(self, action=None, count=None, + src_ip=None, dst_ip=None, + dst_l4_port_min=None, dst_l4_port_max=None, + src_mac=None, dst_mac=None, ethertype=None): + """ + Create an AclEntry object, ACL Entry already exists, value passed + won't update the entry + + :param action: Action should be either "permit" or "deny" + :param count: Optional boolean flag that when true, will make entry + increment hit count for matched packets + :param src_ip: Optional source IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_ip: Optional destination IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_l4_port_min: Optional minimum L4 port number in range; used + in conjunction with dst_l4_port_max. + :param dst_l4_port_max: Optional maximum L4 port number in range; used + in conjunction with dst_l4_port_min. + :param src_mac: Optional source MAC address + Example: + '01:02:03:04:05:06' + :param dst_mac: Optional destination MAC address + Example: + '01:02:03:04:05:06' + :param ethertype: Optional integer EtherType number + :return: True if object was changed + + """ + if action is not None: + self.action = action + + if count is not None: + self.count = count + + if src_ip is not None: + self.src_ip = src_ip + + if dst_ip is not None: + self.dst_ip = dst_ip + + if dst_l4_port_min is not None: + self.dst_l4_port_min = dst_l4_port_min + + if dst_l4_port_max is not None: + self.dst_l4_port_max = dst_l4_port_max + + if src_mac is not None: + self.src_mac = src_mac + + if dst_mac is not None: + self.dst_mac = dst_mac + + if ethertype is not None: + self.ethertype = ethertype + + # Apply changes + return self.apply() diff --git a/pyaoscx/aggregate_address.py b/pyaoscx/aggregate_address.py new file mode 100644 index 0000000..b910ef7 --- /dev/null +++ b/pyaoscx/aggregate_address.py @@ -0,0 +1,453 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.utils.connection import connected +from pyaoscx.pyaoscx_module import PyaoscxModule + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class AggregateAddress(PyaoscxModule): + ''' + Provide configuration management for Aggregate Address on AOS-CX devices. + ''' + + indices = ['address-family', 'ip_prefix'] + resource_uri_name = 'aggregate_addresses' + + def __init__(self, session, address_family, ip_prefix, parent_bgp_router, + uri=None, **kwargs): + self.session = session + # Assign ID + self.address_family = address_family + self.__set_name(ip_prefix) + # Assign parent BGP Router + self.__set_bgp_router(parent_bgp_router) + self._uri = uri + # List used to determine attributes related to the Aggregate Address + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_name(self, ip_prefix): + ''' + Set name attribute in the proper form for references + :param ip_prefix: Object's IP + ''' + + # Add attributes to class + self._is_lag = False + self.ip_prefix = None + self.reference_ip_prefix = None + if r'%2F' in ip_prefix: + self.ip_prefix = utils._replace_percents_ip(ip_prefix) + self.reference_ip_prefix = ip_prefix + else: + self.ip_prefix = ip_prefix + self.reference_ip_prefix = utils._replace_special_characters_ip( + self.ip_prefix) + + def __set_bgp_router(self, parent_bgp_router): + ''' + Set parent BgpRouter object as an attribute for the Aggregate Address class + :param parent_bgp_router a BgpRouter object + ''' + + # Set parent BGP Router + self.__parent_bgp_router = parent_bgp_router + + # Set URI + self.base_uri = \ + '{base_bgp_router_uri}/{bgp_router_apn}/aggregate_addresses'.format( + base_bgp_router_uri=self.__parent_bgp_router.base_uri, + bgp_router_apn=self.__parent_bgp_router.asn) + + for address in self.__parent_bgp_router.aggregate_addresses: + if address.address_family == self.address_family and\ + address.ip_prefix == self.ip_prefix: + # Make list element point to current object + address = self + else: + # Add self to Aggregate Addresses list in parent BGP Router + self.__parent_bgp_router.aggregate_addresses.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a Aggregate Address table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch Aggregate Addresses") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id1=self.address_family, + separator=self.session.api_version.compound_index_separator, + id2=self.reference_ip_prefix + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the Aggregate Address is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['address-family']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'address-family' in self.__original_attributes: + self.__original_attributes.pop('address-family') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_bgp_router): + ''' + Perform a GET call to retrieve all system Aggregate Addresses inside a + BGP Router, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent bgp_router object where Aggregate Address is stored + :return: Dictionary containing Aggregate Addresses IDs as keys and a + AggregateAddress objects as values + ''' + + logging.info("Retrieving the switch Aggregate Address") + + base_uri =\ + '{base_bgp_router_uri}/{bgp_router_apn}/aggregate_addresses'.format( + base_bgp_router_uri=parent_bgp_router.base_uri, + bgp_router_apn=parent_bgp_router.asn) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + agg_address_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a AggregateAddress object + indices, aggregate_address = AggregateAddress.from_uri( + session, parent_bgp_router, uri) + agg_address_dict[indices] = aggregate_address + + return agg_address_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + Aggregate Address table entry. + Checks whether the Aggregate Addresses exists in the switch + Calls self.update() if Aggregate Address is being updated + Calls self.create() if a new Aggregate Address is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + if not self.__parent_bgp_router.materialized: + self.__parent_bgp_router.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing Aggregate Address table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + agg_address_data = {} + + agg_address_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id1=self.address_family, + separator=self.session.api_version.compound_index_separator, + id2=self.reference_ip_prefix + ) + # Compare dictionaries + if agg_address_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(agg_address_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update Aggregate Address table entry {} succeeded\ + ".format( + self.address_family)) + # Set new original attributes + self.__original_attributes = agg_address_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new Aggregate Address table entry + Only returns if an exception is not raise + + :return modified: True if entry was created. + + ''' + + ag_address_data = {} + + ag_address_data = utils.get_attrs(self, self.config_attrs) + ag_address_data['address-family'] = self.address_family + ag_address_data['ip_prefix'] = self.ip_prefix + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(ag_address_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding Aggregate Address table entry {} succeeded\ + ".format(self.address_family)) + + # Get all object's data + self.get() + # Object was modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete Aggregate Address. + + ''' + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id1=self.address_family, + separator=self.session.api_version.compound_index_separator, + id2=self.reference_ip_prefix + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete Aggregate Address table entry {} succeeded\ + ".format(self.address_family)) + + # Delete back reference from BGP Router + for address in self.__parent_bgp_router.aggregate_addresses: + if address.address_family == self.address_family and\ + address.ip_prefix == self.ip_prefix: + self.__parent_bgp_router.aggregate_addresses.remove(address) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_bgp_router, response_data): + ''' + Create a AggregateAddress object given a response_data related to the Aggregate Address + ID object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent BGP Router class where Aggregate Address is stored + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/vrfs/bgp_routers/asn + /aggregate_addresses/id" + } + or a + string: "/rest/v10.04/system/vrfs/bgp_routers/asn/aggregate_addresses + /address_family/ip_prefix" + :return: AggregateAddress object + ''' + aggr_address_arr = session.api_version.get_keys( + response_data, cls.resource_uri_name) + ip_prefix = aggr_address_arr[1] + address_family = aggr_address_arr[0] + + return AggregateAddress( + session, address_family, ip_prefix, parent_bgp_router) + + @classmethod + def from_uri(cls, session, parent_bgp_router, uri): + ''' + Create a AggregateAddress object + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent BGP Router class where Aggregate Address is stored + :param uri: a String with a URI + + :return indices, aggr_address: tuple containing both the AggregateAddress object and the Aggregate Address' ID + ''' + # Obtain ID from URI + index_pattern = \ + re.compile( + r'(.*)aggregate_addresses/(?P.+)[,./-](?P.+)') + index1 = index_pattern.match(uri).group('index1') + index2 = index_pattern.match(uri).group('index2') + + # Create Create a AggregateAddress object + aggr_address = AggregateAddress(session, index1, index2, + parent_bgp_router) + indices = "{},{}".format(index1, index2) + + return indices, aggr_address + + def __str__(self): + return "Aggregate Address ID {}".format(self.address_family) + + def get_uri(self): + ''' + Method used to obtain the specific Aggregate Address URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = \ + '{resource_prefix}{class_uri}/{id1}{separator}{id2}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + id1=self.address_family, + separator=self.session.api_version.compound_index_separator, + id2=self.reference_ip_prefix + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/api.py b/pyaoscx/api.py new file mode 100644 index 0000000..4ec02f6 --- /dev/null +++ b/pyaoscx/api.py @@ -0,0 +1,49 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from abc import ABC, abstractclassmethod +from datetime import date + +# List of the versions supported by the SDK +supported_versions = ['1', '10.04'] + + +class API(ABC): + ''' + Factory class that creates the version's specific API + class. + ''' + latest_version = '10.04' + version = latest_version + license = "Apache-2.0" + + def __str__(self): + return self.version + + @classmethod + def create(cls, target_version): + ''' + Translate the version string name to a valid python symbol + :param cls: API Class object + :param target_version: String with the API Version + :return api: API object + ''' + + target_version = 'v' + target_version.replace('.', '_') + + if target_version == 'v1': + from pyaoscx.rest.v1.api import v1 + elif target_version == 'v10_04': + from pyaoscx.rest.v10_04.api import v10_04 + else: + raise Exception("Invalid version name") + + return locals()[target_version]() + + @abstractclassmethod + def __init__(self): + """ + This method must be overwritten in the derived classes + to set up the internal attributes, like version as minimum. + """ + pass diff --git a/pyaoscx/arp.py b/pyaoscx/arp.py deleted file mode 100644 index c9bcb39..0000000 --- a/pyaoscx/arp.py +++ /dev/null @@ -1,49 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import logging - - -def get_arp_entries(vrf_name, **kwargs): - """ - Perform a GET call on Neighbors table to get ARP entries - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of dictionaries each containing one ARP entry - """ - - queries = {"depth": 1} - - target_url = kwargs["url"] + "system/vrfs/%s/neighbors" % vrf_name - response = kwargs["s"].get(target_url, verify=False, params=queries, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting Neighbors table entries failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting Neighbors table entries succeeded") - - neighbor_info_list = response.json() - - arp_entries_list = [] - - for neighbor_info in neighbor_info_list: - arp_entry = { - "IPv4 Address": neighbor_info['ip_address'], - "MAC Address": neighbor_info['mac'], - # For port and physical port: split string by '/', take last block, and replace any '%' characters - "Port": common_ops._replace_percents((neighbor_info['port'].split('/'))[-1]), - "State": neighbor_info['state'] - } - - if 'phy_port' in neighbor_info: - arp_entry['Physical Port'] = common_ops._replace_percents((neighbor_info['phy_port'].split('/'))[-1]) - - arp_entries_list.append(arp_entry) - - return arp_entries_list diff --git a/pyaoscx/bgp.py b/pyaoscx/bgp.py deleted file mode 100644 index 0a9f039..0000000 --- a/pyaoscx/bgp.py +++ /dev/null @@ -1,294 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops, vrf - -import json -import logging - - -def get_bgp_routers(vrf_name, **kwargs): - """ - Perform a GET call to get a list of all BGP Router Autonomous System Number references - - :param vrf_name: Alphanumeric name of the VRF that we are retrieving all BGP ASNs from - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all BGP Router ASNs in the table - """ - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers" % vrf_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all BGP Router ASNs failed with status code %d: %s" - % (response.status_code, response.text)) - bgp_list = {} - else: - logging.info("SUCCESS: Getting list of all BGP Router ASNs succeeded") - bgp_list = response.json() - - return bgp_list - - -def create_bgp_asn(vrf_name, asn, router_id=None, **kwargs): - """ - Perform a POST call to create a BGP Router Autonomous System Number - - :param vrf_name: Alphanumeric name of the VRF the BGP ASN belongs to - :param asn: Integer that represents the Autonomous System Number - :param router_id: Optional IPv4 address that functions as the BGP Router ID - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - bgp_data = { - "asn": asn - } - - if router_id: - bgp_data['router_id'] = router_id - - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers" % vrf_name - - post_data = json.dumps(bgp_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating BGP ASN '%s' on vrf %s failed with status code %d: %s" - % (asn, vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating BGP ASN '%s' succeeded on vrf %s" % (asn, vrf_name)) - return True - - -def delete_bgp_asn(vrf_name, asn, **kwargs): - """ - Perform a DELETE call to remove a BGP Router Autonomous System Number - - :param vrf_name: Alphanumeric name of the VRF the BGP ASN belongs to - :param asn: Integer that represents the Autonomous System Number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - bgp_list = get_bgp_routers(vrf_name, **kwargs) - - if str(asn) in bgp_list: - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers/%d" % (vrf_name, asn) - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting BGP ASN '%s' on vrf %s failed with status code %d: %s" - % (asn, vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting BGP ASN '%s' succeeded on vrf %s" % (asn, vrf_name)) - return True - else: - logging.info("SUCCESS: No need to Delete BGP ASN '%s' as it does not exists!" % asn) - return True - - -def get_bgp_neighbors_list(vrf_name, asn, **kwargs): - """ - Perform a GET call to get a list of all BGP neighbors for the supplied Autonomous System Number - - :param vrf_name: Alphanumeric name of the VRF that we are retrieving all BGP ASNs from - :param asn: Integer that represents the Autonomous System Number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all BGP neighbors in the table for the ASN - """ - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers/%s/bgp_neighbors" % (vrf_name, asn) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all BGP neighbors for ASN '%s' failed with status code %d: %s" - % (asn, response.status_code, response.text)) - neighbor_list = {} - else: - logging.info("SUCCESS: Getting list of all BGP neighbors for ASN '%s' succeeded" % asn) - neighbor_list = response.json() - return neighbor_list - - -def create_bgp_neighbors(vrf_name, asn, group_ip, family_type="l2vpn_evpn", reflector=False, send_community=False, - local_interface="", **kwargs): - """ - Perform a POST call to create BGP neighbors to the associated BGP ASN. With l2vpn_evpn being True, this will - also apply EVPN settings to the BGP neighbor configurations. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of the VRF the BGP ASN belongs to - :param asn: Integer that represents the Autonomous System Number - :param group_ip: IPv4 address or name of group of the neighbors that functions as the BGP Router link - :param family_type: Alphanumeric to specify what type of neighbor settings to configure. The options are 'l2vpn-evpn', - 'ipv4-unicast', or 'ipv6-unicast'. When setting to l2vpn-evpn, the neighbor configurations also will add - route-reflector-client and send-community settings. - :param reflector: Boolean value to determine whether this neighbor has route reflector enabled. Default is False. - :param send_community: Boolean value to determine whether this neighbor has send-community enabled. Default is False. - :param local_interface: Optional alphanumeric to specify which interface the neighbor will apply to. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if family_type not in ['l2vpn-evpn', 'ipv4-unicast', 'ipv6-unicast']: - raise Exception("ERROR: family_type should be 'l2vpn-evpn', 'ipv4-unicast', or 'ipv6-unicast'") - - neighbor_list = get_bgp_neighbors_list(vrf_name, asn, **kwargs) - - if group_ip not in neighbor_list: - bgp_data = { - "ip_or_group_name": group_ip, - "is_peer_group": False, - "remote_as": asn, - "shutdown": False, - "activate": { - "ipv4-unicast": False, - "ipv6-unicast": False, - "l2vpn-evpn": False - }, - "next_hop_unchanged": { - "l2vpn-evpn": False - }, - "route_reflector_client": { - "ipv4-unicast": False, - "ipv6-unicast": False, - "l2vpn-evpn": False - }, - "send_community": { - "ipv4-unicast": "none", - "ipv6-unicast": "none", - "l2vpn-evpn": "none" - } - } - - if local_interface: - int_percents = common_ops._replace_special_characters(local_interface) - if kwargs["url"].endswith("/v1/"): - bgp_data.update({'local_interface': "/rest/v1/system/ports/%s" % int_percents}) - else: - # Else logic designed for v10.04 and later - bgp_data.update({'local_interface': "/rest/v10.04/system/interfaces/%s" % int_percents}) - - bgp_data['activate'][family_type] = True - - if send_community: - bgp_data['send_community'][family_type] = "both" - - if reflector: - bgp_data['route_reflector_client'][family_type] = reflector - - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers/%s/bgp_neighbors" % (vrf_name, asn) - - post_data = json.dumps(bgp_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating BGP Neighbor for ASN '%s' on interface %s failed with status code %d: %s" - % (asn, local_interface, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating BGP Neighbor for ASN '%s' succeeded on interface %s" % (asn, local_interface)) - return True - else: - logging.info("SUCCESS: BGP Neighbor already exists for ASN '%s' on interface '%s'." % (asn, local_interface)) - return True - - -def create_bgp_vrf(vrf_name, asn, redistribute, **kwargs): - """ - Perform a POST call to create BGP VRF settings for the associated BGP ASN. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of the VRF the BGP ASN belongs to - :param asn: Integer that represents the Autonomous System Number - :param redistribute: Optional alphanumeric to specify which types of routes that should be redistributed by BGP. The - options are "ipv4-unicast" or "ipv6-unicast". - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if redistribute not in ['ipv4-unicast', 'ipv6-unicast']: - raise Exception("ERROR: redistribute should be 'ipv4-unicast' or 'ipv6-unicast'") - - vrf_list = vrf.get_all_vrfs(**kwargs) - - if kwargs["url"].endswith("/v1/"): - vrf_check = "/rest/v1/system/vrfs/%s" % vrf_name - else: # Updated else for when version is v10.04 - vrf_check = vrf_name - - if vrf_check in vrf_list: - bgp_vrf_data = { - "asn": asn - } - if redistribute == 'ipv4-unicast': - bgp_vrf_data['redistribute'] = { - "ipv4-unicast": ["connected"] - } - elif redistribute == 'ipv6-unicast': - bgp_vrf_data['redistribute'] = { - "ipv6-unicast": ["connected"] - } - - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers" % vrf_name - post_data = json.dumps(bgp_vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating BGP VRF '%s' for ASN '%s' failed with status code %d: %s" - % (vrf_name, asn, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating BGP VRF '%s' for ASN '%s' succeeded" % (vrf_name, asn)) - return True - else: - logging.warning("FAIL: Cannot create BGP VRF '%s' as VRF does not exist!" % vrf_name) - return False - - -def delete_bgp_vrf(vrf_name, asn, **kwargs): - """ - Perform a DELETE call to remove BGP VRF settings for the associated BGP ASN. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of the VRF the BGP ASN belongs to - :param asn: Integer that represents the Autonomous System Number - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrf_list = vrf.get_all_vrfs(**kwargs) - - if kwargs["url"].endswith("/v1/"): - vrf_check = "/rest/v1/system/vrfs/%s" % vrf_name - else: # Updated else for when version is v10.04 - vrf_check = vrf_name - - if vrf_check in vrf_list: - target_url = kwargs["url"] + "system/vrfs/%s/bgp_routers" % vrf_name - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting BGP VRF '%s' for ASN '%s' failed with status code %d: %s" - % (vrf_name, asn, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting BGP VRF '%s' for ASN '%s' succeeded" % (vrf_name, asn)) - return True - else: - logging.info("SUCCESS: No need to Delete BGP VRF '%s' as VRF does not exists." % vrf_name) - return True diff --git a/pyaoscx/bgp_neighbor.py b/pyaoscx/bgp_neighbor.py new file mode 100644 index 0000000..89eb701 --- /dev/null +++ b/pyaoscx/bgp_neighbor.py @@ -0,0 +1,450 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class BgpNeighbor(PyaoscxModule): + ''' + Provide configuration management for BGP Neighbor on AOS-CX devices. + ''' + + indices = ['ip_or_ifname_or_group_name'] + resource_uri_name = 'bgp_neighbors' + + def __init__(self, session, ip_or_ifname_or_group_name, parent_bgp_router, + uri=None, **kwargs): + + self.session = session + # Assign ID + self.ip_or_ifname_or_group_name = ip_or_ifname_or_group_name + # Assign parent BGP Router + self.__set_bgp_router(parent_bgp_router) + self._uri = uri + # List used to determine attributes related to the BGP configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_bgp_router(self, parent_bgp_router): + ''' + Set parent BgpRouter object as an attribute for the BGP class + :param parent_bgp_router a BgpRouter object + ''' + + # Set parent BGP Router + self.__parent_bgp_router = parent_bgp_router + + # Set URI + self.base_uri = \ + '{base_bgp_router_uri}/{bgp_router_asn}/bgp_neighbors'.format( + base_bgp_router_uri=self.__parent_bgp_router.base_uri, + bgp_router_asn=self.__parent_bgp_router.asn) + + for bgp_ngh in self.__parent_bgp_router.bgp_neighbors: + if bgp_ngh.ip_or_ifname_or_group_name \ + == self.ip_or_ifname_or_group_name: + # Make list element point to current object + bgp_ngh = self + else: + # Add self to BGP Neighbors list in parent BGP Router + self.__parent_bgp_router.bgp_neighbors.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a BGP Neighbor table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch BGP Neighbors") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.ip_or_ifname_or_group_name + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the BGP Neighbor is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['ip_or_ifname_or_group_name']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'ip_or_ifname_or_group_name' in self.__original_attributes: + self.__original_attributes.pop('ip_or_ifname_or_group_name') + + # If the BGP Neighbor has a local_interface inside the switch + if hasattr(self, 'local_interface') and \ + self.local_interface is not None: + local_interface_response = self.local_interface + interface_cls = self.session.api_version.get_module( + self.session, 'Interface', '') + # Set port as a Interface Object + self.local_interface = interface_cls.from_response( + self.session, local_interface_response) + self.local_interface.get() + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_bgp_router): + ''' + Perform a GET call to retrieve all system BGP Neighbors inside a BGP + Router, and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent BgpRouter object where BGP Neighbor is stored + :return: Dictionary containing BGP Neighbors IDs as keys and a BGP + Neighbors objects as values + ''' + + logging.info("Retrieving the switch BGP Neighbors") + + base_uri = \ + '{base_bgp_router_uri}/{bgp_router_asn}/bgp_neighbors'.format( + base_bgp_router_uri=parent_bgp_router.base_uri, + bgp_router_asn=parent_bgp_router.asn) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + bgp_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a BgpNeighbor object + ip_or_ifname_or_group_name, bgp_neighbor = BgpNeighbor.from_uri( + session, parent_bgp_router, uri) + # Load all BGP Neighbor data from within the Switch + bgp_neighbor.get() + bgp_dict[ip_or_ifname_or_group_name] = bgp_neighbor + + return bgp_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + BGP Neighbor table entry. + Checks whether the BGP Neighbor exists in the switch + Calls self.update() if BGP Neighbor is being updated + Calls self.create() if a new BGP Neighbor is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + if not self.__parent_bgp_router.materialized: + self.__parent_bgp_router.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing BGP Neighbor table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + bgp_neighbor_data = {} + + bgp_neighbor_data = utils.get_attrs(self, self.config_attrs) + + # Get ISL port uri + if self.local_interface is not None: + bgp_neighbor_data["local_interface"] = \ + self.local_interface.get_info_format() + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.ip_or_ifname_or_group_name + ) + + # Compare dictionaries + if bgp_neighbor_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + put_data = json.dumps(bgp_neighbor_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=put_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update BGP table entry {} succeeded".format( + self.ip_or_ifname_or_group_name)) + # Set new original attributes + self.__original_attributes = bgp_neighbor_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new BGP Neighbor table entry + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + + ''' + + bgp_neighbor_data = {} + + bgp_neighbor_data = utils.get_attrs(self, self.config_attrs) + bgp_neighbor_data['ip_or_ifname_or_group_name'] = \ + self.ip_or_ifname_or_group_name + + if hasattr(self, 'local_interface'): + + # If local interface is NOT a string + if not isinstance(self.local_interface, str): + if not self.local_interface.materialized: + raise VerificationError( + 'Local Interface', + 'Object not materialized') + + # Get ISL port uri + bgp_neighbor_data["local_interface"] = \ + self.local_interface.get_info_format() + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(bgp_neighbor_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Adding BGP table entry {} succeeded".format( + self.ip_or_ifname_or_group_name)) + + # Get all object's data + self.get() + + # Object was modified, as it was created inside Device + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete BGP Neighbor table entry. + + ''' + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.ip_or_ifname_or_group_name + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Delete BGP table entry {} succeeded".format( + self.ip_or_ifname_or_group_name)) + + # Delete back reference from BGP_Routers + for neighbor in self.__parent_bgp_router.bgp_neighbors: + if neighbor.ip_or_ifname_or_group_name == \ + self.ip_or_ifname_or_group_name: + self.__parent_bgp_router.bgp_neighbors.remove(neighbor) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_bgp_router, response_data): + ''' + Create a BgpNeighbor object given a response_data related to the BGP Router + ID object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent BgpRouter object where BGP Neighbor is stored + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/vrfs//bgp_routers/asn + /bgp_neighbors/id" + } + or a + string: "/rest/v10.04/system/vrfs//bgp_routers/asn/ + bgp_neighbors/id" + :return: BgpNeighbor object + ''' + bgp_arr = session.api_version.get_keys( + response_data, BgpNeighbor.resource_uri_name) + bgp_neighbor_id = bgp_arr[0] + return BgpNeighbor(session, bgp_neighbor_id, parent_bgp_router) + + @classmethod + def from_uri(cls, session, parent_bgp_router, uri): + ''' + Create a BgpNeighbor object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_bgp_router: parent BgpRouter object where BGP Neighbor is stored + :param uri: a String with a URI + + :return index, bgp_obj: tuple containing both the BGP object and the + BGP's ID + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)bgp_neighbors/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create BGP object + bgp_obj = BgpNeighbor( + session, index, parent_bgp_router, uri=uri) + + return index, bgp_obj + + def __str__(self): + return "Bgp Neighbor ID {}".format( + self.ip_or_ifname_or_group_name) + + def get_uri(self): + ''' + Method used to obtain the specific BGP Neighbor URI + return: Object's URI + ''' + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{id}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + id=self.ip_or_ifname_or_group_name + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/bgp_router.py b/pyaoscx/bgp_router.py new file mode 100644 index 0000000..b4f4a15 --- /dev/null +++ b/pyaoscx/bgp_router.py @@ -0,0 +1,540 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + + +from pyaoscx.bgp_neighbor import BgpNeighbor +from pyaoscx.aggregate_address import AggregateAddress +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class BgpRouter(PyaoscxModule): + ''' + Provide configuration management for BGP on AOS-CX devices. + ''' + + indices = ['asn'] + resource_uri_name = 'bgp_routers' + + # Use to manage BGP Neighbors + bgp_neighbors = ListDescriptor('bgp_neighbors') + aggregate_addresses = ListDescriptor('aggregate_addresses') + + def __init__(self, session, asn: int, parent_vrf, uri=None, **kwargs): + + self.session = session + # Assign id + self.asn = asn + # Assign parent Vrf object + self.__set_vrf(parent_vrf) + self._uri = uri + # List used to determine attributes related to the BGP configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Use to manage BGP Neighbors + self.bgp_neighbors = [] + # Use to manage Aggregate Addresses + self.aggregate_addresses = [] + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_vrf(self, parent_vrf): + ''' + Set parent Vrf object as an attribute for the BGP class + :param parent_vrf: a Vrf object + ''' + + # Set parent Vrf object + self.__parent_vrf = parent_vrf + + # Set URI + self.base_uri = '{base_vrf_uri}/{vrf_name}/bgp_routers'.format( + base_vrf_uri=self.__parent_vrf.base_uri, + vrf_name=self.__parent_vrf.name) + + # Verify BGP Router doesn't exist already inside VRF + for bgp_router in self.__parent_vrf.bgp_routers: + if bgp_router.asn == self.asn: + # Make list element point to current object + bgp_router = self + else: + # Add self to bgp_routers list in parent_vrf + self.__parent_vrf.bgp_routers.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a BGP Router table entry and + fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch BGP Routers") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{asn}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + asn=self.asn + ) + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + # Delete unwanted data + if 'bgp_neighbors' in data: + data.pop('bgp_neighbors') + if 'aggregate_addresses' in data: + data.pop('aggregate_addresses') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the BGP Router is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', + ['asn', 'bgp_neighbors', 'aggregate_addresses']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'asn' in self.__original_attributes: + self.__original_attributes.pop('asn') + # Remove ID + if 'bgp_neighbors' in self.__original_attributes: + self.__original_attributes.pop('bgp_neighbors') + # Remove ID + if 'aggregate_addresses' in self.__original_attributes: + self.__original_attributes.pop('aggregate_addresses') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + # Get BGP Neighbors + if self.bgp_neighbors == []: + # Set BGP Neighbor if any + # Adds BGP Neighbor to parent BGP Router already + BgpNeighbor.get_all(self.session, self) + + if self.aggregate_addresses == []: + # Set Aggregate address if any + # Adds Aggregate Addresses to parent BGP Router already + AggregateAddress.get_all(self.session, self) + return True + + @classmethod + def get_all(cls, session, parent_vrf): + ''' + Perform a GET call to retrieve all system BGP inside a VRF, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where VRF is stored + :return: Dictionary containing BGP Router IDs as keys and a BGP Router + objects as values + ''' + + logging.info("Retrieving the switch BGP Routers") + + base_uri = '{base_vrf_uri}/{vrf_name}/bgp_routers'.format( + base_vrf_uri=parent_vrf.base_uri, + vrf_name=parent_vrf.name) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + bgp_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a BgpRouter object and adds it to parent Vrf object list + asn, bgp = BgpRouter.from_uri(session, parent_vrf, uri) + # Load all BGP Router data from within the Switch + bgp.get() + bgp_dict[asn] = bgp + + return bgp_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + BGP Router table entry. + Checks whether the BGP Router exists in the switch + Calls self.update() if BGP Router is being updated + Calls self.create() if a new BGP Router is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + if not self.__parent_vrf.materialized: + self.__parent_vrf.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing BGP Router table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + bgp_router_data = {} + + bgp_router_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{asn}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + asn=self.asn + ) + + # Compare dictionaries + if bgp_router_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(bgp_router_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update BGP table entry {} succeeded".format( + self.asn)) + + # Set new original attributes + self.__original_attributes = bgp_router_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new BGP Router table entry + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + ''' + bgp_data = {} + + bgp_data = utils.get_attrs(self, self.config_attrs) + bgp_data['asn'] = self.asn + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(bgp_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Adding BGP table entry {} succeeded".format( + self.asn)) + + # Get all object's data + self.get() + # Object was created, thus modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete BGP Router table entry. + + ''' + + uri = "{base_url}{class_uri}/{asn}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + asn=self.asn + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Delete BGP table entry {} succeeded".format( + self.asn)) + + # Delete back reference from VRF + for bgp_router in self.__parent_vrf.bgp_routers: + if bgp_router.asn == self.asn: + self.__parent_vrf.bgp_routers.remove(bgp_router) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_vrf, response_data): + ''' + Create a BgpRouter object given a response_data related to the BGP asn object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where VRF is stored + :param response_data: The response can be either a + dictionary: { + asn: "/rest/v10.04/system/vrfs/bgp_routers/asn" + } + or a + string: "/rest/v10.04/system/vrfs/bgp_routers/asn" + :return: BgpRouter object + ''' + bgp_arr = session.api_version.get_keys( + response_data, BgpRouter.resource_uri_name) + asn = bgp_arr[0] + return BgpRouter(session, asn, parent_vrf) + + @classmethod + def from_uri(cls, session, parent_vrf, uri): + ''' + Create a BgpRouter object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where BGP Router is stored + :param uri: a String with a URI + + :return index, bgp_obj: tuple containing both the BgpRouter object and the BGP's + asn + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)bgp_routers/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create BGP object + bgp_obj = BgpRouter(session, index, parent_vrf, uri=uri) + + return index, bgp_obj + + def __str__(self): + return "BGP Router ID {}".format(self.asn) + + def get_uri(self): + ''' + Method used to obtain the specific BGP Router URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{asn}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + asn=self.asn + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def create_bgp_neighbors(self, group_ip, family_type="l2vpn_evpn", + reflector=False, send_community=False, + local_interface=""): + """ + Perform a POST call to create BGP Neighbors to the associated current + BGP Router - ASN. + With l2vpn_evpn being True, this will also apply EVPN settings to the + BGP neighbor configurations. + + :param group_ip: IPv4 address or name of group of the neighbors that + functions as the BGP Router link. + Example IPv4: + 10.10.12.11/255.255.255.255 + :param family_type: Alphanumeric to specify what type of neighbor + settings to configure. The options are 'l2vpn-evpn', + 'ipv4-unicast', or 'ipv6-unicast'. When setting to l2vpn-evpn, + the neighbor configurations also will add + route-reflector-client and send-community settings. + Defaults to "l2vpn_evpn" + :param reflector: Boolean value to determine whether this neighbor has + route reflector enabled. Default is False. + :param send_community: Boolean value to determine whether this + neighbor has send-community enabled. Default is False. + :param local_interface: Optional alphanumeric to specify which + interface the neighbor will apply to. + Defaults to "" + :return bgp_neighbor_obj: BgpRouter object + """ + + if not self.materialized: + raise VerificationError( + 'VRF {}'.format(self.name), + 'Object not materialized') + + if local_interface != "": + if isinstance(local_interface, str): + local_interface = self.session.api_version.get_module( + self.session, 'Interface', local_interface) + + # Set values needed + activate = { + "ipv4-unicast": False, + "ipv6-unicast": False, + "l2vpn-evpn": False + } + + next_hop_unchanged = { + "l2vpn-evpn": False + } + + route_reflector_client = { + "ipv4-unicast": False, + "ipv6-unicast": False, + "l2vpn-evpn": False + } + + send_community_data = { + "ipv4-unicast": "none", + "ipv6-unicast": "none", + "l2vpn-evpn": "none" + } + + activate[family_type] = True + + if send_community: + send_community_data[family_type] = "both" + + if reflector: + route_reflector_client[family_type] = reflector + + bgp_neighbor_obj = self.session.api_version.get_module( + self.session, 'BgpNeighbor', group_ip, + parent_bgp_router=self, is_peer_group=False, + remote_as=self.asn, shutdown=False, + local_interface=local_interface, + activate=activate, next_hop_unchanged=next_hop_unchanged, + route_reflector_client=route_reflector_client, + send_community=send_community_data + ) + + # Try to obtain data; if not, create + try: + bgp_neighbor_obj.get() + except GenericOperationError: + # Create object inside switch + bgp_neighbor_obj.apply() + + return bgp_neighbor_obj diff --git a/pyaoscx/common_ops.py b/pyaoscx/common_ops.py deleted file mode 100644 index 73939b7..0000000 --- a/pyaoscx/common_ops.py +++ /dev/null @@ -1,123 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from os.path import dirname, abspath, join - -import yaml - - -def _list_remove_duplicates(list_with_dup): - """ - Return a copy of a list without duplicated items. - - :param list_with_dup: Input list that may contain duplicates - :return: List that does not contain duplicates - """ - set_no_dup = set(list_with_dup) - list_no_dup = list(set_no_dup) - return list_no_dup - - -def _replace_percents(str_percents): - """ - Replaces percent-encoded pieces in a string with their special-character counterparts - '%3A' -> ':' - '%2F' -> '/' - '%2C' -> ',' - (e.g. "1/1/9" -> "1%2F1%2F9") - - :param str_percents: string in which to substitute characters - :return: new string with percent phrases replaced by their special-character counterparts - """ - str_special_chars = str_percents.replace("%3A", ":").replace("%2F", "/").replace( - "%2C", ",") - return str_special_chars - - -def _replace_special_characters(str_special_chars): - """ - Replaces special characters in a string with their percent-encoded counterparts - ':' -> '%3A' - '/' -> '%2F' - ',' -> '%2C' - (e.g. "1/1/9" -> "1%2F1%2F9") - - :param str_special_chars: string in which to substitute characters - :return: new string with characters replaced by their percent-encoded counterparts - """ - str_percents = str_special_chars.replace(":", "%3A").replace("/", "%2F").replace( - ",", "%2C") - return str_percents - - -def _response_ok(response, call_type): - """ - Checks whether API HTTP response contains the associated OK code. - - :param response: Response object - :param call_type: String containing the HTTP request type - :return: True if response was OK, False otherwise - """ - ok_codes = { - "GET": [200], - "PUT": [200, 204], - "POST": [201], - "DELETE": [204] - } - - return response.status_code in ok_codes[call_type] - - -def _dictionary_to_list_values(dictionary): - """ - Replaces a dictionary with a list of just the values - Example input: - "interfaces": { - "1/1/21": "/rest/v10.04/system/interfaces/1%2F1%2F21", - "1/1/22": "/rest/v10.04/system/interfaces/1%2F1%2F22" - } - Example output: - "interfaces": [ - "/rest/v10.04/system/interfaces/1%2F1%2F21", - "/rest/v10.04/system/interfaces/1%2F1%2F22" - ] - - :param dictionary: A Non-empty dictionary that will have its values added to a list - :return: A new list with the values from the dictionary - """ - new_list = [] - for x in dictionary: - new_list.append(dictionary[x]) - return new_list - - -def _dictionary_to_string(dictionary): - """ - Replaces a dictionary with a string of the first value. Note this should only be used on a dictionary with - just one value. - Example input: - "vlan_tag": { - "1": "/rest/v10.04/system/vlans/1" - } - Example output: - "vlan_tag": "/rest/v10.04/system/vlans/1" - - :param dictionary: A Non-empty dictionary that will have its values added to a list - :return: A string with just the first value in the input dictionary - """ - return list(dictionary.values())[0] - - -def read_yaml(filename): - """" Reads a YAML file and returns the data in a Python object - - :param filename: Name of YAML file (e.g. 'vlan_data.yml') - :return: Python object - """ - - parentdirpath = dirname(dirname(abspath(__file__))) - sampledatadir = join(parentdirpath, "sampledata") - - with open(abspath(join(sampledatadir, filename)), 'r') as yml_file: - data = yaml.safe_load(yml_file) - return data \ No newline at end of file diff --git a/pyaoscx/config.py b/pyaoscx/config.py deleted file mode 100644 index 94ea20c..0000000 --- a/pyaoscx/config.py +++ /dev/null @@ -1,103 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import json -import logging - - -def get_all_configs(**kwargs): - """ - Perform a GET call to get all configs - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all config URIs - """ - - response = kwargs["s"].get(kwargs["url"] + "fullconfigs", verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting all configs failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting all configs succeeded") - - return response.json() - - -def get_config(config_name, **kwargs): - """ - Perform a GET call to get contents of a config. - - :param config_name: name of config (e.g. running-config) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing config contents - """ - - response = kwargs["s"].get(kwargs["url"] + "fullconfigs/%s" % config_name, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting config '%s' failed with status code %d: %s" - % (config_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting config '%s' succeeded" % config_name) - - return response.json() - - -def upload_running_config(config_data, **kwargs): - """ - Perform a PUT call to upload a new running-config - - :param config_data: Dictionary containing config contents - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - target_url = kwargs["url"] + "fullconfigs/running-config" - post_data = json.dumps(config_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Loading config data to 'running-config' failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Loading config data to 'running-config' succeeded") - return True - - -def copy_config(src_config_name, dst_config_name, **kwargs): - """ - Perform a PUT call to copy contents from one config into another config - - :param src_config_name: Name of config to copy data from - :param dst_config_name: Name of config to copy data into - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - query = {"from": "/rest/v10.04/fullconfigs/%s" % src_config_name} - - target_url = kwargs["url"] + "fullconfigs/%s" % dst_config_name - - response = kwargs["s"].put(target_url, params=query, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Copying config data from '%s' to '%s' failed with status code %d: %s" - % (src_config_name, dst_config_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Copying config data from '%s' to '%s' succeeded" - % (src_config_name, dst_config_name)) - return True diff --git a/pyaoscx/configuration.py b/pyaoscx/configuration.py new file mode 100644 index 0000000..e63bf44 --- /dev/null +++ b/pyaoscx/configuration.py @@ -0,0 +1,503 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.verification_error import VerificationError +from pyaoscx.session import Session +import pyaoscx.utils.util as utils +from pyaoscx.utils.connection import connected + +import logging +import json + + +class Configuration(): + ''' + Represents a Device's Configuration and all of its attributes. + Keeping all configuration information + ''' + + base_uri = "system" + + def __init__(self, session): + self.session = session + # Used to set attributes + self.config_attrs = [] + self.materialized = False + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self): + ''' + Perform a GET call to retrieve system attributes + + ''' + logging.info("Retrieving the switch attributes and capabilities") + depth = self.session.api_version.default_depth + uri = "{base_url}{class_uri}?depth={depth}".format( + base_url=self.session.base_url, + class_uri=Configuration.base_uri, + depth=depth + ) + + try: + response = self.session.s.get( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Create class attributes using util.create_attrs + utils.create_attrs(self, data) + + # Second GET request to obtain just the variables that are writable + selector = self.session.api_version.default_selector + payload = { + "depth": depth, + "selector": selector + } + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Configuration.base_uri, + depth=self.session.api_version.default_depth + ) + try: + response = self.session.s.get( + uri, verify=False, + proxies=self.session.proxy, + params=payload) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Configurable data + config_data = json.loads(response.text) + + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, config_data, 'config_attrs') + + # Set original attributes + self.__original_attributes = config_data + + # Set device as materialized + self.materialized = True + + @connected + def apply(self): + ''' + Main method used to update System Attributes + Checks whether the System is materialized + Calls self.update() if the configuration is being updated + + :return modified: Boolean, True if object was modified + ''' + modified = False + if self.materialized: + modified = self.update() + else: + raise VerificationError("Device", "Not materialized") + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to a Device Configuration + :return modified: Boolean, True if object was modified + ''' + + system_data = {} + + system_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Configuration.base_uri + ) + + # Compare dictionaries + if system_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + + put_data = json.dumps(system_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=put_data, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, + response.status_code, + "UPDATE SYSTEM ATTRIBUTES") + + else: + logging.info("SUCCESS: Updating System Attributes") + # Set new original attributes + self.__original_attributes = system_data + + # Object was modified, returns True + modified = True + + return modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def get_full_config(self, config_name='running-config'): + ''' + Perform a GET request to obtain the device's full config + :param config_name: String with the local-config name wanted + Defaults to running-config + :return config_data: Data containing the full configuration + ''' + uri = "{base_url}fullconfigs/{cfg}".format( + base_url=self.session.base_url, + cfg=config_name + ) + try: + response = self.session.s.get( + uri, verify=False, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Configurable data + config_data = json.loads(response.text) + + return config_data + + def tftp_switch_config_from_remote_location(self, config_file_location, + config_name, vrf): + ''' + TFTP switch config from TFTP server. + :param config_file_location: TFTP server address and path for uploading configuration. + :param config_name: Config file or checkpoint to be uploaded to. When using TFTP + only running-config or startup-config can be used. + :param vrf: VRF to be used to contact TFTP server, required if remote_output_file_tftp_path is provided. + :return success: Return True if response is successful or False if it was not. + ''' + success = False + uri = '{base_url}fullconfigs/'\ + '{cfg}?from={dest}&vrf={vrf}'.format( + base_url=self.session.base_url, + cfg=config_name, + dest=config_file_location, + vrf=vrf) + + try: + response = self.session.s.put( + uri, verify=False, + proxies=self.session.proxy) + + success = True + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError(response.text, response.status_code) + + return success + + def copy_switch_config_to_remote_location(self, config_name, config_type, + destination, vrf): + ''' + Copy TFTP switch config to TFTP server using a PUT request + + :param config_name: String with the config file or checkpoint to be + downloaded. When using TFTP + only running-config or startup-config can be used + :param config_type: Configuration type to be downloaded, JSON or CLI + version of the config. 'json' or 'cli' + :param destination: TFTP server address and path for + copying off configuration, must be reachable through provided vrf + :param vrf: VRF to be used to contact TFTP server + :return True if completed + ''' + + uri = '{base_url}fullconfigs/'\ + '{cfg}?to={dest}&type={type}'\ + '&vrf={vrf}'.format( + base_url=self.session.base_url, + cfg=config_name, + dest=destination, + type=config_type, + vrf=vrf) + try: + response = self.session.s.get( + uri, verify=False, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # If no errors, return True for completion + return True + + def backup_configuration(self, config_name, output_file=None, + vrf=None, config_type='json', + remote_file_tftp_path=None): + ''' + Obtains the switch's full config in json format and saves it to a local file + or a remote location over TFTP + :param config_name: String with the config file or checkpoint to be + downloaded. When using TFTP + only running-config or startup-config can be used + :param output_file: String with the File name and path for locally + downloading configuration, only JSON version of configuration will + be downloaded + :param vrf: VRF to be used to contact TFTP server + :param config_type: Configuration type to be downloaded, JSON or CLI + version of the config. 'json' or 'cli' + Defaults to json + :param remote_file_tftp_path: TFTP server address and path for + copying off configuration, must be reachable through provided vrf + :return bool: True if success + + ''' + success = False + + if remote_file_tftp_path is not None: + tftp_path = remote_file_tftp_path + if vrf is None: + raise VerificationError( + 'Backup Config', + "VRF needs to be provided in order to TFTP " + "the configuration from the switch") + + tftp_path_encoded = utils._replace_special_characters(tftp_path) + + if config_name != 'running-config' and \ + config_name != 'startup-config': + raise VerificationError( + 'Backup Config', + "Only running-config or " + + "startup-config can be backed-up using TFTP") + success = self.copy_switch_config_to_remote_location( + config_name, config_type, tftp_path_encoded, vrf) + else: + config_json = self.get_full_config() + with open(output_file, 'w') as to_file: + formatted_file = json.dumps(config_json, indent=4) + to_file.write(formatted_file) + + success = True + + # Return result + return success + + def create_checkpoint(self, source_config, destination_config): + ''' + Perform a PUT request to create a new checkpoint or copy an + existing checkpoint to AOS-CX switch config. + + :param source_config: Name of the source configuration + from which checkpoint needs to be created or copied. + :param destination_config: Name of the destination configuration + or name of checkpoint. + :return bool: True if success + + ''' + success = False + + uri = '{base_url}fullconfigs/{dest}?from={prefix}fullconfigs/{src}'.format( + base_url=self.session.base_url, + prefix=self.session.resource_prefix, + dest=destination_config, + src=source_config) + + try: + response = self.session.s.put( + uri, verify=False, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError(response.text, response.status_code) + + success = True + + # Return result + return success + + def setup_mgmt_nameservers_dns(self, primary=None, secondary=None): + """ + Setup primary and secondary name servers on a mgmt interface + + :param primary: Primary nameservers on mgmt interface, + a IPv4 address. + Example: + "10.10.2.10" + :param secondary: Secondary nameservers on mgmt interface, + a IP address. + Example: + "10.10.2.10" + + :return modified: Return True if coinfig was modified + """ + + if 'mode' in self.mgmt_intf: + mgmt_if_mode = self.mgmt_intf['mode'] + else: + mgmt_if_mode = 'dhcp' + + if mgmt_if_mode != 'static': + message_part1 = "The management interface must have static" + message_part2 = "IP to configure management interface name servers" + raise Exception(message_part1 + ' ' + message_part2) + + if primary is not None: + self.mgmt_intf['dns_server_1'] = primary + elif secondary is not None: + self.mgmt_intf['dns_server_2'] = secondary + + return self.apply() + + def delete_mgmt_nameservers_dns(self): + """ + Delete primary and secondary name servers on a mgmt interface + + :return modified: Return True if coinfig was modified + """ + + if 'dns_server_1' in self.mgmt_intf: + self.mgmt_intf.pop('dns_server_1') + + if 'dns_server_2' in self.mgmt_intf: + self.mgmt_intf.pop('dns_server_2') + + return self.apply() + + def upload_switch_config(self, + config_name=None, + config_file=None, + config_json=None, + vrf=None, + remote_file_tftp_path=None): + ''' + Uploads configuration from a configuration file. + :param config_name: String with the Config file or checkpoint to be uploaded to. + When using TFTP only running-config or startup-config can be used. + Default: None. + :param config_file: String with the File name and path for locally downloading + configuration, only JSON version of configuration will be downloaded. + Default: None. + :param config_json: String with the JSON file name and path for locally uploading configuration, + only JSON version of configuration can be uploaded. + Default: None. + :param vrf: String for VRF to be used to contact TFTP server, required if + remote_output_file_tftp_path is provided. + Default: None. + :param remote_file_tftp_path: String for TFTP server address and path for copying off + configuration, must be reachable through provided vrf. + Default: None. + :return success: Return boolean True if response is successful or False if it was not. + ''' + + success = False + + if remote_file_tftp_path is not None: + if vrf is None: + raise VerificationError( + "Upload Config", + "VRF needs to be provided in order to TFTP " + "the configuration onto the switch") + + tftp_path_encoded = utils._replace_special_characters( + remote_file_tftp_path) + + if config_name != 'running-config' and config_name != 'startup-config': + raise VerificationError( + "Upload Config", + "Only running-config or startup-config " + "can be uploaded using TFTP") + + success = self.tftp_switch_config_from_remote_location( + tftp_path_encoded, config_name, vrf) + + else: + + success = self.upload_switch_config_from_local( + config_json, config_file, config_name) + + return success + + def upload_switch_config_from_local(self, + config_json=None, + config_file=None, + config_name=None): + ''' + Uploads configuration from a configuration file. + :param config_name: String with the Config file or checkpoint to be uploaded to. + When using TFTP only running-config or startup-config can be used. + Default: None. + :param config_file: String with the File name and path for locally downloading + configuration, only JSON version of configuration will be downloaded. + Default: None. + :param config_json: String with the JSON file name and path for locally uploading configuration, + only JSON version of configuration can be uploaded. + Default: None. + :return success: Return boolean True if response is successful or False if it was not. + ''' + success = False + + if config_json: + with open(config_json) as json_file: + config_json = json.load(json_file) + + if config_file: + with open(config_file) as json_file: + config_json = json.load(json_file) + + config_json = json.dumps(config_json) + + # Create URI from the session base url and the configuration name + uri = '{base_url}fullconfigs/{cfg}'.format( + base_url=self.session.base_url, + cfg=config_name + ) + try: + # Put (REST) configuration file + response = self.session.s.put( + url=uri, + verify=False, + proxies=self.session.proxy, + data=config_json) + + success = True + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + return success \ No newline at end of file diff --git a/pyaoscx/device.py b/pyaoscx/device.py new file mode 100644 index 0000000..70f54ea --- /dev/null +++ b/pyaoscx/device.py @@ -0,0 +1,454 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.verification_error import VerificationError +from pyaoscx.session import Session +import pyaoscx.utils.util as utils +from pyaoscx.utils.connection import connected + + +import logging +import json + + +class Device(): + ''' + Represents a Device and all of its attributes. Keeping all the important information + inside one class. + ''' + + base_uri = "system" + + def __init__(self, session): + self.session = session + self.firmware_version = None + # Used to set attributes + self.config_attrs = [] + self.materialized = False + # Set firmware version + self.get_firmware_version() + + @connected + def get(self): + ''' + Perform a GET call to retrieve device attributes + After data from response, Device class attributes + are generated using create_attrs() + + ''' + logging.info("Retrieving the switch attributes and capabilities") + attributes = [ + 'software_version', + 'software_images', + 'software_info', + 'platform_name', + 'hostname', + 'boot_time', + 'mgmt_intf_status', + 'aruba_central', + 'capabilities', + 'capacities', + 'admin_password_set', + 'other_config', + 'domain_name' + ] + + attributes_list = ','.join(attributes) + uri = "{}system?attributes={}&depth={}".format( + self.session.base_url, attributes_list, + self.session.api_version.default_depth) + + try: + response = self.session.s.get( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Load into json format + data = json.loads(response.text) + + # Create class attributes using util.create_attrs + utils.create_attrs(self, data) + + # Set device as materialized + self.materialized = True + + @connected + def get_subsystems(self): + ''' + Perform GET call to retrieve subsystem attributes and create a dictionary containing them + ''' + # Log + logging.info("Retrieving the switch subsystem attributes and capabilities") + + # Attribute list + attributes = [ + 'product_info', + 'power_supplies', + 'interfaces', + 'fans', + 'resource_utilization' + ] + + # Format attribute list by joining every element with a comma + attributes_list = ','.join(attributes) + + # Build URI + uri = "{}system/subsystems?attributes={}&depth={}".format( + self.session.base_url, attributes_list, + self.session.api_version.default_subsystem_facts_depth) + + try: + # Try to perform a GET call and retrieve the data + response = self.session.s.get( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Load into json format + data = json.loads(response.text) + data_subsystems = {'subsystems' : data} + + # Create class attributes using util.create_attrs + utils.create_attrs(self, data_subsystems) + + @connected + def get_firmware_version(self): + ''' + Perform a GET call to retrieve device firmware version + :return: firmware_version: The firmware version + ''' + + uri = "{}firmware".format(self.session.base_url) + + try: + response = self.session.s.get( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + self.firmware_version = data["current_version"] + # Return Version + return self.firmware_version + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def update_banner(self, banner_info, banner_type='banner'): + ''' + Perform a PUT request to modify a Device's Banner + :param banner_info: String to be configured as the banner. + :param banner_type: Type of banner being configured on the switch. + Either banner or banner_exec + :return modified: Returns True if Banner was modified. + False otherwise + ''' + modified = False + + logging.info("Setting Banner") + depth = self.session.api_version.default_depth + + # Second GET request to obtain just the variables that are writable + selector = self.session.api_version.default_selector + payload = { + "depth": depth, + "selector": selector + } + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Device.base_uri, + depth=self.session.api_version.default_depth + ) + try: + response = self.session.s.get( + uri, verify=False, + proxies=self.session.proxy, + params=payload) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Configurable data + config_data = json.loads(response.text) + + # If Banner type does not exist + if banner_type not in config_data['other_config']: + # Create Banner type + config_data['other_config'][banner_type] = "" + + # Verify data is different + if config_data['other_config'][banner_type] == banner_info: + modified = False + + else: + # Modify Banner + config_data['other_config'][banner_type] = banner_info + + # UPDATE Banner + put_uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Device.base_uri + ) + # Set data to be used inside PUT + put_data = json.dumps(config_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + put_uri, verify=False, data=put_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code, "UPDATE SYSTEM BANNER") + + # Object was modified, returns True + modified = True + + return modified + + def delete_banner(self, banner_type='banner'): + ''' + Perform a DELETE request to delete a device's Banner + :param banner_type: Type of banner being removed on the switch. + Either banner or banner_exec + :return modified: Returns True if Banner was modified. + False otherwise + ''' + logging.info("Removing Banner") + depth = self.session.api_version.default_depth + + # Second GET request to obtain just the variables that are writable + selector = self.session.api_version.default_selector + payload = { + "depth": depth, + "selector": selector + } + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Device.base_uri, + depth=self.session.api_version.default_depth + ) + try: + response = self.session.s.get( + uri, verify=False, + proxies=self.session.proxy, + params=payload) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Configurable data + config_data = json.loads(response.text) + + # If Banner type does not exist + if banner_type not in config_data['other_config']: + modified = False + + else: + # Delete Banner + config_data['other_config'].pop(banner_type) + + # UPDATE Banner + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Device.base_uri + ) + + put_data = json.dumps(config_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=put_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code, "DELETE Banner") + + # Object was modified, returns True + modified = True + + return modified + + def boot_firmware(self, partition_name='primary'): + ''' + Perform a POST request to Boot the AOS-CX switch with image present + to the specified partition + :param partition_name: String name of the partition for device to boot to. + + :return bool: True if success + + ''' + # Lower case for partition name + partition_name = partition_name.lower() + if partition_name not in ['primary', 'secondary']: + raise VerificationError('Boot Firmware', 'Bad partition name') + + success = False + uri = '{base_url}boot?image={part}'.format( + base_url=self.session.base_url, + part=partition_name) + + try: + self.session.s.post( + uri, verify=False, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + success = True + + # Return result + return success + + def upload_firmware_http(self, remote_firmware_file_path, + vrf, + partition_name='primary'): + ''' + Perform a PUT request to upload a firmware image given + a http_request + + :param remote_firmware_file_path: "HTTP server address and path for + uploading firmware image, must be reachable through provided vrf + ex: http://192.168.1.2:8000/TL_10_04_0030A.swi" + :param vrf: VRF to be used to contact HTTP server, required if + remote_firmware_file_path is provided + :param partition_name: Name of the partition for the + image to be uploaded to. + :return bool: True if success + ''' + http_path = remote_firmware_file_path + unsupported_versions = [ + "10.00", + "10.01", + "10.02", + "10.03", + ] + # Verify Version + for version in unsupported_versions: + if version in self.firmware_version: + raise VerificationError( + 'Upload Firmware through HTTPs', + "Minimum supported firmware version is 10.04 for" + + " remote firmware upload, your version is {firmware}" + .format(firmware=self.firmware_version)) + # Verify VRF + if vrf is None: + raise VerificationError( + 'VRF', + "VRF needs to be provided in order" + + " to upload firmware from HTTP server") + http_path_encoded = utils._replace_special_characters(http_path) + + # Build URI + uri = '{base_url}firmware?image={part}&from={path}&vrf={vrf}'\ + .format( + base_url=self.session.base_url, + part=partition_name, + path=http_path_encoded, + vrf=vrf) + + # PUT for a HTTP Request + try: + response = self.session.s.put( + uri, verify=False, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + # True if successful + return True + + def upload_firmware_local(self, partition_name='primary', + firmware_file_path=None): + ''' + Perform a POST request to upload a firmware image from a local file + + :param partition_name: Name of the partition for the + image to be uploaded to. + :param firmware_file_path: File name and path for local file uploading + firmware image + :return success: True if success + ''' + + uri = '{base_url}firmware?image={part}'.format( + base_url=self.session.base_url, + part=partition_name) + + # Upload file + success = utils.file_upload(self.session, firmware_file_path, uri) + # If no errors detected + return success + + def upload_firmware(self, partition_name=None, + firmware_file_path=None, + remote_firmware_file_path=None, + vrf=None): + ''' + Upload a firmware image from a local file OR from a remote location + + :param partition_name: Name of the partition for the + image to be uploaded to. + :param firmware_file_path: File name and path for local file uploading + firmware image. + IMPORTANT: For this to be used, the remote_firmware_file_path + parameter must be left as NONE + :param remote_firmware_file_path: HTTP server address and path for + uploading firmware image, must be reachable through provided vrf + ex: http://192.168.1.2:8000/TL_10_04_0030A.swi + :param vrf: VRF to be used to contact HTTP server, required if + remote_firmware_file_path is provided + :return bool: True if success + ''' + result = None + if partition_name is None: + partition_name = 'primary' + + # Use HTTP Server + if remote_firmware_file_path is not None: + result = self.upload_firmware_http( + remote_firmware_file_path, + vrf, + partition_name) + + # Locally + else: + result = self.upload_firmware_local( + partition_name, + firmware_file_path) + + # If no errors detected + return result diff --git a/pyaoscx/dhcp.py b/pyaoscx/dhcp.py deleted file mode 100644 index bad9c22..0000000 --- a/pyaoscx/dhcp.py +++ /dev/null @@ -1,328 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import json -import logging - - -def get_dhcp_relay(vrf_name, port_name, **kwargs): - """ - Perform a GET call to get DHCP data for an interface - - :param vrf_name: Alphanumeric name of VRF - :param port_name: L3 interface's Port table entry name - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing DHCP Relay data for interface - """ - if kwargs["url"].endswith("/v1/"): - dhcp_relays = _get_dhcp_relay_v1(vrf_name, port_name, **kwargs) - else: # Updated else for when version is v10.04 - dhcp_relays = _get_dhcp_relay(vrf_name, port_name, **kwargs) - return dhcp_relays - - -def _get_dhcp_relay_v1(vrf_name, port_name, **kwargs): - """ - Perform a GET call to get DHCP data for an interface - - :param vrf_name: Alphanumeric name of VRF - :param port_name: L3 interface's Port table entry name - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing DHCP Relay data for interface - """ - payload = {"selector": "configuration"} - - target_url = kwargs["url"] + "system/dhcp_relays/%s/%s" % (vrf_name, port_name) - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting IPv4 DHCP helper(s) for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting IPv4 DHCP helper(s) for Port '%s' succeeded" % port_name) - - return response.json() - - -def _get_dhcp_relay(vrf_name, port_name, **kwargs): - """ - Perform a GET call to get DHCP data for an interface - - :param vrf_name: Alphanumeric name of VRF - :param port_name: L3 interface's Port table entry name - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing DHCP Relay data for interface - """ - payload = {"selector": "writable"} - - target_url = kwargs["url"] + "system/dhcp_relays/%s,%s" % (vrf_name, port_name) - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting IPv4 DHCP helper(s) for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting IPv4 DHCP helper(s) for Port '%s' succeeded" % port_name) - - return response.json() - - -def get_all_dhcp_relays(**kwargs): - """ - Perform a GET call to get a list (or dictionary) of all entries in DHCP Relays table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dict of all DHCP helpers in the table - """ - target_url = kwargs["url"] + "system/dhcp_relays" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dict of all DHCP Relay table entries failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list/dict of all DHCP Relay table entries succeeded") - - dhcp_helpers = response.json() - return dhcp_helpers - - -def add_dhcp_relays(port_name, vrf_name, ipv4_helper_addresses, **kwargs): - """ - Perform a POST call to add IPv4 DHCP helper(s) for an L3 interface. If there are already IPv4 helpers, the new - helpers are added in addition to the already existing helpers. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param ipv4_helper_addresses: List of IPv4 addresses to add as DHCP helpers - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _add_dhcp_relays_v1(port_name, vrf_name, ipv4_helper_addresses, **kwargs) - else: # Updated else for when version is v10.04 - return _add_dhcp_relays(port_name, vrf_name, ipv4_helper_addresses, **kwargs) - - -def _add_dhcp_relays_v1(port_name, vrf_name, ipv4_helper_addresses, **kwargs): - """ - Perform a POST call to add IPv4 DHCP helper(s) for an L3 interface. If there are already IPv4 helpers, the new - helpers are added in addition to the already existing helpers. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param ipv4_helper_addresses: List of IPv4 addresses to add as DHCP helpers - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - dhcp_relays_list = get_all_dhcp_relays(**kwargs) - - if "/rest/v1/system/dhcp_relays/%s/%s" % (vrf_name, port_name) not in dhcp_relays_list: - dhcp_relays = { - "port": kwargs["url"] + "system/ports/%s" % port_name, - "vrf": kwargs["url"] + "system/vrfs/%s" % vrf_name, - "ipv4_ucast_server": ipv4_helper_addresses - } - - target_url = kwargs["url"] + "system/dhcp_relays" - post_data = json.dumps(dhcp_relays, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding IPv4 DHCP helpers %s to SVI Port '%s' failed with status code %d: %s" % - (repr(ipv4_helper_addresses), port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding IPv4 DHCP helpers '%s' to SVI Port '%s' succeeded" % - (repr(ipv4_helper_addresses), port_name)) - return True - - else: - dhcp_data = get_dhcp_relay(vrf_name, port_name, **kwargs) - - dhcp_data['ipv4_ucast_server'] = common_ops._list_remove_duplicates( - dhcp_data['ipv4_ucast_server'] + ipv4_helper_addresses) - - if len(dhcp_data['ipv4_ucast_server']) > 8: - raise Exception("Can't have more than 8 IPv4 DHCP helpers per interface!") - - dhcp_data.pop('port', None) # Must remove this item from json since it can't be modified - dhcp_data.pop('vrf', None) # Must remove this item from json since it can't be modified - - target_url = kwargs["url"] + "system/dhcp_relays/%s/%s" % (vrf_name, port_name) - put_data = json.dumps(dhcp_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding IPv4 DHCP helpers %s to SVI Port '%s' failed with status code %d: %s" % - (repr(ipv4_helper_addresses), port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding IPv4 DHCP helpers '%s' to SVI Port '%s' succeeded" % - (repr(ipv4_helper_addresses), port_name)) - return True - - -def _add_dhcp_relays(port_name, vrf_name, ipv4_helper_addresses, **kwargs): - """ - Perform a POST call to add IPv4 DHCP helper(s) for an L3 interface. If there are already IPv4 helpers, the new - helpers are added in addition to the already existing helpers. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param ipv4_helper_addresses: List of IPv4 addresses to add as DHCP helpers - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - dhcp_relays_dict = get_all_dhcp_relays(**kwargs) - - if "%s,%s" % (vrf_name, port_name) not in dhcp_relays_dict: - dhcp_relays = { - "port": "/rest/v10.04/system/interfaces/%s" % port_name, - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf_name, - "ipv4_ucast_server": ipv4_helper_addresses - } - - target_url = kwargs["url"] + "system/dhcp_relays" - post_data = json.dumps(dhcp_relays, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding IPv4 DHCP helpers %s to SVI Port '%s' failed with status code %d: %s" % - (repr(ipv4_helper_addresses), port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding IPv4 DHCP helpers '%s' to SVI Port '%s' succeeded" % - (repr(ipv4_helper_addresses), port_name)) - return True - else: - dhcp_data = get_dhcp_relay(vrf_name, port_name, **kwargs) - - dhcp_data['ipv4_ucast_server'] = common_ops._list_remove_duplicates( - dhcp_data['ipv4_ucast_server'] + ipv4_helper_addresses) - - if len(dhcp_data['ipv4_ucast_server']) > 8: - raise Exception("Can't have more than 8 IPv4 DHCP helpers per interface!") - - dhcp_data.pop('dhcp_relay_v6_mcast_servers', None) # Must remove this item from json since it can't be modified - - target_url = kwargs["url"] + "system/dhcp_relays/%s,%s" % (vrf_name, port_name) - put_data = json.dumps(dhcp_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding IPv4 DHCP helpers %s to SVI Port '%s' failed with status code %d: %s" % - (repr(ipv4_helper_addresses), port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding IPv4 DHCP helpers '%s' to SVI Port '%s' succeeded" % - (repr(ipv4_helper_addresses), port_name)) - return True - - -def delete_dhcp_relays(port_name, vrf_name="default", **kwargs): - """ - Perform a DELETE call to delete all the IPv4 DHCP helper(s) for an L3 interface. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_dhcp_relays_v1(port_name, vrf_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_dhcp_relays(port_name, vrf_name, **kwargs) - - -def _delete_dhcp_relays_v1(port_name, vrf_name="default", **kwargs): - """ - Perform a DELETE call to delete all the IPv4 DHCP helper(s) for an L3 interface. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - dhcp_helpers_list = get_all_dhcp_relays(**kwargs) - - if "/rest/v1/system/dhcp_relays/%s/%s" % (vrf_name, port_name) in dhcp_helpers_list: - - target_url = kwargs["url"] + "system/dhcp_relays/%s/%s" % (vrf_name, port_name) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting all DHCP relays from interface Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting all DHCP relays from interface Port '%s' succeeded" % port_name) - return True - else: - logging.info("SUCCESS: No need to delete DHCP relays from SVI Port '%s' since they don't exist" - % port_name) - return True - - -def _delete_dhcp_relays(port_name, vrf_name="default", **kwargs): - """ - Perform a DELETE call to delete all the IPv4 DHCP helper(s) for an L3 interface. - - :param port_name: Alphanumeric name of the Port - :param vrf_name: Alphanumeric name of the VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - dhcp_helpers_dict = get_all_dhcp_relays(**kwargs) - - if "%s,%s" % (vrf_name, port_name) in dhcp_helpers_dict: - - target_url = kwargs["url"] + "system/dhcp_relays/%s,%s" % (vrf_name, port_name) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting all DHCP relays from interface Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting all DHCP relays from interface Port '%s' succeeded" % port_name) - return True - else: - logging.info("SUCCESS: No need to delete DHCP relays from SVI Port '%s' since they don't exist" - % port_name) - return True diff --git a/pyaoscx/dhcp_relay.py b/pyaoscx/dhcp_relay.py new file mode 100644 index 0000000..f1c8bd4 --- /dev/null +++ b/pyaoscx/dhcp_relay.py @@ -0,0 +1,456 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.utils.connection import connected +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.interface import Interface +from pyaoscx.vrf import Vrf + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class DhcpRelay(PyaoscxModule): + ''' + Provide configuration management for DHCP Relay on AOS-CX devices. + ''' + + base_uri = "system/dhcp_relays" + resource_uri_name = 'dhcp_relays' + + indices = ['vrf', 'port'] + + def __init__(self, session, vrf, port, uri=None, **kwargs): + self.session = session + # Assign IDs + self.vrf = vrf + self.port = port + self._uri = uri + # List used to determine attributes related to the DHCP Relay + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a DHCP Relay table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch DHCP Relays") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=DhcpRelay.base_uri, + id1=self.vrf.name, + separator=self.session.api_version.compound_index_separator, + id2=self.port.percents_name + ) + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Remove fields because they are not needed for the PUT request + if 'vrf' in data: + data.pop('vrf') + if 'port' in data: + data.pop('port') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the DHCP Relay is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['vrf', 'port']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'vrf' in self.__original_attributes: + self.__original_attributes.pop('vrf') + # Remove ID + if 'port' in self.__original_attributes: + self.__original_attributes.pop('port') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system DHCP Relays, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing DHCP Relays IDs as keys and a + DHCP Relay objects as values + ''' + + logging.info("Retrieving the switch DHCP Relays") + + uri = "{base_url}{class_uri}".format( + base_url=session.base_url, + class_uri=DhcpRelay.base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + dhcp_relay_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a DHCP Relay object + indices, dhcp_relay = DhcpRelay.from_uri( + session, uri) + dhcp_relay_dict[indices] = dhcp_relay + + return dhcp_relay_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + DHCP Relay table entry. + Checks whether the DHCP Relay exists in the switch + Calls self.update() if DHCP Relay is being updated + Calls self.create() if a new DHCP Relay is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing DHCP Relay table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + dhcp_relay_data = {} + + dhcp_relay_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=DhcpRelay.base_uri, + id1=self.vrf.name, + separator=self.session.api_version.compound_index_separator, + id2=self.port.percents_name + ) + + # Compare dictionaries + if dhcp_relay_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + + post_data = json.dumps(dhcp_relay_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update DHCP Relay table entry {} succeeded") + # Set new original attributes + self.__original_attributes = dhcp_relay_data + + # Object was modified, returns True + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new DHCP Relay table entry + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + + ''' + + dhcp_relay_data = {} + + dhcp_relay_data = utils.get_attrs(self, self.config_attrs) + dhcp_relay_data['vrf'] = self.vrf.get_info_format() + dhcp_relay_data['port'] = self.port.get_info_format() + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=DhcpRelay.base_uri + ) + post_data = json.dumps(dhcp_relay_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding DHCP Relay table entry {} succeeded\ + ".format(self.vrf)) + + # Get all object's data + self.get() + + # Object was created, means modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete DhcpRelay table entry. + + ''' + + uri = "{base_url}{class_uri}/{id1}{separator}{id2}".format( + base_url=self.session.base_url, + class_uri=DhcpRelay.base_uri, + id1=self.vrf.name, + separator=self.session.api_version.compound_index_separator, + id2=self.port.percents_name + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete DHCP Relay table entry {} succeeded\ + ".format(self.vrf)) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, response_data): + ''' + Create a DhcpRelay object given a response_data + :param cls: Class calling the method + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/ + /dhcp_relays/{vrf},{port}" + } + or a + string: "/rest/v10.04/system/dhcp_relays/{vrf},{port}" + return: DhcpRelay object + ''' + dhcp_relay_arr = session.api_version.get_keys( + response_data, DhcpRelay.resource_uri_name) + port_name = dhcp_relay_arr[1] + vrf_name = dhcp_relay_arr[0] + # Create Modules + port_obj = session.api_version.get_module( + session, 'Interface', + port_name) + vrf_obj = session.api_version.get_module( + session, 'Vrf', vrf_name) + + return DhcpRelay( + session, vrf_obj, port_obj) + + @classmethod + def from_uri(cls, session, uri): + ''' + Create a DHCP Relay object given a URI + :param cls: Class calling the method + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return indices, dhcp_relay: tuple containing both the indices and + DhcpRelay object + ''' + # Obtain ID from URI + index_pattern = \ + re.compile( + r'(.*)dhcp_relays/(?P.+)/(?P.+)') + vrf = index_pattern.match(uri).group('index1') + port = index_pattern.match(uri).group('index2') + + port_obj = session.api_version.get_module( + session, 'Interface', + port) + vrf_obj = session.api_version.get_module( + session, 'Vrf', vrf) + + # Create DHCP Relay object + dhcp_relay = DhcpRelay(session, vrf_obj, port_obj) + indices = "{},{}".format(vrf, port) + + return indices, dhcp_relay + + def __str__(self): + return "DhcpRelay vrf:{}, port:{}".format(self.vrf, self.port.name) + + def get_uri(self): + ''' + Method used to obtain the specific DhcpRelay URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = \ + '{resource_prefix}{class_uri}/{id1}{separator}{id2}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=DhcpRelay.base_uri, + id1=self.vrf.name, + separator=self.session.api_version.compound_index_separator, + id2=self.port.percents_name + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def add_ipv4_addresses(self, ipv4_list): + """ + Perform a PUT calls to modify an existing DhcpRelay. + Adding a list of IPv4 addresses into IPv4_ucast_server + :param ipv4_list: List of IPv4 addresses + Example: + ['1.1.1.1', '2.2.2.2'] + :return: True if object was changed + """ + + # Set IPv4 + if ipv4_list is not None and ipv4_list != []: + for i in range(len(ipv4_list)): + if ipv4_list[i] not in self.ipv4_ucast_server: + self.ipv4_ucast_server.append(ipv4_list[i]) + + # Apply changes inside switch + return self.apply() + + def add_ipv6_addresses(self, ipv6_list): + """ + Perform a PUT calls to modify an existing DhcpRelay. + Adding a list of IPv6 addresses into IPv6_ucast_server + :param ipv6_list: List of IPv6 addresses + Example: + ['2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'] + :return: True if object was changed + + """ + + # Set IPv6 + if ipv6_list is not None and ipv6_list != []: + for i in range(len(ipv6_list)): + if ipv6_list[i] not in self.ipv6_ucast_server: + self.ipv6_ucast_server.append(ipv6_list[i]) + + # Apply changes inside switch + return self.apply() diff --git a/pyaoscx/dns.py b/pyaoscx/dns.py new file mode 100644 index 0000000..7bd0119 --- /dev/null +++ b/pyaoscx/dns.py @@ -0,0 +1,342 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.configuration import Configuration +from pyaoscx.utils.connection import connected + + +class Dns(PyaoscxModule): + ''' + Provide configuration management for DNS on AOS-CX devices. + As it is a special module, behaves differently. + ''' + + base_uri_vrf = 'system/vrf' + + def __init__(self, session, + vrf_name, + domain_name=None, + domain_list=None, + domain_servers=None, + host_v4_address_mapping=None, + host_v6_address_mapping=None, + uri=None): + + self.session = session + self._uri = uri + # List used to determine attributes related to the DNS configuration + self.config_attrs = [] + self.materialized = False + # Attributes needed for DNS + self.vrf_name = vrf_name + self.dns_domain_name = domain_name + self.dns_domain_list = domain_list + self.dns_name_servers = domain_servers + self.dns_host_v4_address_mapping = host_v4_address_mapping + self.dns_host_v6_address_mapping = host_v6_address_mapping + + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + self.create_attrs = [ + 'dns_domain_name', + 'dns_domain_list', + 'dns_name_servers', + 'dns_host_v4_address_mapping', + 'dns_host_v6_address_mapping' + ] + # Attribute used to know if object was changed recently + self.__modified = False + # VRF attribute where configurable attributes go + self.__internal_vrf = None + # Try to create VRF + self.__internal_vrf = self.session.api_version.get_module( + self.session, 'Vrf', self.vrf_name) + # Materialize internal VRF + self.__internal_vrf.get() + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a DNS inside the VRF + table entry and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + # Get VRF + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', self.vrf_name) + + # Get vrf Object + vrf_obj.get() + + # Sets internal VRF + self.__internal_vrf = vrf_obj + + # Set attributes with incoming VRF attributes + if self.dns_domain_name is None: + self.dns_domain_name = self.__internal_vrf.dns_domain_name + if self.dns_domain_list is None: + self.dns_domain_list = self.__internal_vrf.dns_domain_list + if self.dns_name_servers is None: + self.dns_name_servers = self.__internal_vrf.dns_name_servers + if self.dns_host_v4_address_mapping is None: + self.dns_host_v4_address_mapping = \ + self.__internal_vrf.dns_host_v4_address_mapping + if self.dns_host_v6_address_mapping is None: + self.dns_host_v6_address_mapping = \ + self.__internal_vrf.dns_host_v6_address_mapping + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session): + ''' + Method not required for DNS + ''' + pass + + @connected + def apply(self): + ''' + Main method used to either create a new DNS or update an existing + DNS, configuring it inside the Vrf object. + Checks whether the DNS exists in the switch + Calls self.update() if DNS configuration is being updated + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + modified = False + # Apply changes + modified = self.update() + + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing DNS + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + # Obtain variables + self.__internal_vrf.dns_domain_name = self.dns_domain_name + self.__internal_vrf.dns_domain_list = self.dns_domain_list + self.__internal_vrf.dns_name_servers = self.dns_name_servers + self.__internal_vrf.dns_host_v4_address_mapping = \ + self.dns_host_v4_address_mapping + self.__internal_vrf.dns_host_v6_address_mapping = \ + self.dns_host_v6_address_mapping + + # Applies changes inside VRF + modified = self.__internal_vrf.apply() + return modified + + @connected + def create(self): + ''' + Method not implemented + + ''' + pass + + @connected + def delete(self): + ''' + Perform DELETE call to delete DNS. + + ''' + + # Delete the dns settings inside the VRF + self.dns_domain_name = None + self.dns_domain_list = None + self.dns_name_servers = None + self.dns_host_v4_address_mapping = None + self.dns_host_v6_address_mapping = None + + # Make changes + return self.apply() + + @classmethod + def from_response(cls, session, response_data): + ''' + Not applicable for DNS + + ''' + pass + + @classmethod + def from_uri(cls, session, uri): + ''' + Not applicable for DNS + + ''' + pass + + def __str__(self): + return "DNS object with VRF: '{}'".format(self.vrf_name) + + def get_uri(self): + ''' + Not applicable for DNS + + ''' + pass + + def get_info_format(self): + ''' + Not applicable for DNS + + ''' + pass + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def setup_mgmt_nameservers(self, primary=None, secondary=None): + """ + Setup primary and secondary name servers on a mgmt interface + + :param primary: Primary nameservers on mgmt interface, + a IPv4 address. + Example: + "10.10.2.10" + :param secondary: Secondary nameservers on mgmt interface, + a IP address. + Example: + "10.10.2.10" + + :return modified: Return True if coinfig was modified + """ + # Create configuration Object + config = Configuration() + + # Return if configuration was modified + return config.setup_mgmt_nameservers_dns(primary, secondary) + + def delete_mgmt_nameservers(self): + """ + Delete primary and secondary name servers on a mgmt interface + + :return modified: Return True if coinfig was modified + """ + # Create configuration Object + config = Configuration() + + return config.delete_mgmt_nameservers_dns() + + def setup_dns(self, domain_name=None, domain_list=None, + domain_servers=None, host_v4_address_mapping=None, + host_v6_address_mapping=None): + """ + Setup DNS client configuration within a Vrf object. + + :param domain_name: Domain name used for name resolution by + the DNS client, if 'dns_domain_list' is not configured + :param domain_list: dict of DNS Domain list names to be used for + address resolution, keyed by the resolution priority order + Example: + { + 0: "hpe.com" + 1: "arubanetworks.com" + } + :param domain_servers: dict of DNS Name servers to be used for address + resolution, keyed by the resolution priority order + Example: + { + 0: "4.4.4.10" + 1: "4.4.4.12" + } + :param host_v4_address_mapping: dict of static host + address configurations and the IPv4 address associated with them + Example: + { + "host1": "5.5.44.5" + "host2": "2.2.44.2" + } + :param host_v6_address_mapping: dict of static host + address configurations and the IPv6 address associated with them + Example: + { + "host1": "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + } + :return modified: Returns True if modified, False + otherwise + """ + # Update Values + + if domain_name is not None: + self.dns_domain_name = domain_name + + if domain_list is not None: + self.dns_domain_list = domain_list + + if domain_servers is not None: + self.dns_name_servers = domain_servers + + if host_v4_address_mapping is not None: + self.dns_host_v4_address_mapping = host_v4_address_mapping + + if host_v6_address_mapping is not None: + self.dns_host_v6_address_mapping = host_v6_address_mapping + + return self.apply() + + def delete_dns(self, domain_name=None, domain_list=None, + domain_servers=None, host_v4_address_mapping=None, + host_v6_address_mapping=None): + """ + Delete DNS client configuration within a Vrf object. + + :param domain_name: If value is not None, it is deleted + :param domain_list: If value is not None, it is deleted + :param domain_servers: If value is not None, it is deleted + :param host_v4_address_mapping: If value is not None, it is deleted + :param host_v6_address_mapping: If value is not None, it is deleted + + :return modified: Returns True if modified, False + otherwise + """ + # Update Values + + if domain_name is not None: + self.dns_domain_name = None + + if domain_list is not None: + self.dns_domain_list = None + + if domain_servers is not None: + self.dns_name_servers = None + + if host_v4_address_mapping is not None: + self.dns_host_v4_address_mapping = None + + if host_v6_address_mapping is not None: + self.dns_host_v6_address_mapping = None + + return self.apply() diff --git a/pyaoscx/error.py b/pyaoscx/error.py new file mode 100644 index 0000000..7f66e4d --- /dev/null +++ b/pyaoscx/error.py @@ -0,0 +1,11 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +class LoginError(Exception): + """Exception raised for errors during login. + Attributes: + message -- explanation of the error + """ + + def __init__(self, message, status_code=None): + self.message = message diff --git a/pyaoscx/evpn.py b/pyaoscx/evpn.py deleted file mode 100644 index d318762..0000000 --- a/pyaoscx/evpn.py +++ /dev/null @@ -1,164 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import json -import logging - - -def get_evpn_info(**kwargs): - """ - Perform a GET call to receive the EVPN information on the system - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Information - """ - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/evpns" - else: - # Else logic designed for v10.04 and later - target_url = kwargs["url"] + "system/evpn" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.info("SUCCESS: Getting EVPN information replied with status code %d, no EVPN information exists" - % response.status_code) - evpn_info = [] - else: - logging.info("SUCCESS: Getting EVPN information succeeded") - evpn_info = response.json() - - return evpn_info - - -def create_evpn_instance(**kwargs): - """ - Perform POST calls to create an EVPN instance - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - current_evpn = get_evpn_info(**kwargs) - - if current_evpn: - logging.info("SUCCESS: No need to create EVPN instance since it already exists") - return True - else: - evpn_data = {} - - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/evpns" - else: - # Else logic designed for v10.04 and later - target_url = kwargs["url"] + "system/evpn" - - post_data = json.dumps(evpn_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating EVPN Instance failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating EVPN Instance succeeded") - return True - - -def delete_evpn_instance(**kwargs): - """ - Perform DELETE calls to remove an EVPN instance - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - current_evpn = get_evpn_info(**kwargs) - - if current_evpn: - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/evpns" - else: - # Else logic designed for v10.04 and later - target_url = kwargs["url"] + "system/evpn" - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting EVPN Instance failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting EVPN Instance succeeded") - return True - else: - logging.info("SUCCESS: No need to delete EVPN instance since it doesn't exist") - return True - - -def get_evpn_vlan_list(**kwargs): - """ - Perform a GET call to receive a list of VLANs associated with the EVPN instance - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of EVPN VLANs - """ - target_url = kwargs["url"] + "system/evpns/evpn_vlans" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all EVPN VLANs failed with status code %d: %s" - % (response.status_code, response.text)) - evpn_vlan_list = [] - else: - logging.info("SUCCESS: Getting list of all EVPN VLANs succeeded") - evpn_vlan_list = response.json() - - return evpn_vlan_list - - -def add_evpn_vlan(vlan_id, export_route=["auto"], import_route=["auto"], rd="auto", **kwargs): - """ - Perform POST call to create an EVPN VLAN association - Note that this functions has logic that works for both v1 and v10.04 - - :param vlan_id: Integer representing the VLAN ID - :param export_route: List of route targets to be exported from the VLAN in ASN:nn format, or auto. - :param import_route: List of route targets to be imported from the VLAN in ASN:nn format, or auto. - :param rd: Alphanumeric EVPN RD in ASN:nn format or IP:nn format, or auto. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - evpn_data = { - "export_route_targets": export_route, - "import_route_targets": import_route, - "rd": rd - } - if kwargs["url"].endswith("/v1/"): - evpn_data.update({'vlan': '/rest/v1/system/vlans/%s' % vlan_id}) - target_url = kwargs["url"] + "system/evpns/evpn_vlans" - else: - # Else logic designed for v10.04 and later - evpn_data.update({'vlan': '/rest/v10.04/system/vlans/%s' % vlan_id}) - target_url = kwargs["url"] + "system/evpn/evpn_vlans" - - post_data = json.dumps(evpn_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating EVPN VLAN '%s' association failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating EVPN VLAN '%s' association succeeded" % vlan_id) - return True diff --git a/pyaoscx/exceptions/__init__.py b/pyaoscx/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyaoscx/exceptions/generic_op_error.py b/pyaoscx/exceptions/generic_op_error.py new file mode 100644 index 0000000..47a53ce --- /dev/null +++ b/pyaoscx/exceptions/generic_op_error.py @@ -0,0 +1,33 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.pyaoscx_error import PyaoscxError + + +class GenericOperationError(PyaoscxError): + """" + Class used to add information regarding a Generic Operation Error + inside PYAOSCX + """ + + def __init__(self, *args): + if args: + super().__init__(args[0]) + self.response_code = args[1] + try: + self.extra_info = args[2] + except IndexError: + self.extra_info = None + else: + self.message = None + self.extra_info = None + + def __str__(self): + if self.message and self.extra_info: + return 'GENERIC OPERATION ERROR: {0} Code: {1} on Module {2}'.format( + self.message, self.response_code, self.extra_info) + if self.message: + return 'GENERIC OPERATION ERROR: {0} Code: {1}'.format( + self.message, self.response_code) + else: + return 'GENERIC OPERATION ERROR' diff --git a/pyaoscx/exceptions/login_error.py b/pyaoscx/exceptions/login_error.py new file mode 100644 index 0000000..e8d636d --- /dev/null +++ b/pyaoscx/exceptions/login_error.py @@ -0,0 +1,13 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + + +class LoginError(Exception): + """Exception raised for errors during login. + Attributes: + message -- explanation of the error + """ + + def __init__(self, message, status_code=None): + self.message = message + self.status_code = status_code diff --git a/pyaoscx/exceptions/pyaoscx_error.py b/pyaoscx/exceptions/pyaoscx_error.py new file mode 100644 index 0000000..7d94934 --- /dev/null +++ b/pyaoscx/exceptions/pyaoscx_error.py @@ -0,0 +1,12 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + + +class PyaoscxError(Exception): + """Base class for other PYAOSCX related exceptions""" + + def __init__(self, message): + self.message = message + + def __str__(self): + return(repr(self.message)) diff --git a/pyaoscx/exceptions/response_error.py b/pyaoscx/exceptions/response_error.py new file mode 100644 index 0000000..fdb6c3d --- /dev/null +++ b/pyaoscx/exceptions/response_error.py @@ -0,0 +1,24 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.pyaoscx_error import PyaoscxError + + +class ResponseError(PyaoscxError): + """ + Exception class for a Response Error inside PYAOSCX + """ + + def __init__(self, *args): + if args: + super().__init__(args[1]) + self.response = args[0] + else: + self.message = None + + def __str__(self): + if self.message: + return 'RESPONSE ERROR in {0}: {1}'.format( + self.message, self.response) + else: + return 'RESPONSE ERROR' diff --git a/pyaoscx/exceptions/verification_error.py b/pyaoscx/exceptions/verification_error.py new file mode 100644 index 0000000..5981136 --- /dev/null +++ b/pyaoscx/exceptions/verification_error.py @@ -0,0 +1,24 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.pyaoscx_error import PyaoscxError + + +class VerificationError(PyaoscxError): + """ + Exception class for a Verification Error inside PYAOSCX + """ + + def __init__(self, *args): + if args: + super().__init__(args[1]) + self.module = args[0] + else: + self.message = None + + def __str__(self): + if self.message: + return 'VERIFICATION ERROR: {0} DETAIL: {1}'.format( + self.module, self.message) + else: + return 'VERIFICATION ERROR' diff --git a/pyaoscx/firmware.py b/pyaoscx/firmware.py new file mode 100644 index 0000000..db001d9 --- /dev/null +++ b/pyaoscx/firmware.py @@ -0,0 +1,33 @@ +# (C) Copyright 2020 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +import pyaoscx.utils.util as utils +import logging + + +def get_firmware_version(**kwargs): + """ + Perform GET calls to retrieve the current firmware version. + + :param kwargs: + keyword s: requests.session object with loaded cookie jar + keyword url: URL in main() function + :return: Firmware version string if found, otherwise None + """ + + target_url = kwargs["url"] + "firmware" + + response = kwargs["s"].get( + target_url, + verify=False, + proxies=kwargs["s"].proxies) + + if not utils._response_ok(response, "GET"): + logging.warning("FAIL: Getting firmware version %d: %s" + % (response.status_code, response.text)) + firmware_version = None + else: + logging.info("SUCCESS: Getting firmware version succeeded") + firmware_version = response.json()["current_version"] + + return firmware_version diff --git a/pyaoscx/interface.py b/pyaoscx/interface.py index 92025f7..110b817 100644 --- a/pyaoscx/interface.py +++ b/pyaoscx/interface.py @@ -1,1500 +1,2066 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. + +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import common_ops, port +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError -import json -import random -import logging +from pyaoscx.ipv6 import Ipv6 +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.vlan import Vlan +from pyaoscx.vrf import Vrf +from pyaoscx.utils.connection import connected -def get_interface(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for an Interface table entry - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data for Interface entry - """ - if kwargs["url"].endswith("/v1/"): - return _get_interface_v1(int_name, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_interface(int_name, depth, selector, **kwargs) - - -def _get_interface_v1(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for an Interface table entry - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data for Interface entry - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - target_url = kwargs["url"] + "system/interfaces/%s?" % int_name_percents - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - - result = [] - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting Interface table entry '%s' failed with status code %d: %s" - % (int_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting Interface table entry '%s' succeeded" % int_name) - result = response.json() - return result - - -def _get_interface(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for an Interface table entry - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data for Interface entry - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting Interface table entry '%s' failed with status code %d: %s" - % (int_name, response.status_code, response.text)) - result = {} - else: - logging.info("SUCCESS: Getting Interface table entry '%s' succeeded" % int_name) - result = response.json() - - return result - - -def get_all_interfaces(**kwargs): - """ - Perform a GET call to get a list (or dictionary) of all entries in the Interface table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dict of all Interfaces in the table - """ - target_url = kwargs["url"] + "system/interfaces" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dict of all Interface table entries failed with status code %d: %s" - % (response.status_code, response.text)) - interface_list = [] - else: - logging.info("SUCCESS: Getting list/dict of all Interface table entries succeeded") - interface_list = response.json() - - return interface_list - - -def get_all_interface_names(**kwargs): - """ - Perform a GET call to get a list of all of the names for each interface in the Interface table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all Interface names in the table - """ - target_url = kwargs["url"] + "system/interfaces" - - response = kwargs["s"].get(target_url, verify=False) - interface_list = [] - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of Interface names failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of Interface names succeeded") - uri_list = response.json() - if kwargs["url"].endswith("/v1/"): - for interface_uri in uri_list: - interface_name = interface_uri[(interface_uri.rfind('/')+1):] # Takes string after last '/' - if interface_name != "bridge_normal": # Ignore bridge_normal interface - interface_list.append(common_ops._replace_percents(interface_name)) - else: # Updated else for when version is v10.04 - for interface_key in uri_list: - interface_list.append(interface_key) - return interface_list - - -def get_ipv6_addresses(int_name, depth=0, **kwargs): - """ - Perform a GET call to retrieve the list of IPv6 addresses for an Interface table entry - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all ipv6 addresses for the Interface entry - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/ports/%s/ip6_addresses" % int_name_percents - logport = "Port" - else: # Updated else for when version is v10.04 - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses" % int_name_percents - logport = "Interface" - - payload = { - "depth": depth - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting IPv6 list for %s table entry '%s' failed with status code %d: %s" - % (logport, int_name, response.status_code, response.text)) - result = [] - else: - logging.info("SUCCESS: Getting IPv6 list for %s table entry '%s' succeeded" % (logport, int_name)) - result = response.json() - - return result - - -def add_vlan_interface(vlan_int_name, vlan_port_name, vlan_id, ipv4, vrf_name, vlan_port_desc, int_type="vlan", - user_config=None, **kwargs): - """ - Perform a POST call to add Interface table entry for a VLAN. - - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_port_name: Alphanumeric Port name to associate with the interface - :param vlan_id: Numeric ID of VLAN - :param ipv4: Optional IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param vrf_name: VRF to attach the SVI to. Defaults to "default" if not specified - :param vlan_port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param int_type: Type of interface; generally should be "vlan" for SVI's. - As such, defaults to "internal" if not specified. - :param user_config: User configuration to apply to interface. Defaults to {"admin": "up"} if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _add_vlan_interface_v1(vlan_int_name, vlan_port_name, int_type, user_config, **kwargs) - else: # Updated else for when version is v10.04 - return _add_vlan_interface(vlan_int_name, vlan_id, ipv4, vrf_name, vlan_port_desc, int_type, user_config, **kwargs) - - -def _add_vlan_interface_v1(vlan_int_name, vlan_port_name, int_type="vlan", user_config=None, **kwargs): - """ - Perform a POST call to add Interface table entry for a VLAN. - - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_port_name: Alphanumeric Port name to associate with the interface - :param int_type: Type of interface; generally should be "vlan" for SVI's. - As such, defaults to "internal" if not specified. - :param user_config: User configuration to apply to interface. Defaults to {"admin": "up"} if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_list = get_all_interfaces(**kwargs) - - if "/rest/v1/system/interfaces/%s" % vlan_int_name not in ints_list: - if user_config is None: - # optional argument can't default to a dictionary type, - # so make it None and change it to the dictionary {"admin": "up"} if it was None - user_config = {"admin": "up"} - - vlan_int_data = {"name": vlan_int_name, - "referenced_by": "/rest/v1/system/ports/%s" % vlan_port_name, - "type": int_type, # API says: "vlan: generally represents SVI - L3 VLAN interfaces." - "user_config": user_config - } - - target_url = kwargs["url"] + "system/interfaces" - - post_data = json.dumps(vlan_int_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' for SVI failed with status code %d: %s" - % (vlan_int_name, response.status_code, response.text)) - return False +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class Interface(PyaoscxModule): + ''' + Provide configuration management for Interface on AOS-CX devices. + ''' + + base_uri = "system/interfaces" + indices = ['name'] + resource_uri_name = 'interfaces' + + ip6_addresses = ListDescriptor('ip6_addresses') + + def __init__(self, session, name, uri=None, ip6_addresses=[], **kwargs): + self.session = session + self._uri = uri + + # List used to determine attributes related to the configuration + self.config_attrs = [] + self.materialized = False + + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + + # Set name, percents name and determine if Interface is a LAG + self.name = "" + self.__set_name(name) + + # List of previous interfaces before update + # used to verify if an interface is deleted from lag + self.__prev_interfaces = [] + + # Set ip6 addresses + self.ip6_addresses = ip6_addresses + + # Type required for configuration + self.type = None + # Set type + self.__set_type() + + # Check if data should be added to object + if self.__is_special_type: + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_name(self, name): + ''' + Set name attribute in the proper form for Interface object + Also sets the "percents name"-the name with any special characters + replaced with percent-encodings + :param name: Interface name + ''' + + # Add attributes to class + self.name = None + self.percents_name = None + + if r'%2F' in name or r'%2C' in name or r'%3A' in name: + self.name = utils._replace_percents(name) + self.percents_name = name else: - logging.info("SUCCESS: Adding Interface table entry '%s' for SVI succeeded" % vlan_int_name) - return True - else: - logging.info("SUCCESS: No need to create Interface table entry '%s' for SVI since it already exists" - % vlan_int_name) - return True - - -def _add_vlan_interface(vlan_int_name, vlan_id=None, ipv4=None, vrf_name="default", vlan_port_desc=None, - int_type="vlan", user_config=None, **kwargs): - """ - Perform a POST call to add Interface table entry for a VLAN. - - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_port_name: Alphanumeric Port name to associate with the interface - :param int_type: Type of interface; generally should be "vlan" for SVI's. - As such, defaults to "internal" if not specified. - :param user_config: User configuration to apply to interface. Defaults to {"admin": "up"} if not speicifed. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_dict = get_all_interfaces(**kwargs) - - if vlan_int_name not in ints_dict: - if user_config is None: - # optional argument can't default to a dictionary type, - # so make it None and change it to the dictionary {"admin": "up"} if it was None - user_config = {"admin": "up"} - - vlan_int_data = {"name": vlan_int_name, - "type": int_type, # API says: "vlan: generally represents SVI - L3 VLAN interfaces." - "user_config": user_config, - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf_name, - "vlan_tag": "/rest/v10.04/system/vlans/%s" % vlan_id - } - - if vlan_port_desc is not None: - vlan_int_data['description'] = vlan_port_desc - - if ipv4 is not None: - vlan_int_data['ip4_address'] = ipv4 - - target_url = kwargs["url"] + "system/interfaces" - - post_data = json.dumps(vlan_int_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' for SVI failed with status code %d: %s" - % (vlan_int_name, response.status_code, response.text)) - return False + self.name = name + self.percents_name = utils._replace_special_characters(self.name) + + def __set_type(self): + ''' + Set Interface type when creating an Interface Object + ''' + # Define all patterns + lag_pattern = re.compile(r'lag[0-9]+$') + loopback_pattern = re.compile(r'loopback[0-9]+$') + tunnel_pattern = re.compile(r'tunnel(.*)') + vlan_pattern = re.compile(r'vlan[0-9]+$') + vxlan_pattern = re.compile(r'vxlan(.*)') + + # Sets interface as a special type + self.__is_special_type = True + + if lag_pattern.match(self.name): + self.type = 'lag' + elif loopback_pattern.match(self.name): + self.type = 'loopback' + elif tunnel_pattern.match(self.name): + self.type = 'tunnel' + elif vlan_pattern.match(self.name): + self.type = 'vlan' + elif vxlan_pattern.match(self.name): + self.type = 'vxlan' else: - logging.info("SUCCESS: Adding Interface table entry '%s' for SVI succeeded" % vlan_int_name) - return True - else: - logging.info("SUCCESS: No need to create Interface table entry '%s' for SVI since it already exists" - % vlan_int_name) - return True - - -def add_l2_interface(interface_name, interface_desc=None, interface_admin_state="up", **kwargs): - """ - Perform a POST call to create an Interface table entry for physical L2 interface. - - :param interface_name: Alphanumeric Interface name - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port.add_l2_port(interface_name, interface_desc, interface_admin_state, **kwargs) - else: # Updated else for when version is v10.04 - return _add_l2_interface(interface_name, interface_desc, interface_admin_state, **kwargs) - - -def _add_l2_interface(interface_name, interface_desc=None, interface_admin_state="up", **kwargs): - """ - Perform a PUT call to create an Interface table entry for physical L2 interface. - :param interface_name: Alphanumeric Interface name - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = { - "admin": "up", - "description": interface_desc, - "routing": False, - "user_config": { - "admin": interface_admin_state - }, - } - - target_url = kwargs["url"] + "system/interfaces/" + interface_name_percents - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Interface table entry '%s' succeeded" % interface_name) - return True - - -def add_l3_ipv4_interface(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT or POST call to create an Interface table entry for a physical L3 Interface. If the Interface already - exists, the function will enable routing on the Interface and update the IPv4 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _add_l3_ipv4_interface_v1(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - else: # Updated else for when version is v10.04 - return _add_l3_ipv4_interface(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - - -def _add_l3_ipv4_interface_v1(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT or POST call to create an Interface table entry for a physical L3 Interface. If the Interface already - exists, the function will enable routing on the Interface and update the IPv4 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - return port.add_l3_ipv4_port(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - - -def _add_l3_ipv4_interface(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT call to update an Interface table entry for a physical L3 Interface. If the Interface already - exists, the function will enable routing on the Interface and update the IPv4 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = { - "admin": interface_admin_state, - "interfaces": ["/rest/v10.04/system/interfaces/%s" % interface_name_percents], - "routing": True, - "ip4_address": ip_address, - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf - } - - if interface_desc is not None: - interface_data['description'] = interface_desc - - target_url = kwargs["url"] + "system/interfaces/" + interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring Interface table entry '%s' succeeded" % interface_name) - return True - - -def add_l3_ipv6_interface(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT or POST call to create an Interface table entry for a physical L3 Interface. If the Interface already - exists, the function will enable routing on the Interface and update the IPv6 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv6 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _add_l3_ipv6_interface_v1(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - else: # Updated else for when version is v10.04 - return _add_l3_ipv6_interface(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - - -def _add_l3_ipv6_interface_v1(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT or POST call to create an Interface table entry for a physical L3 Interface. If the Interface already - exists, the function will enable routing on the Interface and update the IPv6 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv6 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - return port.add_l3_ipv6_port(interface_name, ip_address, interface_desc, interface_admin_state, vrf, **kwargs) - - -def _add_l3_ipv6_interface(interface_name, ip_address=None, interface_desc=None, interface_admin_state="up", - vrf="default", **kwargs): - """ - Perform a PUT call to update an Interface table entry for a physical L3 Interface, then a POST call to add an IPv6 - mapping. If the Interface already exists, the function will enable routing on the Interface and update the - IPv6 address if given. - - :param interface_name: Alphanumeric Interface name - :param ip_address: IPv6 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param interface_admin_state: Optional administratively-configured state of the interface. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = { - "admin": interface_admin_state, - "interfaces": ["/rest/v10.04/system/interfaces/%s" % interface_name_percents], - "routing": True, - "vrf": {vrf: "/rest/v10.04/system/vrfs/%s" % vrf} - } - - if interface_desc is not None: - interface_data['description'] = interface_desc - - target_url = kwargs["url"] + "system/interfaces/" + interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Initial configuration of L3 IPv6 Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Initial configuration of L3 IPv6 Interface table entry '%s' succeeded" % interface_name) - # IPv6 defaults - ipv6_data = { - "address": ip_address, - "node_address": True, - "origin": "configuration", - "ra_prefix": True, - "route_tag": 0, - "type": "global-unicast" + self.__is_special_type = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a Interface table entry + + :param depth: Integer deciding how many levels into the API JSON + that references will be returned. + :param selector: Alphanumeric option to select specific + information to return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving Interface") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector } - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses" % interface_name_percents - post_data = json.dumps(ipv6_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Final configuration of L3 IPv6 Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Final configuration of L3 IPv6 Interface table entry '%s' succeeded" % interface_name) - return True - - -def delete_ipv6_address(interface_name, ip, **kwargs): - """ - Perform a DELETE call to remove an IPv6 address from an Interface. - - :param interface_name: Alphanumeric Interface name - :param ip: IPv6 address assigned to the interface that will be removed. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._delete_ipv6_address(interface_name, ip, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_ipv6_address(interface_name, ip, **kwargs) - - -def _delete_ipv6_address(interface_name, ip, **kwargs): - """ - Perform a DELETE call to remove an IPv6 address from an Interface. - - :param interface_name: Alphanumeric Interface name - :param ip: IPv6 address assigned to the interface that will be removed. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if ip in get_ipv6_addresses(interface_name, **kwargs): - interface_name_percents = common_ops._replace_special_characters(interface_name) - ip_address = common_ops._replace_special_characters(ip) - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses/%s" % (interface_name_percents, ip_address) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting IPv6 Address '%s' from Interface table entry '%s' failed with status code %d: %s" - % (ip, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting IPv6 Address '%s' from Interface table entry '%s' succeeded" - % (ip, interface_name)) - return True - else: - logging.info("SUCCESS: No need to delete IPv6 Address '%s' from Interface table entry '%s' since it does not exist" - % (ip, interface_name)) - return True - - -def create_loopback_interface(interface_name, vrf="default", ipv4=None, interface_desc=None, **kwargs): - """ - Perform a PUT and/or POST call to create a Loopback Interface table entry for a logical L3 Interface. If the - Loopback Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param interface_name: Alphanumeric Interface name - :param vrf: VRF to attach the SVI to. Defaults to "default" if not specified - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_loopback_interface_v1(interface_name, vrf, ipv4, interface_desc, **kwargs) - else: # Updated else for when version is v10.04 - return _create_loopback_interface(interface_name, vrf, ipv4, interface_desc, **kwargs) - - -def _create_loopback_interface_v1(interface_name, vrf, ipv4=None, interface_desc=None, **kwargs): - """ - Perform a PUT and/or POST call to create a Loopback Interface table entry for a logical L3 Interface. If the - Loopback Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param interface_name: Alphanumeric Interface name - :param vrf: VRF to attach the SVI to. Defaults to "default" if not specified - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port.create_loopback_port(interface_name, vrf, ipv4, interface_desc, **kwargs) - - -def _create_loopback_interface(interface_name, vrf, ipv4=None, interface_desc=None, **kwargs): - """ - Perform a POST call to create a Loopback Interface table entry for a logical L3 Interface. If the - Loopback Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param interface_name: Alphanumeric Interface name - :param vrf: VRF to attach the SVI to. Defaults to "default" if not specified - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param interface_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = { - "name": interface_name, - "type": "loopback", - "user_config": { - "admin": "up" - }, - "ospf_if_type": "ospf_iftype_loopback", - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf - } - - if ipv4 is not None: - interface_data['ip4_address'] = ipv4 - - if interface_desc is not None: - interface_data['description'] = interface_desc - - target_url = kwargs["url"] + "system/interfaces" - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Configuring Interface table entry '%s' succeeded" % interface_name) + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri, + name=self.percents_name + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy + ) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the module is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['name', 'type'] + ) + + # Set original attributes + self.__original_attributes = data + + # Set a list of interfaces as an attribute + if hasattr(self, 'interfaces') and self.interfaces is not None: + interfaces_list = [] + # Get all URI elements in the form of a list + uri_list = self.session.api_version.get_uri_from_data( + self.interfaces) + + for uri in uri_list: + # Create an Interface object + name, interface = Interface.from_uri(self.session, uri) + + # Check for circular reference + # No need to get() if it's circular; it is already + # materialized. Just set flag + if name == self.name: + interface.materialized = True + else: + # Materialize interface + interface.get() + + # Add interface to list + interfaces_list.append(interface) + + # Set list as Interfaces + self.interfaces = interfaces_list + # Set list of previous Interfaces + self.__prev_interfaces = list(self.interfaces) + + # Set VRF + if hasattr(self, 'vrf') and self.vrf is not None: + # Set VRF as a Vrf object + vrf_obj = Vrf.from_response(self.session, self.vrf) + self.vrf = vrf_obj + # Materialized VRF + self.vrf.get() + + # Set VLAN + if hasattr(self, 'vlan_tag') and self.vlan_tag is not None: + # Set vlan_tag as a Vlan object + vlan_obj = Vlan.from_response(self.session, self.vlan_tag) + self.vlan_tag = vlan_obj + # Materialized Vlan + self.vlan_tag.get() + + # vlan_trunks + # Set a list of VLANs as an attribute + if hasattr(self, 'vlan_trunks') and self.vlan_trunks is not None: + vlan_trunks = [] + # Get all URI elements in the form of a list + uri_list = self.session.api_version.get_uri_from_data( + self.vlan_trunks) + + for uri in uri_list: + # Create a Vlan object + vlan_id, vlan = Vlan.from_uri(self.session, uri) + # Materialize VLAN + vlan.get() + # Add VLAN to dictionary + vlan_trunks.append(vlan) + # Set list as VLANs + self.vlan_trunks = vlan_trunks + + # Set all ACLs + from pyaoscx.acl import ACL + if hasattr(self, 'aclmac_in_cfg') and self.aclmac_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclmac_in_cfg) + # Materialize Acl object + acl.get() + self.aclmac_in_cfg = acl + + if hasattr(self, 'aclmac_out_cfg') and self.aclmac_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclmac_out_cfg) + # Materialize Acl object + acl.get() + self.aclmac_out_cfg = acl + + if hasattr(self, 'aclv4_in_cfg') and self.aclv4_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_in_cfg) + # Materialize Acl object + acl.get() + self.aclv4_in_cfg = acl + + if hasattr(self, 'aclv4_out_cfg') and self.aclv4_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_out_cfg) + # Materialize Acl object + acl.get() + self.aclv4_out_cfg = acl + + if hasattr( + self, + 'aclv4_routed_in_cfg') and self.aclv4_routed_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_routed_in_cfg) + # Materialize Acl object + acl.get() + self.aclv4_routed_in_cfg = acl + + if hasattr( + self, + 'aclv4_routed_out_cfg') and self.aclv4_routed_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_routed_out_cfg) + # Materialize Acl object + acl.get() + self.aclv4_routed_out_cfg = acl + + if hasattr(self, 'aclv6_in_cfg') and self.aclv6_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_in_cfg) + # Materialize Acl object + acl.get() + self.aclv6_in_cfg = acl + + if hasattr(self, 'aclv6_out_cfg') and self.aclv6_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_out_cfg) + # Materialize Acl object + acl.get() + self.aclv6_out_cfg = acl + + if hasattr( + self, + 'aclv6_routed_in_cfg') and self.aclv6_routed_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_routed_in_cfg) + # Materialize Acl object + acl.get() + self.aclv6_routed_in_cfg = acl + + if hasattr( + self, + 'aclv6_routed_out_cfg') and self.aclv6_routed_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_routed_out_cfg) + # Materialize Acl object + acl.get() + self.aclv6_routed_out_cfg = acl + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + if self.ip6_addresses == []: + # Set IPv6 addresses if any + # Loads IPv6 objects already into the Interface + Ipv6.get_all(self.session, self) return True + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system Interfaces and create + a dictionary containing each Interface as a Interface Object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing Interface's name as key and a Interface + objects as values + ''' + + logging.info("Retrieving the switch Interfaces") + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=Interface.base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + interfaces_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create an Interface object + name, interface = Interface.from_uri(session, uri) + + interfaces_dict[name] = interface + + return interfaces_dict + + @classmethod + def from_response(cls, session, response_data): + ''' + Create an Interface object given a response_data related to the + Interface object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + dictionary: { + 1: "/rest/v10.04/system/interfaces/1" + } + or a + string: "/rest/v1/system/interfaces/1" + :return: Interface object + + ''' + interfaces_id_arr = session.api_version.get_keys( + response_data, Interface.resource_uri_name) + interface_name = interfaces_id_arr[0] + return session.api_version.get_module( + session, 'Interface', + interface_name) + + @classmethod + def from_uri(cls, session, uri): + ''' + Create an Interface object given a interface URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return name, interface_obj: tuple containing both the Interface's name + and an Interface object + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)/(?P.+)') + name_percents = index_pattern.match(uri).group('index') + name = utils._replace_percents(name_percents) + # Create Interface object + interface_obj = session.api_version.get_module( + session, 'Interface', + name, uri=uri) + + return name, interface_obj + + + @classmethod + def get_facts(cls, session): + ''' + Perform a GET call to retrieve all Interfaces and their respective data + :param cls: Class reference. + :param session: pyaoscx.Session object used to represent a logical + connection to the device. + :return facts: Dictionary containing Interface IDs as keys and Interface + objects as values + ''' + # Log + logging.info("Retrieving the switch interfaces facts") + + # Set depth + interface_depth = session.api_version.default_facts_depth + + # Build URI + uri =\ + '{base_url}{class_uri}?depth={depth}'.format( + base_url=session.base_url, + class_uri=Interface.base_uri, + depth=interface_depth + ) + + try: + # Try to get facts via GET method + response = session.s.get( + uri, + verify=False, + proxies=session.proxy + ) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Load into json format + facts = json.loads(response.text) + + return facts + + @connected + def create(self): + """ + Perform a POST call to create a Port table entry + and a Interface table entry for Interface Object. + Only returns if an exception is not raise + + :return True if entry was created inside Device + """ + + interface_data = {} + + interface_data = utils.get_attrs(self, self.config_attrs) + + interface_data['name'] = self.name + # Set Type + if self.type is not None: + interface_data['type'] = self.type + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri + ) -def _create_vxlan_interface(interface_name, source_ipv4=None, port_desc=None, dest_udp_port=4789, **kwargs): - """ - Perform POST call to create a VXLAN table entry for a logical L3 Interface. If the - VXLAN Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param interface_name: Alphanumeric Interface name - :param source_ipv4: Optional source IPv4 address to assign to the VXLAN interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param dest_udp_port: Optional Destination UDP Port that the VXLAN will use. Default is set to 4789 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interfaces_dict = get_all_interfaces(**kwargs) - - if interface_name not in interfaces_dict: - interface_data = { - "name": interface_name, - "options": { - "local_ip": source_ipv4, - "vxlan_dest_udp_port": str(dest_udp_port) - }, - "type": "vxlan", - "user_config": { - "admin": "up" - }, - - "admin": "up", - "routing": False - } - - if port_desc is not None: - interface_data['description'] = port_desc - - interface_url = kwargs["url"] + "system/interfaces" post_data = json.dumps(interface_data, sort_keys=True, indent=4) - response = kwargs["s"].post(interface_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding VXLAN Interface table entry '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VXLAN Interface table entry '%s' succeeded" % interface_name) - return True - else: - return update_interface_ipv4(interface_name, source_ipv4, **kwargs) - - -def update_interface_ipv4(interface_name, ipv4, interface_admin_state, vrf, **kwargs): - """ - Perform GET and PUT calls to update an L3 interface's ipv4 address - - :param interface_name: Alphanumeric name of the Port - :param ipv4: IPv4 address to associate with the VLAN Port - :param interface_admin_state: Administratively-configured state of the port. - :param vrf: Name of the VRF to which the Port belongs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - interface_data = get_interface(interface_name, depth=1, selector="writable", **kwargs) - - interface_data['ip4_address'] = ipv4 - interface_data['routing'] = True - interface_data['admin'] = interface_admin_state - interface_data['vrf'] = "/rest/v10.04/system/vrfs/%s" % vrf - - target_url = kwargs["url"] + "system/interfaces/%s" % interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating IPv4 addresses for Port '%s' to '%s' failed with status code %d: %s" - % (interface_name, repr(ipv4), response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating IPv4 addresses for Port '%s' to '%s' succeeded" - % (interface_name, repr(ipv4))) - return True - - -def update_port_ipv6(interface_name, ipv6, addr_type="global-unicast", **kwargs): - """ - Perform a POST call to update an L3 interface's ipv6 address + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) - :param interface_name: Alphanumeric name of the Port - :param ipv6: IPv6 address to associate with the VLAN Port - :param addr_type: Type of IPv6 address. Defaults to "global-unicast" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ + except Exception as e: + raise ResponseError('POST', e) - ipv6_data = {"address": ipv6, - "type": addr_type} + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses" % interface_name - post_data = json.dumps(ipv6_data, sort_keys=True, indent=4) + else: + logging.info("SUCCESS: Adding {} table entry succeeded".format( + self.name)) - response = kwargs["s"].post(target_url, data=post_data, verify=False) + # Get all objects data + self.get() - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Updating IPv6 address for Port '%s' to '%s' failed with status code %d: %s" - % (interface_name, ipv6, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating IPv6 address for Port '%s' to '%s' succeeded" - % (interface_name, ipv6)) return True - -def enable_disable_interface(int_name, state="up", **kwargs): - """ - Perform GET and PUT calls to either enable or disable the interface by setting Interface's admin_state to - "up" or "down" - - :param int_name: Alphanumeric name of the interface - :param state: State to set the interface to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _enable_disable_interface_v1(int_name, state, **kwargs) - else: # Updated else for when version is v10.04 - return _enable_disable_interface(int_name, state, **kwargs) - - -def _enable_disable_interface_v1(int_name, state="up", **kwargs): - """ - Perform GET and PUT calls to either enable or disable the interface by setting Interface's admin_state to - "up" or "down" - - :param int_name: Alphanumeric name of the interface - :param state: State to set the interface to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if state not in ['up', 'down']: - raise Exception("Administratively-configured state of interface should be 'up' or 'down'") - - int_name_percents = common_ops._replace_special_characters(int_name) - - interface_list = get_all_interfaces(**kwargs) - - if "/rest/v1/system/interfaces/%s" % int_name_percents in interface_list: - int_data = get_interface(int_name, 0, "configuration", **kwargs) - int_data['user_config'] = {"admin": state} - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating Interface '%s' with admin-configured state '%s' " - "failed with status code %d: %s" % (int_name, state, response.status_code, response.text)) - success = False + @connected + def apply(self): + ''' + Main method used to update or create a Interface or Port table entry. + Checks whether the Interface exists in the switch + Calls self.update() if Interface is being updated + Calls self.create() if a Interface table entry is being created + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + modified = False + if self.materialized: + modified = self.update() else: - logging.info("SUCCESS: Updating Interface '%s' with admin-configured state '%s' " - "succeeded" % (int_name, state)) - success = True - port._enable_disable_port(int_name, state, **kwargs) - return success - else: - logging.warning("FAIL: Unable to update Interface '%s' because operation could not find interface" % int_name) - return False - - -def _enable_disable_interface(int_name, state="up", **kwargs): - """ - Perform GET and PUT calls to either enable or disable the interface by setting Interface's admin_state to - "up" or "down" - - :param int_name: Alphanumeric name of the interface - :param state: State to set the interface to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if state not in ['up', 'down']: - raise Exception("Administratively-configured state of interface should be 'up' or 'down'") - - int_name_percents = common_ops._replace_special_characters(int_name) - - int_data = get_interface(int_name, 1, "writable", **kwargs) - int_data['user_config'] = {"admin": state} - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating Interface '%s' with admin-configured state '%s' " - "failed with status code %d: %s" % (int_name, state, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating Interface '%s' with admin-configured state '%s' " - "succeeded" % (int_name, state)) - return True - - -def delete_interface(interface_name, **kwargs): - """ - Perform a DELETE call to either the Interface Table or Port Table to delete an interface - - :param interface_name: Name of interface's reference entry in Interface table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_interface_v1(interface_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_interface(interface_name, **kwargs) - - -def _delete_interface_v1(interface_name, **kwargs): - """ - Perform DELETE call to Port Table to delete an interface - - Note: Interface API does not have delete methods. - To delete an Interface, you remove its reference port. - - :param name: Name of interface's reference entry in Port table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - return port.delete_port(interface_name, **kwargs) - - -def _delete_interface(name, **kwargs): - """ - Perform DELETE call to Interface table to delete an interface - :param name: Name of interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_dict = get_all_interfaces(**kwargs) - - if name in ints_dict: - - target_url = kwargs["url"] + "system/interfaces/%s" % name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting Interface table entry '%s' failed with status code %d: %s" - % (name, response.status_code, response.text)) - return False + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def delete(self): + ''' + Perform DELETE call to delete Interface table entry. + + ''' + if not self.__is_special_type: + self.initialize_interface_entry() + else: + # Delete Interface via a DELETE REQUEST + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri, + id=self.name + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError( + response.text, response.status_code) + + # Clean LAG from interfaces + # Delete interface references + for interface in self.__prev_interfaces: + # If interface name is not the same as the current one + if interface.name != self.name and self.type == 'lag': + interface.__delete_lag(self) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing Interface or Port + table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + interface_data = {} + # Get interface PUT data depending on the configuration attributes + # list + interface_data = utils.get_attrs(self, self.config_attrs) + + # Check if VRF is inside the data related to interface + if hasattr(self, 'vrf') and self.vrf is not None: + # Set VRF in the correct format for PUT + interface_data['vrf'] = self.vrf.get_info_format() + + # Check if vlan_tag is inside the data related to interface + if hasattr(self, 'vlan_tag') and self.vlan_tag is not None: + # Set VLAN in the correct format for PUT + interface_data["vlan_tag"] = self.vlan_tag.get_info_format() + + # Set interfaces into correct form + if hasattr(self, 'interfaces') and self.interfaces is not None: + formatted_interfaces = {} + + # Check for interfaces no longer in LAG + if self.__is_special_type and self.type == 'lag': + for element in self.__prev_interfaces: + # If element was deleted from interfaces + if element not in self.interfaces: + # Delete element reference to current LAG + element.__delete_lag(self) + + # Set prev interfaces with current ones + # Copies interfaces + self.__prev_interfaces = list(self.interfaces) + + # Set interfaces into correct form + for element in self.interfaces: + # If element is the same as current, ignore + if element.name == self.name and self.type == 'lag': + pass + else: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Interface {}'.format(element.name), + 'Object inside interfaces not materialized') + formated_element = element.get_info_format() + formatted_interfaces.update(formated_element) + + if self.type == 'lag': + # New element being added to LAG + element.__add_member_to_lag(self) + + # Set values in correct form + interface_data["interfaces"] = formatted_interfaces + + # Set VLANs into correct form + if "vlan_trunks" in interface_data: + formated_vlans = {} + # Set VLANs into correct form + for element in self.vlan_trunks: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Vlan {}'.format(element), + 'Object inside vlan trunks not materialized') + formated_element = element.get_info_format() + formated_vlans.update(formated_element) + + # Set values in correct form + interface_data["vlan_trunks"] = formated_vlans + + # Set all ACLs + if "aclmac_in_cfg" in interface_data and self.aclmac_in_cfg is not None: + # Set values in correct form + interface_data["aclmac_in_cfg"] = self.aclmac_in_cfg.get_info_format() + + if "aclmac_out_cfg" in interface_data and self.aclmac_out_cfg is not None: + # Set values in correct form + interface_data["aclmac_out_cfg"] = self.aclmac_out_cfg.get_info_format() + + if "aclv4_in_cfg" in interface_data and self.aclv4_in_cfg is not None: + # Set values in correct form + interface_data["aclv4_in_cfg"] = self.aclv4_in_cfg.get_info_format() + + if "aclv4_out_cfg" in interface_data and self.aclv4_out_cfg is not None: + # Set values in correct form + interface_data["aclv4_out_cfg"] = self.aclv4_out_cfg.get_info_format() + + if "aclv4_routed_in_cfg" in interface_data and self.aclv4_routed_in_cfg is not None: + # Set values in correct form + interface_data["aclv4_routed_in_cfg"] = self.aclv4_routed_in_cfg.get_info_format( + ) + + if "aclv4_routed_out_cfg" in interface_data and self.aclv4_routed_out_cfg is not None: + # Set values in correct form + interface_data["aclv4_routed_out_cfg"] = self.aclv4_routed_out_cfg.get_info_format( + ) + + if "aclv6_in_cfg" in interface_data and self.aclv6_in_cfg is not None: + # Set values in correct form + interface_data["aclv6_in_cfg"] = self.aclv6_in_cfg.get_info_format() + + if "aclv6_out_cfg" in interface_data and self.aclv6_out_cfg is not None: + # Set values in correct form + interface_data["aclv6_out_cfg"] = self.aclv6_out_cfg.get_info_format() + + if "aclv6_routed_in_cfg" in interface_data and self.aclv6_routed_in_cfg is not None: + # Set values in correct form + interface_data["aclv6_routed_in_cfg"] = self.aclv6_routed_in_cfg.get_info_format( + ) + + if "aclv6_routed_out_cfg" in interface_data and self.aclv6_routed_out_cfg is not None: + # Set values in correct form + interface_data["aclv6_routed_out_cfg"] = self.aclv6_routed_out_cfg.get_info_format( + ) + + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri, + name=self.percents_name + ) + + # Compare dictionaries + if interface_data == self.__original_attributes: + # Object was not modified + modified = False else: - logging.info("SUCCESS: Deleting Interface table entry '%s' succeeded" % name) - return True - else: - logging.info("SUCCESS: No need to delete Interface table entry '%s' because it doesn't exist" - % name) - return True - - -def delete_l2_interface(interface_name, **kwargs): - """ - Perform either a PUT call to the Interface Table or DELETE call to Port Table to delete an interface - If trying to re-initialize an L2 interface, use the function initialize_l2_interface() - - :param interface_name: Name of interface's reference entry in Interface table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_l2_interface_v1(interface_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_l2_interface(interface_name, **kwargs) - - -def _delete_l2_interface_v1(interface_name, **kwargs): - """ - Perform DELETE call to Port Table to delete an L2 interface - - Note: Interface API does not have delete methods. - To delete an Interface, you remove its reference port. - - :param name: Name of interface's reference entry in Port table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - return port.delete_port(interface_name, **kwargs) - - -def _delete_l2_interface(interface_name, **kwargs): - """ - Perform a PUT call to the Interface Table to reset an interface to it's default values - - :param interface_name: Name of interface's reference entry in Interface table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - target_url = kwargs["url"] + "system/interfaces/%s" % interface_name_percents - - interface_data = {} - interface_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=interface_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Deleting Interface '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting Interface '%s' succeeded" % interface_name) - return True - - -def _port_set_vlan_mode(l2_port_name, vlan_mode, **kwargs): - """ - Perform GET and PUT calls to set an L2 interface's VLAN mode (native-tagged, native-untagged, or access) - - :param l2_port_name: L2 interface's Interface table entry name - :param vlan_mode: A string, either 'native-tagged', 'native-untagged', or 'access', specifying the desired VLAN - mode - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if vlan_mode not in ['native-tagged', 'native-untagged', 'access']: - raise Exception("ERROR: VLAN mode should be 'native-tagged', 'native-untagged', or 'access'") - - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - int_data = get_interface(l2_port_name_percents, depth=1, selector="writable", **kwargs) - - int_data['vlan_mode'] = vlan_mode - int_data['routing'] = False - - target_url = kwargs["url"] + "system/interfaces/%s" % l2_port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting port '%s' VLAN mode to '%s' failed with status code %d: %s" - % (l2_port_name, vlan_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting port '%s' VLAN mode to '%s' succeeded" % (l2_port_name, vlan_mode)) - return True - - -def _port_set_untagged_vlan(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN on an access port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to set on access port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - int_data = get_interface(l2_port_name_percents, depth=1, selector="writable", **kwargs) - - int_data['vlan_mode'] = "access" - int_data['vlan_tag'] = "/rest/v10.04/system/vlans/%s" % vlan_id - int_data['routing'] = False - - target_url = kwargs["url"] + "system/interfaces/%s" % l2_port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting Port '%s' access VLAN to VLAN ID '%d' failed with status code %d: %s" - % (l2_port_name, vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting Port '%s' access VLAN to VLAN ID '%d' succeeded" - % (l2_port_name, vlan_id)) - return True - - -def _port_add_vlan_trunks(l2_port_name, vlan_trunk_ids={}, **kwargs): - """ - Perform GET and PUT/POST calls to add specified VLANs to a trunk port. By default, this will also set the port to - have 'no routing' and if there is not a native VLAN, will set the native VLAN to VLAN 1. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_trunk_ids: Dictionary of VLANs to specify as allowed on the trunk port. If empty, the interface will - allow all VLANs on the trunk. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - trunk_list = {} - for x in vlan_trunk_ids: - x_keys = {str(x): "/rest/v10.04/system/vlans/%d" % x} - trunk_list.update(x_keys) - - port_data = get_interface(l2_port_name, depth=1, selector="writable", **kwargs) - - if not port_data['vlan_tag']: - port_data['vlan_tag'] = "/rest/v10.04/system/vlans/1" - else: - # Convert the dictionary to a URI string - port_data['vlan_tag'] = common_ops._dictionary_to_string(port_data['vlan_tag']) - - if not port_data['vlan_mode']: - port_data['vlan_mode'] = "native-untagged" - port_data['routing'] = False - - if not trunk_list: - port_data['vlan_trunks'] = [] - else: - for key in trunk_list: - if key not in port_data['vlan_trunks']: - port_data['vlan_trunks'].append("/rest/v10.04/system/vlans/%s" % key) - - target_url = kwargs["url"] + "system/interfaces/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding VLANs '%s' to Port '%s' trunk failed with status code %d: %s" - % (vlan_trunk_ids, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VLANs '%s' to Port '%s' trunk succeeded" - % (vlan_trunk_ids, l2_port_name)) - return True - - -def _port_set_native_vlan(l2_port_name, vlan_id, tagged=True, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN to be the native VLAN on the trunk. Also gives the option to set - the VLAN as tagged. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to add to trunk port - :param tagged: Boolean to determine if the native VLAN will be set as the tagged VLAN. If False, the VLAN - will be set as the native untagged VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if tagged: - vlan_mode = "native-tagged" - else: - vlan_mode = "native-untagged" - - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - vlan_uri = "/rest/v10.04/system/vlans/%d" % vlan_id - vlan_key = {str(vlan_id): vlan_uri} - port_data = get_interface(l2_port_name_percents, depth=1, selector="writable", **kwargs) - - port_data['vlan_tag'] = vlan_uri - port_data['routing'] = False - port_data['vlan_mode'] = vlan_mode - - if (port_data['vlan_trunks']) and (vlan_key not in port_data['vlan_trunks']): - port_data['vlan_trunks'].update(vlan_key) - - target_url = kwargs["url"] + "system/interfaces/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting native VLAN ID '%d' to Port '%s' failed with status code %d: %s" - % (vlan_id, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting native VLAN ID '%d' to Port '%s' succeeded" - % (vlan_id, l2_port_name)) - return True - - -def _delete_vlan_port(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT calls to remove a VLAN from a trunk port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to remove from trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - port_data = get_interface(l2_port_name, depth=1, selector="writable", **kwargs) - - if str(vlan_id) in port_data['vlan_trunks']: - # remove vlan from 'vlan_trunks' - port_data['vlan_trunks'].pop(str(vlan_id)) - - target_url = kwargs["url"] + "system/interface/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing VLAN ID '%d' from Port '%s' trunk failed with status code %d: %s" - % (vlan_id, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing VLAN ID '%d' from Port '%s' trunk succeeded" - % (vlan_id, l2_port_name)) - return True - - -def add_port_to_lag(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to configure a Port as a LAG member, and also enable the port. For v1, - also perform DELETE call to remove the Port table entry for the port. - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _add_port_to_lag_v1(int_name, lag_id, **kwargs) - else: # Updated else for when version is v10.04 - return _add_port_to_lag(int_name, lag_id, **kwargs) - - -def _add_port_to_lag_v1(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to configure a Port as a LAG member, and also enable the port. - Also perform DELETE call to remove the Port table entry for the port. - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - int_name_percents = common_ops._replace_special_characters(int_name) - - int_data = get_interface(int_name, 0, "configuration", **kwargs) - - int_data['user_config'] = {"admin": "up"} - int_data['other_config']['lacp-aggregation-key'] = lag_id - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding Interface '%s' to LAG '%d' " - "failed with status code %d: %s" % (int_name, lag_id, response.status_code, response.text)) - success = False - else: - logging.info("SUCCESS: Adding Interface '%s' to LAG '%d' " - "succeeded" % (int_name, lag_id)) - success = True - # Delete Port Table entry for the port - return success and port.delete_port(int_name_percents, **kwargs) - - -def _add_port_to_lag(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to configure a Port as a LAG member, and also enable the port - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - int_data = get_interface(int_name, 1, "writable", **kwargs) - - int_data['user_config'] = {"admin": "up"} - int_data['other_config']['lacp-aggregation-key'] = lag_id - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding Interface '%s' to LAG '%d' " - "failed with status code %d: %s" % (int_name, lag_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Interface '%s' to LAG '%d' " - "succeeded" % (int_name, lag_id)) - return True - - -def remove_port_from_lag(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to configure a Port as a LAG member, and also disable the port - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _remove_port_from_lag_v1(int_name, lag_id, **kwargs) - else: # Updated else for when version is v10.04 - return _remove_port_from_lag(int_name, lag_id, **kwargs) - - -def _remove_port_from_lag_v1(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to remove a Port from a LAG, and also disable the port - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - # Create Port Table entry for the port - add_l2_interface(int_name, **kwargs) - - int_name_percents = common_ops._replace_special_characters(int_name) - - int_data = get_interface(int_name, 0, "configuration", **kwargs) - - int_data['user_config'] = {"admin": "down"} - int_data['other_config'].pop('lacp-aggregation-key', None) - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing interface '%s' from LAG '%d' " - "failed with status code %d: %s" % (int_name, lag_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing interface '%s' from LAG '%d' " - "succeeded" % (int_name, lag_id)) - return True - - -def _remove_port_from_lag(int_name, lag_id, **kwargs): - """ - Perform GET and PUT calls to remove a Port from a LAG, and also disable the port - - :param int_name: Alphanumeric name of the interface - :param lag_id: Numeric ID of the LAG to which the port is to be added - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - int_data = get_interface(int_name, 1, "writable", **kwargs) - - int_data['user_config'] = {"admin": "down"} - int_data['other_config'].pop('lacp-aggregation-key', None) - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing interface '%s' from LAG '%d' " - "failed with status code %d: %s" % (int_name, lag_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing interface '%s' from LAG '%d' " - "succeeded" % (int_name, lag_id)) - return True - - -def _clear_interface_acl(interface_name, acl_type, **kwargs): - """ - Perform GET and PUT calls to clear an interface's ACL - - :param port_name: Alphanumeric name of the Port - :param acl_type: Type of ACL: options are 'aclv4_out', 'aclv4_in', 'aclv6_in', or 'aclv6_out' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if acl_type not in ['aclv4_out', 'aclv4_in', 'aclv6_in', 'aclv6_out']: - raise Exception("ERROR: acl_type should be 'aclv4_out', 'aclv4_in', 'aclv6_in', or 'aclv6_out'") - - int_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = get_interface(interface_name, depth=1, selector="writable", **kwargs) - - if interface_name.startswith('lag'): - if interface_data['interfaces']: - interface_data['interfaces'] = common_ops._dictionary_to_list_values(interface_data['interfaces']) - - cfg_type = acl_type + '_cfg' - cfg_version = acl_type + '_cfg_version' - - interface_data.pop(cfg_type, None) - interface_data.pop(cfg_version, None) - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) + put_data = json.dumps(interface_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=put_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Updating Interface table and Port table '{}' succeeded".format( + self.name)) + # Set new original attributes + self.__original_attributes = interface_data + # Object was modified + modified = True + return modified + + @connected + def __add_member_to_lag(self, lag): + """ + Perform PUT calls to configure a Port as a LAG member, + and also enable the port + + :param lag: pyaoscx.Interface object, to which the current port is + being assigned to + """ + + if not lag.materialized: + raise VerificationError( + 'LAG {}'.format(lag.name), + 'Object is not materialized - Perform get()') + + lag_name = lag.name + # Extract LAG ID from LAG name + lag_id = int(re.search('\\d+', lag_name).group()) + + # Update Values + try: + self.user_config["admin"] = "down" + except AttributeError: + pass + try: + self.other_config['lacp-aggregation-key'] = lag_id + except AttributeError: + pass + + # Make a POST call and update values + self.update() + + @connected + def __delete_lag(self, lag): + """ + Perform PUT calls to update Interface, deleting the LAG + reference inside of the Port that was assigned to that + LAG + + :param lag: pyaoscx.Interface object + """ + + if not lag.materialized: + raise VerificationError( + 'LAG {}'.format(lag.name), + 'Object is not materialized - Perform get()') + + # Update Values + try: + self.user_config["admin"] = "down" + except AttributeError: + pass + self.other_config.pop('lacp-aggregation-key', None) + + # Make a PUT call and update values + self.update() + + def get_uri(self): + ''' + Method used to obtain the specific Interface URI + return: Object's URI + ''' + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{name}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Interface.base_uri, + name=self.percents_name + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def __str__(self): + """ + String containing the Interface name + :return: String + """ + return "Interface Object, name: '{}'".format(self.name) + + def __set_to_default(self): + ''' + Perform a PUT call to set Interface to default settings + :return: True if object was changed + ''' + # Check for IPv6 addresses and delete them + for address in self.ip6_addresses: + address.delete() + # Clean Attribute + self.ip6_addresses = [] + + interface_data = {} + # Clear Interfaces + if hasattr(self, 'interfaces') and self.interfaces is not None: + if self.__is_special_type and self.name == 'lag': + self.interfaces = [] + for element in self.__prev_interfaces: + # If element was deleted from interfaces + if element not in self.interfaces: + # Delete element reference to current LAG + try: + element.__delete_lag(self) + except AttributeError: + # Ignore error + pass + else: + self.interfaces = [self] + + # Set prev interfaces with current ones + # Copies interfaces + self.__prev_interfaces = list(self.interfaces) + + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri, + name=self.percents_name + ) + + put_data = json.dumps(interface_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=put_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError(response.text, response.status_code) - response = kwargs["s"].put(target_url, data=put_data, verify=False) + else: + logging.info( + "SUCCESS: Set Interface to default settings '{}' \ + succeeded".format(self.name)) - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing %s ACL on Interface '%s' failed with status code %d: %s" - % (cfg_type, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing %s ACL on Interface '%s' succeeded" - % (cfg_type, interface_name)) + # Update values with new ones + self.get() return True - -def initialize_interface_entry(int_name, **kwargs): - """ - Perform a PUT call on the interface to initialize it to it's default state. - - :param int_name: Alphanumeric name of the system interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(int_name) - int_data = {} - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Initializing interface '%s' failed with status code %d: %s" - % (int_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Initializing interface '%s' succeeded" % int_name) - success = True - # Remove all IPv6 entries for this interface - ipv6_list = get_ipv6_addresses(int_name, **kwargs) - if ipv6_list: - for ipv6_address in ipv6_list: - success = success and delete_ipv6_address(int_name, ipv6_address, **kwargs) - return success - - -def initialize_interface(interface_name, **kwargs): - """ - Perform a PUT call to the Interface Table or Port Table to initialize an interface to factory settings - - :param interface_name: Name of interface's reference entry in Interface table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port.initialize_port_entry(interface_name, **kwargs) - else: # Updated else for when version is v10.04 - return initialize_interface_entry(interface_name, **kwargs) \ No newline at end of file + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def configure_l2(self, phys_ports=None, ipv4=None, vlan_ids_list=None, + vlan_tag=1, lacp="passive", description=None, + admin="up", fallback_enabled=False, mc_lag=False, + vlan_mode='native-untagged', + trunk_allowed_all=False, + native_vlan_tag=True): + """ + Configure a Interface object, set the attributes to a L2 LAG + and apply() changes inside Switch + + :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", + "1/1/2", "1/1/3"]) or list of Interface Objects. + :param ipv4: Optional list of IPv4 address to assign + to the interface. If more than one is specified, + all addresses except for the first are added as secondary_ip4 + Defaults to nothing if not specified. + Example: + ['1.1.1.1', '2.2.2.2'] + :param vlan_ids_list: Optional list of integer VLAN IDs or VLAN + objects to add as trunk VLANS. Defaults to empty list if not + specified. + :param vlan_tag: Optional VLAN ID or Vlan object to be added as + vlan_tag. Defaults to VLAN 1. + :param lacp: Should be either "passive" or "active." Defaults to + "passive" if not specified + :param description: Optional description for the interface. Defaults + to nothing if not specified. + :param admin: Optional administratively-configured state of the port. + Defaults to "up" if not specified + :param fallback_enabled: Boolean to determine if the LAG uses LACP + fallback. Defaults to False if not specified. + :param mc_lag: Boolean to determine if the LAG is multi-chassis. + Defaults to False if not specified. + :param vlan_mode: Vlan mode on Interface, should be access or trunk + Defaults to 'native-untagged' + :param trunk_allowed_all: Flag for vlan trunk allowed all on L2 + interface, vlan_mode must be set to trunk. + :param native_vlan_tag: Flag for accepting only tagged packets on + VLAN trunk native, vlan_mode must be set to trunk. + + :return: True if object was changed + + """ + + if not self.materialized: + raise VerificationError('Interface {}'.format( + self.name), 'Object not materialized') + + ''' + Set ALL incoming attributes + ''' + + # Set Physical Ports + if phys_ports is not None: + self.interfaces = [] + for port in phys_ports: + port_obj = self.session.api_version.get_module( + self.session, 'Interface', + port) + # Materialize Port + port_obj.get() + self.interfaces.append(port_obj) + # Set lacp + self.lacp = lacp + + # Set Mode + self.vlan_mode = vlan_mode + + if self.vlan_mode == 'access': + # Set VLAN Tag into Object + if isinstance(vlan_tag, int): + # Create Vlan object + vlan_tag = Vlan(self.session, vlan_tag) + # Try to get data; if non-existent, throw error + vlan_tag.get() + self.vlan_tag = vlan_tag + + # Modify if trunk + elif self.vlan_mode == 'trunk': + if vlan_tag is None: + vlan_tag = 1 + + # Create Vlan object + vlan_tag = Vlan(self.session, vlan_tag) + # Try to get data; if non-existent, throw error + vlan_tag.get() + # Set VLAN tag + self.vlan_tag = vlan_tag + + # Set VLAN mode + if native_vlan_tag: + self.vlan_mode = 'native-tagged' + else: + self.vlan_mode = 'native-untagged' + + if not trunk_allowed_all: + self.vlan_mode = 'native-untagged' + # Set VLAN Trunks + if vlan_ids_list is not None: + self.vlan_trunks = [] + for vlan in vlan_ids_list: + vlan_obj = Vlan(self.session, vlan) + vlan_obj.get() + self.vlan_trunks.append(vlan_obj) + + elif trunk_allowed_all: + self.vlan_mode = 'native-untagged' + + # Set description + if description is not None: + self.description = description + # Set admin + self.admin = admin + if "lag" not in self.name: + try: + self.user_config["admin"] = admin + except AttributeError: + # For loopback + pass + + # Set IPv4 + if ipv4 is not None and ipv4 != []: + for i in range(len(ipv4)): + if i == 0: + self.ip4_address = ipv4[i] + else: + self.ip4_address_secondary.append(ipv4[i]) + # If IPv4 is empty, delete + elif ipv4 == []: + self.ip4_address = None + self.ip4_address_secondary = None + + # Set all remaining attributes for a Lag to be a L2 + self.routing = False + if self.__is_special_type: + self.other_config["mclag_enabled"] = mc_lag + self.other_config["lacp-fallback"] = fallback_enabled + + # Apply Changes inside Switch + return self.apply() + + def configure_l3(self, phys_ports=None, ipv4=None, ipv6=None, + vrf="default", lacp="passive", description=None, + admin="up", fallback_enabled=False, mc_lag=False): + """ + Configure a Interface object, if not materialized, materialize it and + then set the attributes to a L3 LAG + and apply() changes inside Switch + + :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", + "1/1/2", "1/1/3"]) or list of Interface Objects. + :param ipv4: Optional list of IPv4 address to assign + to the interface. If more than one is specified, + all addresses except for the first are added as secondary_ip4 + Defaults to nothing if not specified. + Example: + ['1.1.1.1', '2.2.2.2'] + :param ipv6: String list of IPv6 addresses to assign to the interface. + Defaults to nothing if not specified. + List of A Ipv6 objects is accepted + Example: + ['2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'] + :param vrf: VRF to attach the SVI to. Defaults to "default" if not + specified + A Vrf object is also accepted + :param lacp: Should be either "passive" or "active." Defaults to + "passive" if not specified. + :param description: Optional description for the interface. Defaults to + nothing if not specified. + :param admin: Optional administratively-configured state of the port. + Defaults to "up" if not specified + :param fallback_enabled: Boolean to determine if the LAG uses LACP + fallback. Defaults to False if not specified. + :param mc_lag: Boolean to determine if the LAG is multi-chassis. + Defaults to False if not specified. + :return: True if object was changed + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + ''' + Set ALL incoming attributes + ''' + + # Set Physical Ports + if phys_ports is not None: + self.interfaces = [] + for port in phys_ports: + port_obj = self.session.api_version.get_module( + self.session, 'Interface', + port) + # Materialize Port + port_obj.get() + self.interfaces.append(port_obj) + + # Set IPv4 + if ipv4 is not None and ipv4 != []: + for i in range(len(ipv4)): + if i == 0: + self.ip4_address = ipv4[i] + else: + self.ip4_address_secondary.append(ipv4[i]) + # If IPv4 is empty, delete + elif ipv4 == []: + self.ip4_address = None + self.ip4_address_secondary = None + + # Set IPv6 + if ipv6 is not None and ipv6 != []: + for ip_address in ipv6: + # Verify if incoming address is a string + if isinstance(ip_address, str): + # Create Ipv6 object -- add it to ipv6_addresses internal + # list + ip_address = self.session.api_version.get_module( + self.session, 'Ipv6', ip_address, + parent_int=self, + type="global-unicast", + preferred_lifetime=604800, + valid_lifetime=2592000, + node_address=True, + ra_prefix=True) + # Try to get data, if non existent create + try: + # Try to obtain IPv6 address data + ip_address.get() + # If Ipv6 Object is non existent, create it + except GenericOperationError: + # Create IPv6 inside switch + ip_address.apply() + # If IPv6 is empty, delete + elif ipv6 == []: + self.ip6_addresses = [] + + # Set lacp + self.lacp = lacp + # Set description + if description is not None: + self.description = description + # Set admin + self.admin = admin + if "lag" not in self.name: + try: + self.user_config["admin"] = admin + except AttributeError: + # For loopback + pass + + # Set VRF + vrf_obj = Vrf(self.session, vrf) + vrf_obj.get() + self.vrf = vrf_obj + + # Set all remaining attributes for a Lag to be a L3 + self.routing = True + if self.__is_special_type: + self.other_config["mclag_enabled"] = mc_lag + self.other_config["lacp-fallback"] = fallback_enabled + self.vlan_mode = "native-untagged" + # Apply Changes inside Switch + return self.apply() + + def configure_svi( + self, + vlan=None, + ipv4=None, + ipv6=None, + vrf=None, + description=None, + int_type="vlan", + user_config='up'): + """ + Configure a Interface table entry for a VLAN. + + :param vlan: Numeric ID of VLAN + A Vlan object is also accepted + :param ipv4: Optional list of IPv4 address to assign + to the interface. If more than one is specified, + all addresses except for the first are added as secondary_ip4 + Defaults to nothing if not specified. + Example: + ['1.1.1.1'] + :param ipv6: String list of IPv6 addresses to assign to the interface. + Defaults to nothing if not specified. + A Ipv6 object is also accepted + Example: + ['2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'] + :param vrf: VRF to attach the SVI to. Defaults to "default" if not + specified. + A Vrf object is also accepted + :param description: Optional description for the interface. Defaults + to nothing if not specified. + :param int_type: Type of interface; generally should be "vlan" for + SVI's. Defaults to vlan + :param user_config: User configuration to apply to interface. Defaults + to "up" if not specified. + :return: True if object was changed + + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + if vlan is not None: + vlan_tag = vlan + # Set VLAN Tag into Object + if isinstance(vlan, int): + name = "VLAN {}".format(str(vlan)) + # Create Vlan object + vlan_tag = self.session.api_version.get_module( + self.session, 'Vlan', vlan, name=name) + # Try to obtain data; if not, create + try: + vlan_tag.get() + except GenericOperationError: + # Create object inside switch + vlan_tag.apply() + + self.vlan_tag = vlan_tag + + # Set IPv4 + if ipv4 is not None and ipv4 != []: + for i in range(len(ipv4)): + if i == 0: + self.ip4_address = ipv4[i] + else: + self.ip4_address_secondary.append(ipv4[i]) + # If IPv4 is empty, delete + elif ipv4 == []: + self.ip4_address = None + self.ip4_address_secondary = None + # Set IPv6 + if ipv6 is not None and ipv6 != []: + for ip_address in ipv6: + # Verify if incoming address is a string + if isinstance(ip_address, str): + # Create Ipv6 object -- add it to ipv6_addresses internal + # list + ip_address = self.session.api_version.get_module( + self.session, 'Ipv6', ip_address, + parent_int=self, + type="global-unicast", + preferred_lifetime=604800, + valid_lifetime=2592000, + node_address=True, + ra_prefix=True) + # Try to get data, if non existent create + try: + # Try to obtain IPv6 address data + ip_address.get() + # If Ipv6 Object is non existent, create it + except GenericOperationError: + # Create IPv6 inside switch + ip_address.apply() + # If IPv6 is empty, delete + elif ipv6 == []: + self.ip6_addresses = [] + + # Set VRF + if vrf is not None: + if isinstance(vrf, str): + vrf = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + vrf.get() + + self.vrf = vrf + + # Set user config + self.admin = user_config + try: + self.user_config["admin"] = user_config + except AttributeError: + # Ignore if attribute error + pass + + # Set type + self.type = int_type + + if description is not None: + self.description = description + + # Apply changes + return self.apply() + + def add_ipv4_address(self, ip_address): + """ + Configure a Interface object to add a new IPv4 address to it and + calls apply(), applying changes inside Switch + + :param ip_address: IPv4 address to assign to the interface. + Example: + "1.1.1.1" + :return: True if object was changed + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + # Set incoming IPv4 address + self.ip4_address = ip_address + + # Apply changes inside switch + return self.apply() + + def add_ipv6_address(self, ip_address, address_type="global-unicast"): + """ + Configure a Interface object to append a IPv6 address to its + ip6_addresses list and apply changes + + :param ip_address: IPv6 address to assign to the interface. + A Ipv6 object is also accepted. + Example of String: + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :param address_type: Type of Address. + Defaults to global-unicast + :return: Ipv6 object. + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), + 'Object not materialized') + + # Verify if incoming address is a string + if isinstance(ip_address, str): + # Create Ipv6 object -- add it to ipv6_addresses internal list + ipv6 = self.session.api_version.get_module( + self.session, 'Ipv6', ip_address, parent_int=self, + type=address_type, + preferred_lifetime=604800, + valid_lifetime=2592000, + node_address=True, + ra_prefix=True) + # Try to get data, if non existent create + try: + # Try to obtain IPv6 address data + ipv6.get() + # If Ipv6 Object is non existent, create it + except GenericOperationError: + # Create IPv6 inside switch + ipv6.apply() + + # Apply changes inside switch + self.apply() + + return ipv6 + + def delete_ipv6_address(self, ip_address): + """ + Given a IPv6 address, delete that address from the current + Interface object. + :param ip_address: IPv6 address to assign to the interface. + A Ipv6 object is also accepted + Example: + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + # Verify if incoming address is a object + if isinstance(ip_address, Ipv6): + # Obtain address + ip_address = ip_address.address + + # Iterate through every address inside interface + for add_obj in self.ip6_addresses: + if add_obj.address == ip_address: + # Removing address does an internal delete + self.ip6_addresses.remove(add_obj) + + def configure_loopback(self, vrf, ipv4=None, description=None): + """ + Configure a Interface object to create a Loopback Interface table + entry for a logical L3 Interface. If the Loopback Interface already + exists and an IPv4 address is given, the function will update the + IPv4 address. + + :param vrf: VRF to attach the Loopback to. Defaults to "default" + if not specified + :param ipv4: IPv4 address to assign to the interface. Defaults to + nothing if not specified. + Example: + '1.1.1.1' + :param description: Optional description for the interface. Defaults + to nothing if not specified. + :return: True if object was changed + + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), + 'Object not materialized') + + # Set VRF + if vrf is not None: + if isinstance(vrf, str): + vrf = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + vrf.get() + + self.vrf = vrf + + # Set IPv4 + if ipv4 is not None and ipv4 != []: + for i in range(len(ipv4)): + if i == 0: + self.ip4_address = ipv4[i] + else: + self.ip4_address_secondary.append(ipv4[i]) + # If IPv4 is empty, delete + elif ipv4 == []: + self.ip4_address = None + self.ip4_address_secondary = None + + if description is not None: + self.description = description + + # Set all remaining attributes to create a loopback + + # Set both admin and user up + self.admin = 'up' + if "lag" not in self.name: + try: + self.user_config["admin"] = 'up' + except AttributeError: + # For loopback + pass + + self.ospf_if_type = "ospf_iftype_loopback" + + # Apply changes to switch + return self.apply() + + def configure_vxlan(self, source_ipv4=None, description=None, + dest_udp_port=4789): + """ + Configure VXLAN table entry for a logical L3 + Interface. If the VXLAN Interface already exists and an IPv4 + address is given, the function will update the IPv4 address. + + :param source_ipv4: Optional source IPv4 address to assign to the + VXLAN interface. Defaults to nothing if not specified. + Example: + '1.1.1.1' + :param description: Optional description for the interface. Defaults + to nothing if not specified. + :param dest_udp_port: Optional Destination UDP Port that the VXLAN + will use. Default is set to 4789 + :return: True if object was changed + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + # Set Values + self.options["local_ip"] = source_ipv4 + self.options["vxlan_dest_udp_port"] = str(dest_udp_port) + + self.type = "vxlan" + # Both user and admin up + self.admin = 'up' + if "lag" not in self.name: + try: + self.user_config["admin"] = 'up' + except AttributeError: + # For loopback + pass + + if description is not None: + self.description = description + + # Apply changes + return self.apply() + + def set_vlan_mode(self, vlan_mode): + """ + Set an L2 interface's VLAN mode (native-tagged, + native-untagged, or access) + + :param vlan_mode: A string, either 'native-tagged', 'native-untagged', + or 'access', specifying the desired VLAN mode + :return: True if object was changed + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + # Set Values + self.vlan_mode = vlan_mode + self.routing = False + + # Apply changes + return self.apply() + + def set_untagged_vlan(self, vlan): + """ + Set the untagged VLAN on an access port + + :param vlan: Numeric ID of VLAN to set on access port + A Vlan object is also accepted + :return: True if object was changed + """ + + if not self.materialized: + raise VerificationError( + 'Interface {}'.format(self.name), 'Object not materialized') + + # Set Values + self.vlan_mode = 'access' + + vlan_tag = vlan + # Set Vlan Tag into Object + if isinstance(vlan, int): + # Create Vlan object + vlan_tag = self.session.api_version.get_module( + self.session, 'Vlan', vlan) + # Try to get data; if non-existent, throw error + vlan_tag.get() + + # Set Vlan Tag + self.vlan_tag = vlan_tag + + self.routing = False + + # Apply changes + return self.apply() + + def add_vlan_trunks(self, vlan_trunk_ids): + """ + Add specified VLANs to a trunk port. By default, this will also set + the port to have 'no routing' and if there is not a native VLAN, + will set the native VLAN to VLAN 1. + + :param vlan_trunk_ids: Dictionary of VLANs to specify + as allowed on the trunk port. If empty, the interface + will allow all VLANs on the trunk. + :return: True if object was changed + """ + + # Set vlan Trunks + if vlan_trunk_ids is not None: + self.vlan_trunks = [] + + for vlan in vlan_trunk_ids: + + vlan_obj = self.session.api_version.get_module( + self.session, 'Vlan', vlan) + vlan_obj.get() + + self.vlan_trunks.append(vlan_obj) + + self.routing = False + + # Set other values in case of None + if self.vlan_mode is not None: + self.vlan_mode = "native-untagged" + + if self.vlan_tag is not None: + vlan_tag_obj = self.session.api_version.get_module( + self.session, 'Vlan', 1) + vlan_tag_obj.get() + self.vlan_tag = vlan_tag_obj + + # Apply Changes + return self.apply() + + def set_native_vlan(self, vlan, tagged=True): + """ + Set a VLAN to be the native VLAN on the trunk. + Also gives the option to set the VLAN as tagged. + + :param vlan: Numeric ID of VLAN to add to trunk port. + A Vlan object is also accepted + :param tagged: Boolean to determine if the native VLAN + will be set as the tagged VLAN. If False, the VLAN + will be set as the native untagged VLAN + Defaults to True + :return: True if object was changed + """ + + if tagged: + self.vlan_mode = "native-tagged" + else: + self.vlan_mode = "native-untagged" + + vlan_tag = vlan + # Set Vlan Tag into Object + if isinstance(vlan_tag, int): + # Create Vlan object + vlan_tag = self.session.api_version.get_module( + self.session, 'Vlan', vlan) + # Try to get data; if non-existent, throw error + vlan_tag.get() + + self.vlan_tag = vlan_tag + + self.routing = False + + # Flag used to check if the incoming vlan has to be added to trunks + add = True + # Verify native vlan is in vlan trunks + for vlan_obj in self.vlan_trunks: + # Check vlan + if vlan_obj.id == vlan: + # Don't add + add = False + if add: + # Add new vlan to vlan trunks + self.vlan_trunks.append(self.vlan_tag) + + # Apply Changes + return self.apply() + + def delete_vlan(self, vlan): + """ + Delete a VLAN from a trunk port. + + :param vlan: Numeric ID of VLAN to delete from the trunk port. + A Vlan object is also accepted + : return: True if successfully deleted, False otherwise + """ + # Import VLAN to identify object type + from pyaoscx.vlan import Vlan + if isinstance(vlan, Vlan): + vlan_id = vlan.id + else: + vlan_id = vlan + + deleted = False + # Iterate through vlan trunks in search of the vlan + for vlan_obj in self.vlan_trunks: + if vlan_obj.id == vlan_id: + # Delete vlan from vlan trunks + self.vlan_trunks.remove(vlan_obj) + deleted = True + # Apply Changes + self.apply() + + return deleted + + def add_port_to_lag(self, interface): + """ + Configure a Port as a LAG member, and also enable the port. + Add port to list of interfaces inside Interface object + + :param interface: Alphanumeric name of the interface + A Interface object is also accepted + :return: True if object was changed + """ + + # Identify interface variable type + if isinstance(interface, str): + # Create Interface Object + interface_obj = self.session.api_version.get_module( + self.session, 'Interface', interface) + # Try to get data; if non-existent, throw error + interface_obj.get() + + elif isinstance(interface, Interface): + interface_obj = interface + + for member in self.interfaces: + # Check existance inside members + if member.name == interface_obj.name: + # Stop execution + return False + + # Add interface as a member of the lag + self.interfaces.append(interface_obj) + + # Apply changes + return self.apply() + + def remove_port_from_lag(self, interface): + """ + Remove a Port from LAG, and also disable the port. + Remove port from list of interfaces inside Interface object + + :param interface: Alphanumeric name of the interface + A Interface object is also accepted + :return: True if object was changed + """ + if not self.__is_special_type: + raise VerificationError( + "Interface {}".format(self.name), + "Interface object must be a lag to remove a Port") + + # Identify interface type + if isinstance(interface, Interface): + interface_name = interface.name + elif isinstance(interface, str): + interface_name = interface + + for member in self.interfaces: + # Check existence inside members + if member.name == interface_name: + # Remove interface from Member + self.interfaces.remove(member) + + # When changes are applied, port is disabled and lacp key changed + return self.apply() + + def clear_acl(self, acl_type): + """ + Clear an interface's ACL + + :param acl_type: Type of ACL: options are 'aclv4_out', 'aclv4_in', + 'aclv6_in', or 'aclv6_out' + :return: True if object was changed + """ + if acl_type == "ipv6": + self.aclv6_in_cfg = None + self.aclv6_in_cfg_version = None + if acl_type == "ipv4": + self.aclv4_in_cfg = None + self.aclv4_in_cfg_version = None + if acl_type == "mac": + self.aclmac_in_cfg = None + self.aclmac_in_cfg_version = None + + # Apply Changes + return self.apply() + + def initialize_interface_entry(self): + """ + Initialize Interface to its default state. + :return: True if object was changed + """ + # Set interface to default settings + return self.__set_to_default() + + def set_state(self, state="up"): + """ + Either enable or disable the interface by setting Interface's + admin_state to "up" or "down" + + :param state: State to set the interface to + Defaults to up + :return: True if object was changed + + """ + # Set interface to default settings + self.admin = state + if "lag" not in self.name: + try: + self.user_config["admin"] = state + except AttributeError: + # For loopback + pass + + # Apply Changes + return self.apply() + + def configure_vsx(self, active_forwarding, vsx_sync, act_gw_mac, + act_gw_ip): + """ + Configure VSX IPv4 settings on a VLAN Interface. + + :param active_forwarding: True or False Boolean to set VSX active + forwarding + :param vsx_sync: List of alphanumeric values to enable VSX + configuration synchronization. The options are + any combination of 'active-gateways', 'irdp', and 'policies'. + VSX Sync is mainly used in the Primary. + :param act_gw_mac: Alphanumeric value of the Virtual MAC address for + the interface active gateway + Example: + '01:02:03:04:05:06' + :param act_gw_ip: Alphanumeric value of the Virtual IP address for the + interface active gateway. + Example: + '1.1.1.1' + :return: True if object was changed + """ + # Set values + vsx_sync_list = [] + if "active-gateways" in vsx_sync: + vsx_sync_list.append("^vsx_virtual.*") + if "irdp" in vsx_sync: + vsx_sync_list.append(".irdp.*") + if "policies" in vsx_sync: + vsx_sync_list.append("^policy.*") + + self.vsx_active_forwarding_enable = active_forwarding + self.vsx_sync = vsx_sync_list + self.vsx_virtual_gw_mac_v4 = act_gw_mac + self.vsx_virtual_ip4 = [act_gw_ip] + + # Apply changes + return self.apply() + + def delete_vsx_configuration(self): + """ + Delete VSX IPv4 settings on a VLAN Interface. + :return: True if object was changed + + """ + # Set values + self.vsx_active_forwarding_enable = False + self.vsx_sync = [] + self.vsx_virtual_gw_mac_v4 = None + self.vsx_virtual_ip4 = [] + + # Apply changes + return self.apply() + + def configure_l3_ipv4_port(self, ip_address=None, + port_desc=None, port_admin_state="up", + vrf="default"): + """ + Function will enable routing on the port and update the IPv4 address + if given. + + :param ip_address: IPv4 address to assign to the interface. Defaults + to nothing if not specified. + Example: + '1.1.1.1' + :param port_desc: Optional description for the interface. Defaults to + nothing if not specified. + :param port_admin_state: Optional administratively-configured state of + the port. + Defaults to "up" if not specified + :param vrf: Name of the VRF to which the Port belongs. Defaults to + "default" if not specified. + :return: True if object was changed + + """ + + # Set IPv4 + if ip_address is not None: + self.ip4_address = ip_address + + # Set description + if port_desc is not None: + self.description = port_desc + # Set admin + self.admin = port_admin_state + if "lag" not in self.name: + try: + self.user_config["admin"] = port_admin_state + except AttributeError: + # For loopback + pass + + # Set vrf + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + vrf_obj.get() + self.vrf = vrf_obj + + # Set routing + self.routing = True + + # Apply Changes inside Switch + return self.apply() + + def update_ospf_interface_authentication(self, vrf, + auth_type, + digest_key, auth_pass): + """ + Perform PUT calls to update an Interface with OSPF to have + authentication + + :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to + :param auth_type: Alphanumeric type of authentication, chosen between + 'md5', 'null', and 'text' + :param digest_key: Integer between 1-255 that functions as the digest + key for the authentication method + :param auth_pass: Alphanumeric text for the authentication password. + Note that this will be translated to a + base64 String in the configuration and json. + :return: True if object was changed + """ + + # Configure Port/Interface + self.configure_l3_ipv4_port(vrf=vrf) + + self.ospf_auth_type = auth_type + self.ospf_auth_md5_keys = {str(digest_key): auth_pass} + self.ospf_if_type = "ospf_iftype_broadcast" + self.routing = True + # Set vrf + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + vrf_obj.get() + self.vrf = vrf_obj + + # Apply changes + return self.apply() + + def update_ospf_interface_type(self, vrf, + interface_type="pointtopoint"): + """ + Update the Interface's OSPFv2 type, + as well as enable routing on the interface + + :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to + :param interface_type: Alphanumeric type of OSPF interface. + The options are 'broadcast', 'loopback', 'nbma', + 'none', 'pointomultipoint', 'pointopoint', and 'virtuallink' + Defaults to pointtopoint + :return: True if object was changed + """ + if interface_type not in [ + 'broadcast', 'loopback', 'statistics', + 'nbma', 'pointomultipoint', + 'pointopoint', 'virtuallink', None]: + + raise Exception( + "ERROR: Incorrect value for interface type.\ + The options are 'broadcast', 'loopback', 'nbma', " + "'none', 'pointomultipoint', 'pointopoint', and 'virtuallink'") + + # Configure Port/Interface + self.configure_l3_ipv4_port(vrf=vrf) + + self.ospf_if_type = "ospf_iftype_%s" % interface_type + self.routing = True + # Set vrf + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + vrf_obj.get() + self.vrf = vrf_obj + + # Apply changes + return self.apply() + + def set_active_gateway(self, ip_address, gateway_mac): + """ + Update Active Gateway of a Interface + + :param ip_address: IPv4 address to assign to the interface. + Example: + '1.1.1.1' + :param gateway_mac: Active Gateway MAC address to assign to the interface. + Example: + '01:02:03:04:05:06' + :return: True if object was changed + + """ + # Configure Active Gateaway IP + self.vsx_virtual_ip4 = ip_address + # Configure Gateaway mac + self.vsx_virtual_gw_mac_v4 = gateway_mac + + # Apply changes + return self.apply() + + def update_interface_qos_profile(self, qos_profile_details): + """ + Update QoS schedule profile attached to a Interface + + :param qos_profile_details: dict of QoS schedule profile. + :return: True if object was changed + + """ + # Update Values + self.qos = qos_profile_details + + # Apply changes + return self.apply() + + def update_interface_qos_rate(self, qos_rate): + """ + Update the rate limit values configured for + broadcast/multicast/unknown unicast traffic. + + :param qos_rate: dict of the rate limit values; should have the + format [''] = e.g. + { + 'unknown-unicast': 100pps, + 'broadcast': 200pps, 'multicast': 200pps + }" + :return: True if object was changed + + """ + rate_limits = {} + if qos_rate is not None: + for k, v in qos_rate.items(): + for i, c in enumerate(v): + if not c.isdigit(): + break + number = v[:i] + unit = v[i:].lstrip() + + rate_limits[k] = number + rate_limits[k + '_units'] = unit + + self.rate_limits = rate_limits + + # Apply changes + return self.apply() + + def update_acl_in(self, acl_name, list_type): + """ + Perform GET and PUT calls to apply ACL on an interface. This function + specifically applies an ACL to Ingress traffic of the interface + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of IPv4, IPv6 or MAC to specify the + type of ACL + :return: True if object was changed + + """ + import random + + # Create Acl object + acl_obj = self.session.api_version.get_module( + self.session, 'ACL', index_id=acl_name, list_type=list_type) + + if list_type == "ipv6": + self.aclv6_in_cfg = acl_obj + if hasattr(self, 'aclv6_in_cfg_version') and \ + self.aclv6_in_cfg_version is None: + self.aclv6_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + + if list_type == "ipv4": + self.aclv4_in_cfg = acl_obj + if hasattr(self, 'aclv4_in_cfg_version') and \ + self.aclv4_in_cfg_version is None: + self.aclv4_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "mac": + self.aclmac_in_cfg = acl_obj + if hasattr(self, 'aclmac_in_cfg_version') and \ + self.aclmac_in_cfg_version is None: + self.aclmac_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + + # Apply changes + return self.apply() + + def update_acl_out(self, acl_name, list_type): + """ + Perform GET and PUT calls to apply ACL on an interface. + This function specifically applies an ACL + to Egress traffic of the interface, which must be a routing + interface + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of IPv4, IPv6 or MAC to specify the + type of ACL + :return: True if object was changed + + """ + import random + + # Create Acl object + acl_obj = self.session.api_version.get_module( + self.session, 'ACL', index_id=acl_name, list_type=list_type) + + if list_type == "ipv6": + self.aclv6_out_cfg = acl_obj + if hasattr(self, 'aclv6_out_cfg_version') and \ + self.aclv6_out_cfg_version is None: + self.aclv6_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "ipv4": + self.aclv4_out_cfg = acl_obj + if hasattr(self, 'aclv4_out_cfg_version') and \ + self.aclv4_out_cfg_version is None: + self.aclv4_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "mac": + self.aclmac_out_cfg = acl_obj + if hasattr(self, 'aclmac_out_cfg_version') and \ + self.aclmac_out_cfg_version is None: + self.aclmac_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + + # Routeing + self.routing = True + + # Apply changes + return self.apply() diff --git a/pyaoscx/ipv6.py b/pyaoscx/ipv6.py new file mode 100644 index 0000000..c436802 --- /dev/null +++ b/pyaoscx/ipv6.py @@ -0,0 +1,442 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class Ipv6(PyaoscxModule): + ''' + Provide configuration management for IPv6 on AOS-CX devices. + ''' + + indices = ['address'] + resource_uri_name = 'ip6_addresses' + + def __init__(self, session, address, parent_int, uri=None, **kwargs): + + self.session = session + # Assign address + self.__set_name(address) + # Assign parent Interface + self.__set_interface(parent_int) + self._uri = uri + # List used to determine attributes related to the IPv6 configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_name(self, address): + ''' + Set name attribute in the proper form for IPv6 + ''' + + # Add attributes to class + self.address = None + self.reference_address = None + + if r'%2F' in address or r'%3A' in address: + self.address = utils._replace_percents_ip(address) + self.reference_address = address + else: + self.address = address + self.reference_address = utils._replace_special_characters_ip( + self.address) + + def __set_interface(self, parent_int): + ''' + Set parent interface as an attribute for the Ipv6 object + :param parent_int a Interface object + ''' + + # Set Parent Interface + self.__parent_int = parent_int + # Set Name for URI purposes + self.__parent_int_name = self.__parent_int.percents_name + + # Set URI + self.base_uri = '{base_int_uri}/{interface_name}/ip6_addresses'.format( + base_int_uri=self.__parent_int.base_uri, + interface_name=self.__parent_int_name) + + # Add self to ip6_address list in parent Interface + for ip6_address in self.__parent_int.ip6_addresses: + if ip6_address.address == self.address: + # Make list element point to current object + ip6_address = self + + # Adds to parent list + self.__parent_int.ip6_addresses.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a IPv6 table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch IPv6") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{address}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address=self.reference_address + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the IPv6 is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, data, 'config_attrs', ['address']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'address' in self.__original_attributes: + self.__original_attributes.pop('address') + # Remove type + if 'type' in self.__original_attributes: + self.__original_attributes.pop('type') + # Remove origin + if 'origin' in self.__original_attributes: + self.__original_attributes.pop('origin') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_int): + ''' + Perform a GET call to retrieve all system IPv6 addresses inside a Interface, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_int: parent Interface object where IPv6 is stored + :return: Dictionary containing IPv6 IDs as keys and a Ipv6 object as + value + ''' + + logging.info("Retrieving the switch IPv6") + + base_uri = '{base_int_uri}/{interface_name}/ip6_addresses'.format( + base_int_uri=parent_int.base_uri, + interface_name=parent_int.percents_name) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + ipv6_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a Ipv6 object + address, ipv6 = Ipv6.from_uri(session, parent_int, uri) + # Load all IPv6 data from within the Switch + ipv6.get() + ipv6_dict[address] = ipv6 + + return ipv6_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + IPv6. + Checks whether the IPv6 exists in the switch + Calls self.update() if IPv6 is being updated + Calls self.create() if a new IPv6 is being created + + ''' + if not self.__parent_int.materialized: + if self.__parent_int.__is_special_type: + # Verify if it's a LAG + self.__parent_int.apply() + else: + self.__parent_int.get() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing IPv6 table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + ''' + # Variable returned + modified = False + ip6_data = {} + + ip6_data = utils.get_attrs(self, self.config_attrs) + + # Delete Type + if 'type' in ip6_data: + ip6_data.pop('type') + if 'origin' in ip6_data: + ip6_data.pop('origin') + + uri = "{base_url}{class_uri}/{address}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address=self.reference_address + ) + # Compare dictionaries + if ip6_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + + post_data = json.dumps(ip6_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update IPv6 table entry {} succeeded".format + (self.address)) + + # Set new original attributes + self.__original_attributes = ip6_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new IPv6 using the object's attributes + as POST body + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + ''' + ipv6_data = {} + + ipv6_data = utils.get_attrs(self, self.config_attrs) + ipv6_data['address'] = self.address + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(ipv6_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding IPv6 table entry {} succeeded".format( + self.address)) + + # Get all object's data + self.get() + # Object was created, thus modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete IPv6 address from interface on the switch. + + ''' + + uri = "{base_url}{class_uri}/{address}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address=self.reference_address + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete IPv6 table entry {} succeeded".format( + self.address)) + + # Delete back reference from VRF + for ip6 in self.__parent_int.ip6_addresses: + if ip6.address == self.address: + self.__parent_int.ip6_addresses.remove(ip6) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_int, response_data): + ''' + Create a IPv6 object given a response_data related to the IP6 + address object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_int: parent Interface object where IPv6 is stored + :param response_data: The response can be either a + dictionary: { + address: "/rest/v10.04/interface/ip6_addresses/address" + } + or a + string: "/rest/v10.04/interface/ip6_addresses/address" + :return: IPv6 object + ''' + ipv6_arr = session.api_version.get_keys( + response_data, Ipv6.resource_uri_name) + address = ipv6_arr[0] + return Ipv6(session, address, parent_int) + + @classmethod + def from_uri(cls, session, parent_int, uri): + ''' + Create a Ipv6 object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_int: Parent Interface class where IPv6 is stored + :param uri: a String with a URI + + :return index, ipv6_obj: tuple containing both the Ipv6 Object and + the ipv6's address + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)ip6_addresses/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create Ipv6 object + ipv6_obj = Ipv6(session, index, parent_int, uri=uri) + + return index, ipv6_obj + + def __str__(self): + return "IPv6 address {}".format(self.address) + + def get_uri(self): + ''' + Method used to obtain the specific IPv6 URI + return: Object's URI + ''' + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{id}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + id=self.reference_address + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/lag.py b/pyaoscx/lag.py deleted file mode 100644 index 6aa08e6..0000000 --- a/pyaoscx/lag.py +++ /dev/null @@ -1,482 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops -from pyaoscx import port -from pyaoscx import interface - -import json -import re -import logging - - -def create_l2_lag_interface(name, phys_ports, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - vlan_ids_list=[], desc=None, admin_state="up", **kwargs): - """ - Perform a POST call to create a Port table entry for L2 LAG interface. - - :param name: Alphanumeric name of LAG Port - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param vlan_ids_list: Optional list of integer VLAN IDs to add as trunk VLANS. Defaults to empty list if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_l2_lag_interface_v1(name, phys_ports, lacp_mode, mc_lag, fallback_enabled, vlan_ids_list, desc, - admin_state, **kwargs) - else: # Updated else for when version is v10.04 - success = _create_l2_lag_interface(name, phys_ports, lacp_mode, mc_lag, fallback_enabled, vlan_ids_list, desc, - admin_state, **kwargs) - - if mc_lag or fallback_enabled: - return success and _update_l2_lag_interface(name, mc_lag, fallback_enabled, **kwargs) - else: - return success - - -def _create_l2_lag_interface_v1(name, phys_ports, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - vlan_ids_list=[], desc=None, admin_state="up", **kwargs): - """ - Perform a POST call to create a Port table entry for L2 LAG interface. - - :param name: Alphanumeric name of LAG Port - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param vlan_ids_list: Optional list of integer VLAN IDs to add as trunk VLANS. Defaults to empty list if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - - port_name_percents = common_ops._replace_special_characters(name) - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - - # Extract LAG ID from LAG name - lag_id = int(re.search('\d+', name).group()) - - # For each port, add LAG ID to the Interface table entry, and delete the Port table entry - for phys_port in phys_ports: - interface.add_port_to_lag(phys_port, lag_id, **kwargs) - - interfaces = ["/rest/v1/system/interfaces/%s" % common_ops._replace_special_characters(phys_port) - for phys_port in phys_ports] - port_data = {"admin": admin_state, - "interfaces": interfaces, - "name": name, - "routing": False, - "vlan_trunks": ["/rest/v1/system/vlans/%d" % vlan_id for vlan_id in vlan_ids_list], - "lacp": lacp_mode, - "other_config": { - "mclag_enabled": mc_lag, - "lacp-fallback": fallback_enabled - }, - "vlan_mode": "native-untagged", - "vlan_tag": "/rest/v1/system/vlans/1" - } - - if desc is not None: - port_data['description'] = desc - - target_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Port table entry '%s' failed with status code %d: %s" - % (name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Port table entry '%s' succeeded" % name) - return True - else: - logging.info("SUCCESS: No need to add Port table entry '%s' because it already exists" - % name) - return True - - -def _create_l2_lag_interface(name, phys_ports, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - vlan_ids_list=[], desc=None, admin_state="up", **kwargs): - """ - Perform a POST call to create a Port table entry for L2 LAG interface. - - :param name: Alphanumeric name of LAG Port - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param vlan_ids_list: Optional list of integer VLAN IDs to add as trunk VLANS. Defaults to empty list if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_dict = interface.get_all_interfaces(**kwargs) - - if name not in ints_dict: - - # Extract LAG ID from LAG name - lag_id = int(re.search('\d+', name).group()) - - # For each port, add LAG ID to the Interface table entry - for phys_port in phys_ports: - interface.add_port_to_lag(phys_port, lag_id, **kwargs) - - interfaces = ["/rest/v10.04/system/interfaces/%s" % common_ops._replace_special_characters(phys_port) - for phys_port in phys_ports] - int_data = {"admin": admin_state, - "interfaces": interfaces, - "name": name, - "type": "lag", - "routing": False, - "vlan_trunks": ["/rest/v10.04/system/vlans/%d" % vlan_id for vlan_id in vlan_ids_list], - "lacp": lacp_mode, - "other_config": {}, - "vlan_mode": "native-untagged", - "vlan_tag": "/rest/v10.04/system/vlans/1" - } - - if desc is not None: - int_data['description'] = desc - - target_url = kwargs["url"] + "system/interfaces" - post_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Interface table entry '%s' succeeded" % name) - return True - else: - logging.info("SUCCESS: No need to add Interface table entry '%s' because it already exists" - % name) - return True - - -def _update_l2_lag_interface(name, mc_lag=False, fallback_enabled=False, **kwargs): - """ - Perform GET and PUT calls to update the Interface table entry for L2 LAG interface. - - :param name: Alphanumeric name of LAG Port - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - int_name_percents = common_ops._replace_special_characters(name) - - int_data = interface.get_interface(name, 1, "writable", **kwargs) - - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - if int_data['vlan_trunks']: - int_data['vlan_trunks'] = common_ops._dictionary_to_list_values(int_data['vlan_trunks']) - - if int_data['vlan_tag']: - # Convert the dictionary to a URI string - int_data['vlan_tag'] = common_ops._dictionary_to_string(int_data['vlan_tag']) - - int_data['other_config']['mclag_enabled'] = mc_lag - int_data['other_config']['lacp-fallback'] = fallback_enabled - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating LAG Interface entry '%s' " - "failed with status code %d: %s" % (name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating LAG Interface entry '%s' " - "succeeded" % name) - return True - - -def create_l3_lag_interface(name, phys_ports, ipv4, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - desc=None, admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for L3 LAG interface. - - :param name: Alphanumeric Port name - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_l3_lag_interface_v1(name, phys_ports, ipv4, lacp_mode, mc_lag, fallback_enabled, - desc, admin_state, vrf, **kwargs) - else: # Updated else for when version is v10.04 - return _create_l3_lag_interface(name, phys_ports, ipv4, lacp_mode, mc_lag, fallback_enabled, - desc, admin_state, vrf, **kwargs) - - -def create_l3_lag_interface(name, phys_ports, ipv4, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - desc=None, admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for L3 LAG interface. - - :param name: Alphanumeric Port name - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_l3_lag_interface_v1(name, phys_ports, ipv4, lacp_mode, mc_lag, fallback_enabled, - desc, admin_state, vrf, **kwargs) - else: # Updated else for when version is v10.04 - return _create_l3_lag_interface(name, phys_ports, ipv4, lacp_mode, mc_lag, fallback_enabled, - desc, admin_state, vrf, **kwargs) - - -def _create_l3_lag_interface_v1(name, phys_ports, ipv4, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - desc=None, admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for L3 LAG interface. - - :param name: Alphanumeric Port name - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - - port_name_percents = common_ops._replace_special_characters(name) - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - - # Extract LAG ID from LAG name - lag_id = int(re.search('\d+', name).group()) - - # For each port, add LAG ID to the Interface table entry, and delete the Port table entry - for phys_port in phys_ports: - interface.add_port_to_lag(phys_port, lag_id, **kwargs) - - interfaces = ["/rest/v1/system/interfaces/%s" % common_ops._replace_special_characters(phys_port) - for phys_port in phys_ports] - port_data = {"admin": admin_state, - "interfaces": interfaces, - "name": name, - "routing": True, - "vrf": "/rest/v1/system/vrfs/%s" % vrf, - "ip4_address": ipv4, - "lacp": lacp_mode, - "other_config": { - "mclag_enabled": mc_lag, - "lacp-fallback": fallback_enabled - }, - } - - if desc is not None: - port_data['description'] = desc - - if ipv4 is not None: - port_data['ip4_address'] = ipv4 - - target_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Port table entry '%s' failed with status code %d: %s" - % (name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Port table entry '%s' succeeded" % name) - return True - else: - logging.info("SUCCESS: No need to add Port table entry '%s' because it already exists" - % name) - return True - - -def _create_l3_lag_interface(name, phys_ports, ipv4, lacp_mode="passive", mc_lag=False, fallback_enabled=False, - desc=None, admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for L3 LAG interface. - - :param name: Alphanumeric Port name - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param lacp_mode: Should be either "passive" or "active." Defaults to "passive" if not specified. - :param mc_lag: Boolean to determine if the LAG is multi-chassis. Defaults to False if not specified. - :param fallback_enabled: Boolean to determine if the LAG uses LACP fallback. Defaults to False if not specified. - :param desc: Optional description for the interface. Defaults to nothing if not specified. - :param admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ints_dict = interface.get_all_interfaces(**kwargs) - - if name not in ints_dict: - - # Extract LAG ID from LAG name - lag_id = int(re.search('\d+', name).group()) - - # For each port, add LAG ID to the Interface table entry, and delete the Port table entry - for phys_port in phys_ports: - interface.add_port_to_lag(phys_port, lag_id, **kwargs) - - interfaces = ["/rest/v10.04/system/interfaces/%s" % common_ops._replace_special_characters(phys_port) - for phys_port in phys_ports] - int_data = {"admin": admin_state, - "interfaces": interfaces, - "name": name, - "type": "lag", - "vrf": "/rest/v10.04/system/vrfs/%s" % vrf, - "routing": True, - "ip4_address": ipv4, - "lacp": lacp_mode, - # "other_config": { - # "mclag_enabled": mc_lag, - # "lacp-fallback": fallback_enabled - # } - - } - """commented out the other_config since it causes error""" - - if desc is not None: - int_data['description'] = desc - - target_url = kwargs["url"] + "system/interfaces" - post_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Interface table entry '%s' succeeded" % name) - return True - else: - logging.info("SUCCESS: No need to add Interface table entry '%s' because it already exists" - % name) - return True - - -def delete_lag_interface(name, phys_ports, **kwargs): - """ - Perform a DELETE call to delete a LAG interface. For v1, also remove the LAG ID from the port's Interface, - and create the associated Port table entry. - - :param name: Alphanumeric name of LAG interface - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_lag_interface_v1(name, phys_ports, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_lag_interface(name, phys_ports, **kwargs) - - -def _delete_lag_interface_v1(name, phys_ports, **kwargs): - """ - Perform a DELETE call to delete a LAG interface. Also, for each physical port, create the associated Port table - entry, and remove the LAG ID from the Port and Interface entries by initializing them to default state. - - :param name: Alphanumeric name of LAG interface - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - success = interface.delete_interface(name, **kwargs) - - # For each port, create a Port table entry, then initialize the Port and Interface entries to remove LAG - for phys_port in phys_ports: - success = success and interface.add_l2_interface(phys_port, **kwargs) - success = success and port.initialize_port_entry(phys_port, **kwargs) - - return success - - -def _delete_lag_interface(name, phys_ports, **kwargs): - """ - Perform a DELETE call to delete a LAG interface. Also, for each physical port, remove the LAG ID from the Interface - entries by initializing them to default state. - - :param name: Alphanumeric name of LAG interface - :param phys_ports: List of physical ports to aggregate (e.g. ["1/1/1", "1/1/2", "1/1/3"]) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - success = interface.delete_interface(name, **kwargs) - - # For each port, initialize the Interface entry to remove LAG - for phys_port in phys_ports: - success = success and interface.initialize_interface_entry(phys_port, **kwargs) - - return success diff --git a/pyaoscx/lldp.py b/pyaoscx/lldp.py deleted file mode 100644 index a0f0621..0000000 --- a/pyaoscx/lldp.py +++ /dev/null @@ -1,231 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - - -from pyaoscx import common_ops, port, interface - -import json -import logging - - -def get_all_lldp_neighbors(**kwargs): - """ - Perform a GET call to get a list of all entries in the lldp_neighbors table. This is currently only supported in - v1, so even a v10.04 or later AOS-CX device will use the v1 REST call. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all lldp_neighors in the table - """ - target_url = kwargs["url"] + "system/interfaces/*/lldp_neighbors" - - if not kwargs["url"].endswith("/v1/"): - target_url = target_url.replace('v10.04', 'v1') - - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all lldp_neighbors entries failed with status code %d: %s" - % (response.status_code, response.text)) - lldp_neighbors_list = [] - else: - logging.info("SUCCESS: Getting list of all lldp_neighbors entries succeeded") - lldp_neighbors_list = response.json() - - return lldp_neighbors_list - - -def get_interface_lldp_neighbor_mac_port(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve lldp_neighbor MAC and port data for an Interface. This will return a list if using - v1, or a dictionary if using v10.04 or later. - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dictionary containing lldp_neighbor MAC and port data - """ - if kwargs["url"].endswith("/v1/"): - return _get_interface_lldp_neighbor_mac_port_v1(int_name, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_interface_lldp_neighbor_mac_port(int_name, depth, selector, **kwargs) - - -def _get_interface_lldp_neighbor_mac_port_v1(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve lldp_neighbor MAC and port data for an Interface - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List containing lldp_neighbor MAC and port data - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - target_url = kwargs["url"] + "system/interfaces/%s/lldp_neighbors" % int_name_percents - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting lldp_neighbor MAC and port data for interface '%s' " - "failed with status code %d: %s" % (int_name, response.status_code, response.text)) - result = {} - else: - logging.info("SUCCESS: Getting lldp_neighbor MAC and port data for interface '%s' succeeded" % int_name) - result = response.json() - - return result - - -def _get_interface_lldp_neighbor_mac_port(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve lldp_neighbor MAC and port data for an Interface - Note: Depth removed due to inconsistent behavior - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing lldp_neighbor MAC and port data - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - target_url = kwargs["url"] + "system/interfaces/%s/lldp_neighbors" % int_name_percents - payload = { - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting lldp_neighbor MAC and port data for interface '%s' " - "failed with status code %d: %s" % (int_name, response.status_code, response.text)) - result = {} - else: - logging.info("SUCCESS: Getting lldp_neighbor MAC and port data for interface '%s' succeeded" % int_name) - result = response.json() - - return result - - -def get_lldp_neighbor_info(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve LLDP neighbor info for an Interface - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing LLDP neighbor info - """ - if kwargs["url"].endswith("/v1/"): - return _get_lldp_neighbor_info_v1(int_name, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_lldp_neighbor_info(int_name, depth, selector, **kwargs) - - -def _get_lldp_neighbor_info_v1(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve LLDP neighbor info for an Interface - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing LLDP neighbor info - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - mac_port_info = get_interface_lldp_neighbor_mac_port(int_name, **kwargs) - port_info = mac_port_info[0][mac_port_info[0].rfind('/')+1:] # Retrieves port from end of URI - mac_port_info[0] = mac_port_info[0][:mac_port_info[0].rfind('/')] # Substring to remove the port from URI - mac_info = mac_port_info[0][mac_port_info[0].rfind('/')+1:] # Retrieves MAC from end of URI - - target_url = kwargs["url"] + "system/interfaces/%s/lldp_neighbors/%s/%s" % (int_name_percents, mac_info, port_info) - - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting LLDP Neighbor information for interface '%s' failed with status code %d: %s" - % (int_name, response.status_code, response.text)) - result = {} - else: - logging.info("SUCCESS: Getting LLDP Neighbor information for interface '%s' succeeded" % int_name) - result = response.json() - - return result - - -def _get_lldp_neighbor_info(int_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieveLLDP neighbor info for an Interface - - :param int_name: Alphanumeric name of the interface - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing LLDP neighbor info - """ - int_name_percents = common_ops._replace_special_characters(int_name) - - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - mac_port_info = get_interface_lldp_neighbor_mac_port(int_name, **kwargs) - unpacked_info = list(mac_port_info) # Unpacking dictionary to list - port_info = unpacked_info[0][unpacked_info[0].rfind(',')+1:] # Retrieves port from end of URI - unpacked_info[0] = unpacked_info[0][:unpacked_info[0].rfind(',')] # Substring to remove the port from URI - mac_info = unpacked_info[0][unpacked_info[0].rfind(',')+1:] # Retrieves MAC from end of URI - - target_url = kwargs["url"] + "system/interfaces/%s/lldp_neighbors/%s,%s" % (int_name_percents, mac_info, port_info) - - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=3) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting LLDP Neighbor information for interface '%s' failed with status code %d: %s" - % (int_name, response.status_code, response.text)) - result = {} - else: - logging.info("SUCCESS: Getting LLDP Neighbor information for interface '%s' succeeded" % int_name) - result = response.json() - - return result - diff --git a/pyaoscx/loop_protect.py b/pyaoscx/loop_protect.py deleted file mode 100644 index b0bb272..0000000 --- a/pyaoscx/loop_protect.py +++ /dev/null @@ -1,246 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops, interface, port - -import json -import logging - - -def update_port_loop_protect(interface_name, action=None, vlan_list=[], **kwargs): - """ - Perform GET and PUT calls to apply Loop-protect options on an interface. - - :param interface_name: Alphanumeric String that is the name of the interface that will apply loop-protect options - :param action: Alphanumeric String that will specify the actions for the Loop-protect interface. The options are - "do-not-disable", "tx-disable", "tx-rx-disable", or None. - :param vlan_list: List of VLANs that will be configured for Loop-protect on the interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if action not in ['do-not-disable', 'tx-disable', 'tx-rx-disable', None]: - raise Exception("ERROR: Action should be 'do-not-disable', 'tx-disable', 'tx-rx-disable' or None") - - if kwargs["url"].endswith("/v1/"): - return _update_port_loop_protect_v1(interface_name, action, vlan_list, **kwargs) - else: # Updated else for when version is v10.04 - return _update_port_loop_protect(interface_name, action, vlan_list, **kwargs) - - -def _update_port_loop_protect_v1(interface_name, action="", vlan_list=[], **kwargs): - """ - Perform GET and PUT calls to apply Loop-protect options on an interface. Note that Loop-protect requires that - the interface is L2, so this function will also update the interface to reflect that. - - :param interface_name: Alphanumeric String that is the name of the interface that will apply loop-protect options - :param action: Alphanumeric String that will specify the actions for the Loop-protect interface. The options are - "do-not-disable", "tx-disable", "tx-rx-disable", or None. - :param vlan_list: List of VLANs that will be configured for Loop-protect on the interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - port_data['loop_protect_enable'] = True - # make interface L2 - port_data['routing'] = False - - # strings appended to output prints for status - action_output = "" - vlan_output = "" - - if action not in ['do-not-disable', 'tx-disable', 'tx-rx-disable', None]: - raise Exception("ERROR: Action should be 'do-not-disable', 'tx-disable', 'tx-rx-disable' or None") - elif action: - port_data['loop_protect_action'] = action - action_output = " with Action %s " % action - - if vlan_list: - vlan_output = " with VLAN(s) [" - for vlan in vlan_list: - vlan_url = "/rest/v1/system/vlans/%s" % vlan - if vlan_url not in port_data['loop_protect_vlan']: - port_data['loop_protect_vlan'].append(vlan_url) - vlan_output += (" " + str(vlan)) - vlan_output += "] " - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying Loop-protect to Interface '%s'%s%s failed with status code %d: %s" - % (interface_name, action_output, vlan_output, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Loop-protect to Interface '%s'%s%s succeeded" - % (interface_name, action_output, vlan_output)) - return True - - -def _update_port_loop_protect(interface_name, action="", vlan_list=[], **kwargs): - """ - Perform GET and PUT calls to apply Loop-protect options on an interface. Note that Loop-protect requires that - the interface is L2, so this function will also update the interface to reflect that. - - :param interface_name: Alphanumeric String that is the name of the interface that will apply loop-protect options - :param action: Alphanumeric String that will specify the actions for the Loop-protect interface. The options are - "do-not-disable", "tx-disable", "tx-rx-disable", or None. - :param vlan_list: List of VLANs that will be configured for Loop-protect on the interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(interface_name) - int_data = interface.get_interface(int_name_percents, depth=1, selector="writable", **kwargs) - - if interface_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - if int_data['vlan_trunks']: - int_data['vlan_trunks'] = common_ops._dictionary_to_list_values(int_data['vlan_trunks']) - if int_data['loop_protect_vlan']: - int_data['loop_protect_vlan'] = common_ops._dictionary_to_list_values(int_data['loop_protect_vlan']) - - int_data['loop_protect_enable'] = True - # make interface L2 - int_data['routing'] = False - - # strings appended to output prints for status - action_output = "" - vlan_output = "" - - if action not in ['do-not-disable', 'tx-disable', 'tx-rx-disable', None]: - raise Exception("ERROR: Action should be 'do-not-disable', 'tx-disable', 'tx-rx-disable' or None") - elif action: - int_data['loop_protect_action'] = action - action_output = " with Action %s " % action - - if vlan_list: - vlan_output = " with VLAN(s) [" - for vlan in vlan_list: - vlan_url = "/rest/v10.04/system/vlans/%s" % vlan - if vlan_url not in int_data['loop_protect_vlan']: - int_data['loop_protect_vlan'].append(vlan_url) - vlan_output += (str(vlan) + " ") - vlan_output += "] " - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying Loop-protect to Interface '%s'%s%s failed with status code %d: %s" - % (interface_name, action_output, vlan_output, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Loop-protect to Interface '%s'%s%s succeeded" - % (interface_name, action_output, vlan_output)) - return True - - -def clear_port_loop_protect(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's Loop-protect settings - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _clear_port_loop_protect_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _clear_port_loop_protect(port_name, **kwargs) - - -def _clear_port_loop_protect_v1(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's Loop-protect settings - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data.pop('loop_protect_enable', None) - port_data.pop('loop_protect_action', None) - port_data['loop_protect_vlan'] = [] - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing Loop-protect options on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing the Loop-protect options on Port '%s' succeeded" - % (port_name)) - return True - - -def _clear_port_loop_protect(interface_name, **kwargs): - """ - Perform GET and PUT calls to clear an Interface's Loop-protect settings - - :param interface_name: Alphanumeric name of the Interface - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - int_name_percents = common_ops._replace_special_characters(interface_name) - - int_data = interface.get_interface(interface_name, depth=1, selector="writable", **kwargs) - - if interface_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - if int_data['vlan_trunks']: - int_data['vlan_trunks'] = common_ops._dictionary_to_list_values(int_data['vlan_trunks']) - - int_data['loop_protect_enable'] = None - int_data['loop_protect_action'] = "tx-disable" - int_data['loop_protect_vlan'] = [] - - target_url = kwargs["url"] + "system/interfaces/%s" % int_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing Loop-protect options on Interface '%s' failed with status code %d: %s" - % (interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing the Loop-protect options on Interface '%s' succeeded" - % (interface_name)) - return True diff --git a/pyaoscx/mac.py b/pyaoscx/mac.py deleted file mode 100644 index 46e2aa1..0000000 --- a/pyaoscx/mac.py +++ /dev/null @@ -1,78 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import logging - - -def get_all_mac_addrs(vlan_id, **kwargs): - """ - Perform a GET call to get MAC address(es) for VLAN - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of MAC address URIs - """ - target_url = kwargs["url"] + "system/vlans/%d/macs" % vlan_id - - response = kwargs["s"].get(target_url, verify=False) - mac_data = [] - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting MAC address(es) of VLAN ID '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting MAC address(es) of VLAN ID '%d' succeeded" % vlan_id) - mac_data = response.json() - return mac_data - - -def get_mac_info(vlan_id, mac_type, mac_addr, **kwargs): - """ - Perform a GET call to get MAC info - - :param vlan_id: Numeric ID of VLAN - :param mac_type: The source of the MAC address. Must be "dynamic," "VSX," "static," "VRRP," - "port-access-security," "evpn," or "hsc" - :param mac_addr: MAC address - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing MAC data - """ - target_url = kwargs["url"] + "system/vlans/%d/macs/%s/%s" % (vlan_id, mac_type, mac_addr) - - response = kwargs["s"].get(target_url, verify=False) - mac_data = [] - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting data for MAC '%s' of VLAN ID '%d' failed with status code %d: %s" - % (mac_addr, vlan_id, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting data for MAC '%s' of VLAN ID '%d' succeeded" % (mac_addr, vlan_id)) - mac_data = response.json() - return mac_data - - -def get_all_mac_addresses_on_system(**kwargs): - """ - Perform a GET call to get a list of all of the MAC address URIs for all VLANs on the system - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all MAC address URIs on the system - """ - target_url = kwargs["url"] + "system/vlans/*/macs" - response = kwargs["s"].get(target_url, verify=False) - - mac_list = [] - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting a list of all MAC addresses on the system, failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting a list of all MAC addresses on the system succeeded") - mac_list = response.json() - return mac_list diff --git a/pyaoscx/ospf.py b/pyaoscx/ospf.py deleted file mode 100644 index 1017624..0000000 --- a/pyaoscx/ospf.py +++ /dev/null @@ -1,1050 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops, interface, port - -import json -import logging - - -def get_ospf_routers(vrf, **kwargs): - """ - Perform a GET call to get a list of all OSPF Router IDs - - :param vrf: Alphanumeric name of the VRF that we are retrieving all Router IDs from - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all OSPF Router IDs in the table - """ - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers" % vrf - - response = kwargs["s"].get(target_url, verify=False) - - ospf_list = [] - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all OSPF Router IDs failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all OSPF Router IDs succeeded") - ospf_list = response.json() - - return ospf_list - - -def create_ospf_id(vrf, ospf_id, redistribute=["connected", "static"], **kwargs): - """ - Perform a POST call to create an OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param redistribute: List of types of redistribution methods for the OSPF Process, with the options being "bgp", - "connected", and "static" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ospf_data = { - "instance_tag": ospf_id, - "redistribute": redistribute - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers" % vrf - - post_data = json.dumps(ospf_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPF ID '%s' on vrf %s failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPF ID '%s' succeeded on vrf %s" % (ospf_id, vrf)) - return True - - -def create_ospf_area(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPF Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_ospf_area_v1(vrf, ospf_id, area_id, area_type, **kwargs) - else: # Updated else for when version is v10.04 - return _create_ospf_area(vrf, ospf_id, area_id, area_type, **kwargs) - - -def _create_ospf_area_v1(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPF Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - area_data = { - "area_id": area_id, - "area_type": area_type, - "ipsec_ah": {}, - "ipsec_esp": {}, - "ospf_interfaces": {}, - "ospf_vlinks": {}, - "other_config": {} - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s/areas" % (vrf, ospf_id) - - post_data = json.dumps(area_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPF Area '%s' on OSPF ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPF Area '%s' succeeded on OSPF ID %s" % (area_id, ospf_id)) - return True - - -def _create_ospf_area(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPF Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - area_data = { - "area_id": area_id, - "area_type": area_type, - "ipsec_ah": {}, - "ipsec_esp": {}, - "other_config": { - "stub_default_cost": 1, - "stub_metric_type": "metric_non_comparable" - } - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s/areas" % (vrf, ospf_id) - - post_data = json.dumps(area_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPF Area '%s' on OSPF ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPF Area '%s' succeeded on OSPF ID %s" % (area_id, ospf_id)) - return True - - -def create_ospf_interface(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPF area. - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_ospf_interface_v1(vrf, ospf_id, area_id, interface_name, **kwargs) - else: # Updated else for when version is v10.04 - return _create_ospf_interface(vrf, ospf_id, area_id, interface_name, **kwargs) - - -def _create_ospf_interface_v1(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPF area. - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - port_uri = '/rest/v1/system/ports/' + port_name_percents - - port_data = { - "interface_name": interface_name, - "port": port_uri - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s/areas/%s/ospf_interfaces" % (vrf, ospf_id, area_id) - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Applying Interface '%s' to OSPF ID '%s' failed with status code %d: %s" - % (interface_name, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Interface '%s' to OSPF ID '%s' succeeded" - % (interface_name, ospf_id)) - return True - - -def _create_ospf_interface(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPF area. - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - port_uri = '/rest/v10.04/system/interfaces/' + interface_name_percents - - interface_data = { - "interface_name": interface_name, - "port": port_uri - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s/areas/%s/ospf_interfaces" % (vrf, ospf_id, area_id) - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Applying Interface '%s' to OSPF ID '%s' failed with status code %d: %s" - % (interface_name, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Interface '%s' to OSPF ID '%s' succeeded" - % (interface_name, ospf_id)) - return True - - -def update_ospf_interface_authentication(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPF to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_ospf_interface_authentication_v1(vrf, ospf_id, interface_name, auth_type, - digest_key, auth_pass, **kwargs) - else: # Updated else for when version is v10.04 - return _update_ospf_interface_authentication(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs) - - -def _update_ospf_interface_authentication_v1(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPF to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - port_name_percents = common_ops._replace_special_characters(interface_name) - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - port.add_l3_ipv4_port(interface_name, vrf=vrf, **kwargs) - - port_data = port.get_port(interface_name, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - port_data['ospf_auth_type'] = auth_type - port_data['ospf_auth_md5_keys'] = {str(digest_key): auth_pass} - port_data['ospf_if_type'] = "ospf_iftype_broadcast" - port_data['routing'] = True - port_data['vrf'] = "/rest/v1/system/vrfs/" + vrf - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPF %s Authentication for Port '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPF %s Authentication for Port '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def _update_ospf_interface_authentication(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPF to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = interface.get_interface(interface_name, depth=1, selector="writable", **kwargs) - - interface_data['ospf_auth_type'] = auth_type - interface_data['ospf_auth_md5_keys'] = {str(digest_key): auth_pass} - interface_data['ospf_if_type'] = "ospf_iftype_broadcast" - interface_data['routing'] = True - interface_data['vrf'] = "/rest/v10.04/system/vrfs/%s" % vrf - - target_url = kwargs["url"] + "system/interfaces/%s" % interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPF %s Authentication for Port '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPF %s Authentication for Port '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def update_ospf_interface_type(vrf, ospf_id, interface_name, interface_type="pointtopoint", **kwargs): - """ - Perform PUT calls to update the type of OSPFv2 Interface given, as well as enable routing on the interface - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param interface_type: Alphanumeric type of OSPF interface. The options are 'broadcast', 'loopback', 'nbma', - 'none', 'pointomultipoint', 'pointopoint', and 'virtuallink' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if interface_type not in ['broadcast', 'loopback', 'statistics', 'nbma', 'pointomultipoint', - 'pointopoint', 'virtuallink', None]: - raise Exception("ERROR: Incorrect value for interface type. The options are 'broadcast', 'loopback', 'nbma', " - "'none', 'pointomultipoint', 'pointopoint', and 'virtuallink'") - if kwargs["url"].endswith("/v1/"): - return _update_ospf_interface_type_v1(vrf, ospf_id, interface_name, interface_type, **kwargs) - else: # Updated else for when version is v10.04 - return _update_ospf_interface_type(vrf, ospf_id, interface_name, interface_type, **kwargs) - - -def _update_ospf_interface_type_v1(vrf, ospf_id, interface_name, interface_type, **kwargs): - """ - Perform PUT calls to update the type of OSPFv2 Interface given, as well as enable routing on the interface - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param interface_type: Alphanumeric type of OSPF interface. The options are 'broadcast', 'loopback', 'nbma', - 'none', 'pointomultipoint', 'pointopoint', and 'virtuallink' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - port_name_percents = common_ops._replace_special_characters(interface_name) - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - port.add_l3_ipv4_port(interface_name, vrf=vrf, **kwargs) - - port_data = port.get_port(interface_name, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - port_data['ospf_if_type'] = "ospf_iftype_%s" % interface_type - port_data['routing'] = True - port_data['vrf'] = "/rest/v1/system/vrfs/" + vrf - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPF %s interface type for Port '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPF %s interface type for Port '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def _update_ospf_interface_type(vrf, ospf_id, interface_name, interface_type, **kwargs): - """ - Perform PUT calls to update the type of OSPFv2 Interface given, as well as enable routing on the interface - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPF area - :param interface_type: Alphanumeric type of OSPF interface. The options are 'broadcast', 'loopback', 'nbma', - 'none', 'pointomultipoint', 'pointopoint', and 'virtuallink' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = interface.get_interface(interface_name, depth=1, selector="writable", **kwargs) - - interface_data['ospf_if_type'] = "ospf_iftype_%s" % interface_type - interface_data['routing'] = True - interface_data['vrf'] = "/rest/v10.04/system/vrfs/" + vrf - - target_url = kwargs["url"] + "system/interfaces/%s" % interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPF %s interface type for Interface '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPF %s interface type for Interface '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def delete_ospf_id(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPF Router ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_ospf_id_v1(vrf, ospf_id, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_ospf_id(vrf, ospf_id, **kwargs) - - -def _delete_ospf_id_v1(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPF Router ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ospf_list = get_ospf_routers(vrf, **kwargs) - ospf_uri = "/rest/v1/system/vrfs/%s/ospf_routers/%s" % (vrf, ospf_id) - ospf_id_key = str(ospf_id) - - if ospf_id_key in ospf_list and ospf_uri == ospf_list[ospf_id_key]: - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s" % (vrf, ospf_id) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPF ID '%s' on VRF '%s' failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPF ID %s on VRF '%s' succeeded" % (ospf_id, vrf)) - return True - else: - logging.info("SUCCESS: No need to delete OSPF ID %s on VRF '%s' since it doesn't exist" - % (ospf_id, vrf)) - return True - - -def _delete_ospf_id(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPF Router ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ospf_list = get_ospf_routers(vrf, **kwargs) - ospf_uri = "/rest/v10.04/system/vrfs/%s/ospf_routers/%s" % (vrf, ospf_id) - ospf_id_key = str(ospf_id) - - if ospf_id_key in ospf_list and ospf_uri == ospf_list[ospf_id_key]: - - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s" % (vrf, ospf_id) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPF ID '%s' on VRF '%s' failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPF ID %s on VRF '%s' succeeded" % (ospf_id, vrf)) - return True - else: - logging.info("SUCCESS: No need to delete OSPF ID %s on VRF '%s' since it doesn't exist" - % (ospf_id, vrf)) - return True - - -def delete_ospf_area(vrf, ospf_id, area_id, **kwargs): - """ - Perform a DETELE call to remove an OSPF Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - target_url = kwargs["url"] + "system/vrfs/%s/ospf_routers/%s/areas/%s" % (vrf, ospf_id, area_id) - - response = kwargs["s"].delete(target_url, verify=False, timeout=2) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPF Area '%s' on OSPF ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPF Area '%s' succeeded on OSPF ID %s" % (area_id, ospf_id)) - return True - - -def get_ospfv3_routers(vrf, **kwargs): - """ - Perform a GET call to get a list of all OSPFv3 Router IDs - - :param vrf: Alphanumeric name of the VRF that we are retrieving all Router IDs from - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all OSPFv3 Router IDs in the table - """ - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers" % vrf - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all OSPFv3 Router IDs failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all OSPFv3 Router IDs succeeded") - - ospfv3_list = response.json() - return ospfv3_list - - -def create_ospfv3_id(vrf, ospf_id, **kwargs): - """ - Perform a POST call to create an OSPFv3 ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPF process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ospf_data = { - "instance_tag": ospf_id - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers" % vrf - - post_data = json.dumps(ospf_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPFv3 ID '%s' on VRF %s failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPFv3 ID '%s' succeeded on VRF %s" % (ospf_id, vrf)) - return True - - -def create_ospfv3_area(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPFv3 Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_ospfv3_area_v1(vrf, ospf_id, area_id, area_type, **kwargs) - else: # Updated else for when version is v10.04 - return _create_ospfv3_area(vrf, ospf_id, area_id, area_type, **kwargs) - - -def _create_ospfv3_area_v1(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPFv3 Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - area_data = { - "area_id": area_id, - "area_type": area_type, - "ipsec_ah": {}, - "ipsec_esp": {}, - "ospf_interfaces": {}, - "ospf_vlinks": {}, - "other_config": {} - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s/areas" % (vrf, ospf_id) - - post_data = json.dumps(area_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPFv3 Area '%s' on OSPFv3 ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPFv3 Area '%s' succeeded on OSPFv3 ID %s" % (area_id, ospf_id)) - return True - - -def _create_ospfv3_area(vrf, ospf_id, area_id, area_type='default', **kwargs): - """ - Perform a POST call to create an OSPFv3 Area for the specified OSPF ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param area_type: Alphanumeric defining how the external routing and summary LSAs for this area will be handled. - Options are "default","nssa","nssa_no_summary","stub","stub_no_summary" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - area_data = { - "area_id": area_id, - "area_type": area_type, - "ipsec_ah": {}, - "ipsec_esp": {}, - "other_config": { - "stub_default_cost": 1, - "stub_metric_type": "metric_non_comparable" - } - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s/areas" % (vrf, ospf_id) - - post_data = json.dumps(area_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating OSPFv3 Area '%s' on OSPFv3 ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating OSPFv3 Area '%s' succeeded on OSPFv3 ID %s" % (area_id, ospf_id)) - return True - - -def create_ospfv3_interface(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPFv3 area. - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_ospfv3_interface_v1(vrf, ospf_id, area_id, interface_name, **kwargs) - else: # Updated else for when version is v10.04 - return _create_ospfv3_interface(vrf, ospf_id, area_id, interface_name, **kwargs) - - -def _create_ospfv3_interface_v1(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPFv3 area. - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(interface_name) - port_uri = '/rest/v1/system/ports/' + port_name_percents - - port_data = { - "interface_name": interface_name, - "port": port_uri - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s/areas/%s/ospf_interfaces" % (vrf, ospf_id, area_id) - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Applying Interface '%s' to OSPFv3 ID '%s' failed with status code %d: %s" - % (interface_name, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Interface '%s' to OSPFv3 ID '%s' succeeded" - % (interface_name, ospf_id)) - return True - - -def _create_ospfv3_interface(vrf, ospf_id, area_id, interface_name, **kwargs): - """ - Perform POST calls to attach an interface to an OSPFv3 area. - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - port_uri = '/rest/v10.04/system/interfaces/' + interface_name_percents - - interface_data = { - "interface_name": interface_name, - "port": port_uri - } - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s/areas/%s/ospf_interfaces" % (vrf, ospf_id, area_id) - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Applying Interface '%s' to OSPFv3 ID '%s' failed with status code %d: %s" - % (interface_name, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying Interface '%s' to OSPFv3 ID '%s' succeeded" - % (interface_name, ospf_id)) - return True - - -def update_ospfv3_interface_authentication(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPFv3 to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_ospfv3_interface_authentication_v1(vrf, ospf_id, interface_name, auth_type, - digest_key, auth_pass, **kwargs) - else: # Updated else for when version is v10.04 - return _update_ospfv3_interface_authentication(vrf, ospf_id, interface_name, auth_type, - digest_key, auth_pass, **kwargs) - - -def _update_ospfv3_interface_authentication_v1(vrf, ospf_id, interface_name, auth_type, - digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPFv3 to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - port_name_percents = common_ops._replace_special_characters(interface_name) - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - port.add_l3_ipv4_port(interface_name, vrf=vrf, **kwargs) - - port_data = port.get_port(interface_name, depth=0, selector="configuration", **kwargs) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - port_data['ospf_auth_type'] = auth_type - port_data['ospf_auth_md5_keys'] = {str(digest_key): auth_pass} - port_data['ospf_if_type'] = "ospf_iftype_broadcast" - port_data['routing'] = True - port_data['vrf'] = "/rest/v1/system/vrfs/" + vrf - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPFv3 %s Authentication for Port '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPFv3 %s Authentication for Port '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def _update_ospfv3_interface_authentication(vrf, ospf_id, interface_name, auth_type, digest_key, auth_pass, **kwargs): - """ - Perform PUT calls to update an Interface with OSPFv3 to have authentication - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param interface_name: Alphanumeric name of the interface that will be attached to the OSPFv3 area - :param auth_type: Alphanumeric type of authentication, chosen between 'md5', 'null', and 'text' - :param digest_key: Integer between 1-255 that functions as the digest key for the authentication method - :param auth_pass: Alphanumeric text for the authentication password. Note that this will be translated to a - base64 String in the configuration and json. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - interface_name_percents = common_ops._replace_special_characters(interface_name) - - interface_data = interface.get_interface(interface_name, 2, "writable", **kwargs) - - interface_data['ospf_auth_type'] = auth_type - interface_data['ospf_auth_md5_keys'] = {digest_key: auth_pass} - interface_data['ospf_if_type'] = "ospf_iftype_broadcast" - interface_data['routing'] = True - interface_data['vrf'] = "/rest/v10.04/system/vrfs/" + vrf - - target_url = kwargs["url"] + "system/interfaces/%s" % interface_name_percents - put_data = json.dumps(interface_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating OSPFv3 %s Authentication for Port '%s' failed with status code %d: %s" - % (ospf_id, interface_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating OSPFv3 %s Authentication for Port '%s' succeeded" % (ospf_id, interface_name)) - return True - - -def delete_ospfv3_id(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPFv3 Router ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_ospfv3_id_v1(vrf, ospf_id, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_ospfv3_id(vrf, ospf_id, **kwargs) - - -def _delete_ospfv3_id_v1(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPFv3 Router ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ospf_list = get_ospfv3_routers(vrf, **kwargs) - ospf_uri = "/rest/v1/system/vrfs/%s/ospfv3_routers/%s" % (vrf, ospf_id) - ospf_id_key = str(ospf_id) - - if ospf_id_key in ospf_list and ospf_uri == ospf_list[ospf_id_key]: - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s" % (vrf, ospf_id) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPFv3 ID '%s' on VRF '%s' failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPFv3 ID %s on VRF '%s' succeeded" % (ospf_id, vrf)) - return True - else: - logging.info("SUCCESS: No need to delete OSPFv3 ID %s on VRF '%s' since it doesn't exist" - % (ospf_id, vrf)) - return True - - -def _delete_ospfv3_id(vrf, ospf_id, **kwargs): - """ - Perform a DELETE call to delete an OSPFv3 Router ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ospf_list = get_ospfv3_routers(vrf, **kwargs) - ospf_uri = "/rest/v10.04/system/vrfs/%s/ospfv3_routers/%s" % (vrf, ospf_id) - ospf_id_key = str(ospf_id) - - if ospf_id_key in ospf_list and ospf_uri == ospf_list[ospf_id_key]: - - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s" % (vrf, ospf_id) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPFv3 ID '%s' on VRF '%s' failed with status code %d: %s" - % (ospf_id, vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPFv3 ID %s on VRF '%s' succeeded" % (ospf_id, vrf)) - return True - else: - logging.info("SUCCESS: No need to delete OSPFv3 ID %s on VRF '%s' since it doesn't exist" - % (ospf_id, vrf)) - return True - - -def delete_ospfv3_area(vrf, ospf_id, area_id, **kwargs): - """ - Perform a DELETE call to remove an OSPFv3 Area for the specified OSPFv3 ID - - :param vrf: Alphanumeric name of the VRF the OSPFv3 ID belongs to - :param ospf_id: OSPFv3 process ID between numbers 1-63 - :param area_id: Unique identifier as a string in the form of x.x.x.x - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - target_url = kwargs["url"] + "system/vrfs/%s/ospfv3_routers/%s/areas/%s" % (vrf, ospf_id, area_id) - - response = kwargs["s"].delete(target_url, verify=False, timeout=2) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting OSPFv3 Area '%s' on OSPFv3 ID %s failed with status code %d: %s" - % (area_id, ospf_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting OSPFv3 Area '%s' succeeded on OSPFv3 ID %s" % (area_id, ospf_id)) - return True diff --git a/pyaoscx/ospf_area.py b/pyaoscx/ospf_area.py new file mode 100644 index 0000000..b7ef05f --- /dev/null +++ b/pyaoscx/ospf_area.py @@ -0,0 +1,435 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.ospf_interface import OspfInterface +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class OspfArea(PyaoscxModule): + ''' + Provide configuration management for OSPF Area instance on AOS-CX devices. + ''' + + indices = ['area_id'] + resource_uri_name = 'areas' + + # Use to manage references + ospf_interfaces = ListDescriptor('ospf_interfaces') + + def __init__(self, session, area_id, parent_ospf_router, uri=None, + **kwargs): + + self.session = session + # Assign ID + self.area_id = area_id + # Assign parent OSPF Router + self.__set_ospf_router(parent_ospf_router) + self._uri = uri + # List used to determine attributes related to the OPSF configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Use to manage OSPF Interfaces + self.ospf_interfaces = [] + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_ospf_router(self, parent_ospf_router): + ''' + Set parent OSPF Router as an attribute for the OspfArea object + :param parent_ospf_router a OspfRouter object where OSPF Area + is stored + ''' + + # Set parent OSPF router + self.__parent_ospf_router = parent_ospf_router + + # Set URI + self.base_uri = \ + '{base_ospf_router_uri}/{ospf_router_instance_tag}/areas'.format( + base_ospf_router_uri=self.__parent_ospf_router.base_uri, + ospf_router_instance_tag=self.__parent_ospf_router.instance_tag) + + for ospf_area in self.__parent_ospf_router.areas: + if ospf_area.area_id == self.area_id: + # Make list element point to current object + ospf_area = self + else: + # Add self to OSPF Routers list in parent OSPF Router + self.__parent_ospf_router.areas.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a OSPF Area table entry and + fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch OSPF Areas") + + depth = self.session.api_version.default_depth if depth is None \ + else depth + selector = self.session.api_version.default_selector if selector is \ + None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.area_id + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Delete unwanted data + if 'ospf_interfaces' in data: + data.pop('ospf_interfaces') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the OSPF Area is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, data, 'config_attrs', ['area_id']) + + # Set original attributes + self.__original_attributes = data + + # Remove ID + if 'area_id' in self.__original_attributes: + self.__original_attributes.pop('area_id') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + # Clean areas + if self.ospf_interfaces == []: + # Set Areas if any + # Adds OSPF Interface to parent OSPF Area already + OspfInterface.get_all(self.session, self) + + return True + + @classmethod + def get_all(cls, session, parent_ospf_router): + ''' + Perform a GET call to retrieve all system OSPF Area inside a + OPSF Router, and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_router: parent OPSF Router object where OPSF Area is stored + :return: Dictionary containing OSPF Area IDs as keys and a OSPF + Area objects as values + ''' + + logging.info("Retrieving the switch OSPF Area") + + base_uri = \ + '{base_ospf_router_uri}/{ospf_router_instance_tag}/areas'.format( + base_ospf_router_uri=parent_ospf_router.base_uri, + ospf_router_instance_tag=parent_ospf_router.instance_tag) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + ospf_area_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create an OspfArea object + area_id, ospf_area = OspfArea.from_uri( + session, parent_ospf_router, uri) + # Load all OSPF Router data from within the Switch + ospf_area.get() + ospf_area_dict[area_id] = ospf_area + + return ospf_area_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + Ospf Area table entry. + Checks whether the OSPF Area exists in the switch + Calls self.update() if OSPF Area is being updated + Calls self.create() if a new OSPF Area is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + if not self.__parent_ospf_router.materialized: + self.__parent_ospf_router.apply() + + modified = False + + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing OSPF Area table entry + + :return modified: True if Object was modified and a PUT request was made + False otherwise + ''' + # Variable returned + modified = False + + ospf_area_data = {} + + ospf_area_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.area_id + ) + + # Compare dictionaries + if ospf_area_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(ospf_area_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update osf area table entry {} success".format( + self.area_id)) + # Set new original attributes + self.__original_attributes = ospf_area_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new OSPF Area + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + ''' + ospf_area_data = {} + + ospf_area_data = utils.get_attrs(self, self.config_attrs) + ospf_area_data['area_id'] = self.area_id + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(ospf_area_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding OSPF Area table entry {} succeeded".format( + self.area_id)) + + # Get all object's data + self.get() + # Object was created, thus modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete OSPF Area table entry. + + ''' + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.area_id + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete OSPF Area table entry {} succeeded".format( + self.area_id)) + + # Delete back reference from ospf_routers + for area in self.__parent_ospf_router.areas: + if area.area_id == self.area_id: + self.__parent_ospf_router.areas.remove(area) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_ospf_router, response_data): + ''' + Create an OspfArea object given a response_data related to the ospf + router ID object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_router: parent OspfRouter object where OspfArea object + is stored + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/vrfs//ospf_routers/ + instance_tag/areas/area_id" + } + or a + string: "/rest/v10.04/system/vrfs//ospf_routers/ + instance_tag/areas/area_id" + :return: OspfArea object + + ''' + ospf_area_arr = session.api_version.get_keys( + response_data, OspfArea.resource_uri_name) + ospf_area_id = ospf_area_arr[0] + return OspfArea(session, ospf_area_id, parent_ospf_router) + + @classmethod + def from_uri(cls, session, parent_ospf_router, uri): + ''' + Create an OspfArea object given a URI + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_router: parent OspfRouter object where OspfArea + object is stored + :param uri: a String with a URI + :return index, ospf_area_obj: tuple containing both the OspfArea object + and the OSPF Area's ID + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)areas/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create OspfArea object + ospf_area_obj = OspfArea( + session, index, parent_ospf_router, uri=uri) + + return index, ospf_area_obj + + def __str__(self): + return "OSPF Area ID {}".format(self.area_id) + + def get_uri(self): + ''' + Method used to obtain the specific OSPF Area URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{id}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + id=self.area_id + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/ospf_interface.py b/pyaoscx/ospf_interface.py new file mode 100644 index 0000000..ea61a3d --- /dev/null +++ b/pyaoscx/ospf_interface.py @@ -0,0 +1,436 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class OspfInterface(PyaoscxModule): + ''' + Provide configuration management for OSPF Interface on AOS-CX devices. + ''' + + indices = ['interface_name'] + resource_uri_name = 'ospf_interfaces' + + def __init__(self, session, interface_name, parent_ospf_area, uri=None, + **kwargs): + + self.session = session + # Assign ID + self.interface_name = interface_name + # Assign parent OspfArea object + self.__set_ospf_area(parent_ospf_area) + self._uri = uri + # List used to determine attributes related to the OSPF Interface + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_ospf_area(self, parent_ospf_area): + ''' + Set parent OspfArea object as an attribute for the OspfInterface object + :param parent_ospf_area: a OspfArea object + ''' + + # Set parent OspfArea object + self.__parent_ospf_area = parent_ospf_area + + # Set URI + self.base_uri = \ + '{base_ospf_area_uri}/{ospf_area_area_id}/ospf_interfaces'.format( + base_ospf_area_uri=self.__parent_ospf_area.base_uri, + ospf_area_area_id=self.__parent_ospf_area.area_id + ) + + for ospf_interface in self.__parent_ospf_area.ospf_interfaces: + if ospf_interface.interface_name == self.interface_name: + # Make list element point to current object + ospf_interface = self + else: + # Add self to OspfInterface objects list in parent OspfArea object + self.__parent_ospf_area.ospf_interfaces.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a OSPF Interfaces table entry + and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch OSPF Interface table entries") + + depth = self.session.api_version.default_depth if depth is None \ + else depth + selector = self.session.api_version.default_selector if selector is \ + None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.interface_name + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the OSPF Interfaces is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['interface_name']) + + # Set original attributes + self.__original_attributes = data + + # Remove ID + if 'interface_name' in self.__original_attributes: + self.__original_attributes.pop('interface_name') + + # If the OSPF Interface has a port inside the switch + if hasattr(self, 'port') and \ + self.port is not None: + port_response = self.port + interface_cls = self.session.api_version.get_module( + self.session, 'Interface', '') + # Set port as a Interface Object + self.port = interface_cls.from_response( + self.session, port_response) + self.port.get() + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_ospf_area): + ''' + Perform a GET call to retrieve all system OSPF Interfaces inside a + OSPF Area, and create a dictionary containing them as OspfInterface + objects + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_area: parent OspfArea object where OspfInterface object + is stored + :return: Dictionary containing OSPF Interface IDs as keys and a + OspfInterface objects as values + ''' + + logging.info("Retrieving the switch OSPF Interfaces of an OSPF area") + + base_uri = '{base_ospf_area_uri}/{ospf_area_area_id}/ospf_interfaces'.format( + base_ospf_area_uri=parent_ospf_area.base_uri, + ospf_area_area_id=parent_ospf_area.area_id) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + ospf_interface_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a OspfInterface object + interface_name, ospf_interface = OspfInterface.from_uri( + session, parent_ospf_area, uri) + # Load all OSPF Interfaces data from within the Switch + ospf_interface.get() + ospf_interface_dict[interface_name] = ospf_interface + + return ospf_interface_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + OSPF Interface table entry. + Checks whether the OSPF Interface exists in the switch + Calls self.update() if OSPF Interface being updated + Calls self.create() if a new OSPF Interface is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + if not self.__parent_ospf_area.materialized: + self.__parent_ospf_area.apply() + + modified = False + + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing OSPF Interface table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + ''' + # Modified variable + modified = False + + ospf_interface_data = {} + + ospf_interface_data = utils.get_attrs(self, self.config_attrs) + + # Get port uri + if self.port is not None: + ospf_interface_data["port"] = \ + self.port.get_info_format() + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.interface_name + ) + + # Compare dictionaries + if ospf_interface_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + + post_data = json.dumps( + ospf_interface_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update OSPF Interface table entry {} succeeded".format( + self.interface_name)) + # Set new original attributes + self.__original_attributes = ospf_interface_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new OSPF Interface table entry + Only returns if an exception is not raise + :return: True if OSPF Interface table entry was added + ''' + + ospf_interface_data = {} + + ospf_interface_data = utils.get_attrs(self, self.config_attrs) + ospf_interface_data['interface_name'] = self.interface_name + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(ospf_interface_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding OSPF Interface table entry {} succeeded".format( + self.interface_name)) + + # Get all object's data + self.get() + # Object was modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete OSPF Interface + + ''' + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.interface_name + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Delete OSPF Interface table\ + entry {} succeeded".format(self.interface_name)) + + # Delete back reference from ospf_areas + for ospf_interface in self.__parent_ospf_area.ospf_interfaces: + if ospf_interface.interface_name == self.interface_name: + self.__parent_ospf_area.ospf_interfaces.remove(ospf_interface) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_ospf_area, response_data): + ''' + Create a OspfInterface object given a response_data related to the + OSPF Area ID object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_area: parent OspfArea object where OspfInterface + object is stored + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/vrfs//ospf_routers/ + instance_tag/areas/area_id/ospf_interfaces/id" + } + or a + string: "/rest/v10.04/system/vrfs//ospf_routers/instance_tag/ + areas/area_id/ospf_interfaces/id" + :return: OspfInterface object + ''' + ospf_interface_arr = session.api_version.get_keys( + response_data, OspfInterface.resource_uri_name) + ospf_interface_name = ospf_interface_arr[0] + return OspfInterface(session, ospf_interface_name, parent_ospf_area) + + @classmethod + def from_uri(cls, session, parent_ospf_area, uri): + ''' + Create a OspfInterface object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_ospf_area: parent OspfArea object where OspfInterface + object is stored + + :return index, ospf_interface_obj: tuple containing both the + OspfInterface object and the OSPF Interface's ID + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)ospf_interfaces/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create OspfInterface object + ospf_interface_obj = OspfInterface( + session, index, parent_ospf_area, uri=uri) + + return index, ospf_interface_obj + + def __str__(self): + return "OSPF Interface ID {}".format(self.interface_name) + + def get_uri(self): + ''' + Method used to obtain the specific OSPF Interface uri + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{id}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + id=self.interface_name + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/ospf_router.py b/pyaoscx/ospf_router.py new file mode 100644 index 0000000..be917c6 --- /dev/null +++ b/pyaoscx/ospf_router.py @@ -0,0 +1,487 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.ospf_area import OspfArea +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class OspfRouter(PyaoscxModule): + ''' + Provide configuration management for an OSPF routing protocol on AOS-CX devices. + ''' + + indices = ['instance_tag'] + resource_uri_name = 'ospf_routers' + + # Use to manage references + areas = ListDescriptor('areas') + + def __init__(self, session, instance_tag, parent_vrf, uri=None, **kwargs): + + self.session = session + # Assign ID + self.instance_tag = instance_tag + # Assign parent Vrf object + self.__set_vrf(parent_vrf) + self._uri = uri + # List used to determine attributes related to the OSPF + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Use to manage Areas + self.areas = [] + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_vrf(self, parent_vrf): + ''' + Set parent Vrf object as an attribute for the OspfRouter object + :param parent_vrf a Vrf object + ''' + + # Set parent Vrf object + self.__parent_vrf = parent_vrf + + # Set URI + self.base_uri = '{base_vrf_uri}/{vrf_name}/ospf_routers'.format( + base_vrf_uri=self.__parent_vrf.base_uri, + vrf_name=self.__parent_vrf.name) + + # Verify OSPF Router instance doesn't exist already inside VRF + for ospf_router in self.__parent_vrf.ospf_routers: + if ospf_router.instance_tag == self.instance_tag: + # Make list element point to current object + ospf_router = self + else: + # Add self to ospf_routers list in parent Vrf object + self.__parent_vrf.ospf_routers.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a OSPF Router table entry and + fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch OSPF Router information") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{instance_tag}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + instance_tag=self.instance_tag + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + # Delete unwanted data + if 'areas' in data: + data.pop('areas') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the OSPF Router is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['instance_tag']) + + # Set original attributes + self.__original_attributes = data + + # Remove ID + if 'instance_tag' in self.__original_attributes: + self.__original_attributes.pop('instance_tag') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + # Set a list of passive_interfaces as an attribute + if hasattr(self, 'passive_interfaces') and \ + self.passive_interfaces is not None: + interfaces_list = [] + # Get all URI elements in the form of a list + uri_list = self.session.api_version.get_uri_from_data( + self.passive_interfaces) + + for uri in uri_list: + # Create an Interface object + name, interface = Interface.from_uri(self.session, uri) + + # Materialize interface + interface.get() + + # Add interface to list + interfaces_list.append(interface) + + # Set list as Interfaces + self.passive_interfaces = interfaces_list + + # Clean OSPF Area settings + if self.areas == []: + # Set Areas if any + # Adds Area to parent OspfRouter + OspfArea.get_all(self.session, self) + return True + + @classmethod + def get_all(cls, session, parent_vrf): + ''' + Perform a GET call to retrieve all system OSPF settings for a + given VRF, and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where OspfRouter object is stored + :return: Dictionary containing OSPF Router IDs as keys and a OspfRouter + objects as values + ''' + + logging.info("Retrieving the switch OSPF Router data") + + base_uri = '{base_vrf_uri}/{vrf_name}/ospf_routers'.format( + base_vrf_uri=parent_vrf.base_uri, + vrf_name=parent_vrf.name) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + ospf_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a OspfRouter object and adds it to parent Vrf object list + instance_tag, ospf = OspfRouter.from_uri(session, parent_vrf, uri) + # Load all OSPF Router data from within the Switch + ospf.get() + ospf_dict[instance_tag] = ospf + + return ospf_dict + + @connected + def apply(self): + ''' + Main method used to either create update an existing + OSPF Router Table Entry. + Checks whether the VRF exists in the switch + Calls self.update() if OSPF Router is being updated + Calls self.create() if a new OSPF Router is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + if not self.__parent_vrf.materialized: + self.__parent_vrf.apply() + + modified = False + + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing OSPF Router table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + ''' + + ospf_router_data = {} + + ospf_router_data = utils.get_attrs(self, self.config_attrs) + + # Set passive_interfaces into correct form + if hasattr(self, 'passive_interfaces') and \ + self.passive_interfaces is not None: + formated_interfaces = {} + + # Set interfaces into correct form + for element in self.passive_interfaces: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Interface {}'.format(element.name), + 'Object inside passive_interfaces not materialized') + formated_element = element.get_info_format() + formated_interfaces.update(formated_element) + + # Set values in correct form + ospf_router_data["passive_interfaces"] = formated_interfaces + + uri = "{base_url}{class_uri}/{instance_tag}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + instance_tag=self.instance_tag + ) + + # Compare dictionaries + if ospf_router_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(ospf_router_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update OSPF Router table entry {} succeeded\ + ".format( + self.instance_tag)) + # Set new original attributes + self.__original_attributes = ospf_router_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new OSPF Router table entry + Only returns if an exception is not raise + + :return modified: True if entry was created + + ''' + + ospf_router_data = {} + + ospf_router_data = utils.get_attrs(self, self.config_attrs) + ospf_router_data['instance_tag'] = self.instance_tag + + # Set passive_interfaces into correct form + if hasattr(self, 'passive_interfaces') \ + and self.passive_interfaces is not None: + formated_interfaces = {} + + # Set interfaces into correct form + for element in self.passive_interfaces: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Interface {}'.format(element.name), + 'Object inside passive_interfaces not materialized') + formated_element = element.get_info_format() + formated_interfaces.update(formated_element) + + # Set values in correct form + ospf_router_data["passive_interfaces"] = formated_interfaces + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(ospf_router_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Adding OSPF table entry {} succeeded\ + ".format(self.instance_tag)) + + # Get all object's data + self.get() + # Object was created + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete OSPF Router table entry. + + ''' + + uri = "{base_url}{class_uri}/{instance_tag}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + instance_tag=self.instance_tag + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete OSPF Router table entry {} succeeded".format( + self.instance_tag)) + + # Delete back reference from VRF + for ospf_router in self.__parent_vrf.ospf_routers: + if ospf_router.instance_tag == self.instance_tag: + self.__parent_vrf.ospf_routers.remove(ospf_router) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_vrf, response_data): + ''' + Create a OspfRouter object given a response_data related to the OspfRouter + object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where OspfRouter object is stored + :param response_data: The response can be either a + dictionary: { + instance_tag: "/rest/v10.04/system/vrfs/ospf_routers/ + instance_tag" + } + or a + string: "/rest/v10.04/system/vrfs/ospf_routers/instance_tag" + :return: OspfRouter object + ''' + ospf_arr = session.api_version.get_keys( + response_data, OspfRouter.resource_uri_name) + instance_tag = ospf_arr[0] + return OspfRouter(session, instance_tag, parent_vrf) + + @classmethod + def from_uri(cls, session, parent_vrf, uri): + ''' + Create a OspfRouter object given a URI + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where OspfRouter object is stored + :param uri: a String with a URI + + :return index, ospf_obj: tuple containing both the OspfRouter object and + the OSPF Router's instance_tag + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)ospf_routers/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create OspfRouter object + ospf_obj = OspfRouter(session, index, parent_vrf, uri=uri) + + return index, ospf_obj + + def __str__(self): + return "OSPF Router ID {}".format(self.instance_tag) + + def get_uri(self): + ''' + Method used to obtain the specific OSPF Router URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{instance_tag}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + instance_tag=self.instance_tag + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/port.py b/pyaoscx/port.py deleted file mode 100644 index e187a2b..0000000 --- a/pyaoscx/port.py +++ /dev/null @@ -1,998 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops, interface - -import json -import logging - - -def get_port(port_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for a Port table entry - - :param port_name: Alphanumeric name of the port - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - if kwargs["url"].endswith("/v1/"): - return _get_port_v1(port_name, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_port(port_name, depth, selector, **kwargs) - - -def _get_port_v1(port_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for a Port table entry - - :param port_name: Alphanumeric name of the port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - payload = { - "depth": depth, - "selector": selector - } - - port_name_percents = common_ops._replace_special_characters(port_name) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - port_name = common_ops._replace_percents(port_name_percents) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - output = {} - else: - logging.info("SUCCESS: Getting Port table entry '%s' succeeded" % port_name) - output = response.json() - - return output - - -def _get_port(port_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to retrieve data for a Port table entry - - :param port_name: Alphanumeric name of the port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - payload = { - "depth": depth, - "selector": selector - } - - port_name_percents = common_ops._replace_special_characters(port_name) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - port_name = common_ops._replace_percents(port_name_percents) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - output = {} - else: - logging.info("SUCCESS: Getting Port table entry '%s' succeeded" % port_name) - output = response.json() - - return output - - -def get_all_ports(**kwargs): - """ - Perform a GET call to get a list of all entries in the Port table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all ports in the table - """ - target_url = kwargs["url"] + "system/ports" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all Port table entries failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all Port table entries succeeded") - - ports_list = response.json() - return ports_list - - -def add_vlan_port(vlan_port_name, vlan_id, ipv4=None, vrf_name="default", vlan_port_desc=None, - port_admin_state="up", **kwargs): - """ - Perform a POST call to create a logical VLAN Port as part of SVI creation. - - :param vlan_port_name: Alphanumeric Port name - :param vlan_id: Numeric ID of VLAN - :param ipv4: Optional IPv4 address to assign to the interface.Defaults to nothing if not specified. - :param vrf_name: VRF to attach the SVI to. Defaults to "default" if not specified - :param vlan_port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param port_admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ports_list = get_all_ports(**kwargs) - - if "/rest/v1/system/ports/%s" % vlan_port_name not in ports_list: - vlan_port_data = {"admin": port_admin_state, - "name": vlan_port_name, - "vrf": "/rest/v1/system/vrfs/%s" % vrf_name, - "vlan_tag": "/rest/v1/system/vlans/%s" % vlan_id - } - - if vlan_port_desc is not None: - vlan_port_data['description'] = vlan_port_desc - - if ipv4 is not None: - vlan_port_data['ip4_address'] = ipv4 - - target_url = kwargs["url"] + "system/ports" - post_data = json.dumps(vlan_port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Port table entry '%s' for SVI failed with status code %d: %s" - % (vlan_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Port table entry '%s' for SVI succeeded" % vlan_port_name) - return True - else: - logging.info("SUCCESS: No need to create VLAN Port '%s' since it already exists" % vlan_port_name) - return True - - -def add_l2_port(port_name, port_desc=None, port_admin_state="up", **kwargs): - """ - Perform a POST call to create a Port table entry for physical L2 interface. If the Port table entry exists, this - function will perform a PUT call to update the entry with the given parameters. - - :param port_name: Alphanumeric Port name - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param port_admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = get_all_ports(**kwargs) - - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = { - "admin": port_admin_state, - "interfaces": ["/rest/v1/system/interfaces/%s" % port_name_percents], - "name": port_name, - "routing": False - } - - if port_desc is not None: - port_data['description'] = port_desc - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - target_url = kwargs["url"] + "system/ports" - payload_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=payload_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Port table entry '%s' succeeded" % port_name) - return True - else: - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - port_data.pop('name', None) # must remove this item from the json since name can't be modified - payload_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=payload_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating Port table entry '%s' succeeded" % port_name) - return True - - -def add_l3_ipv4_port(port_name, ip_address=None, port_desc=None, port_admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for a physical L3 interface. If the port already exists, the - function will enable routing on the port and update the IPv4 address if given. - - :param port_name: Alphanumeric Port name - :param ip_address: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param port_admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = get_all_ports(**kwargs) - - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = {"admin": port_admin_state, - "interfaces": ["/rest/v1/system/interfaces/%s" % port_name_percents], - "name": port_name, - "routing": True, - "vrf": "/rest/v1/system/vrfs/%s" % vrf - } - - if port_desc is not None: - port_data['description'] = port_desc - - if ip_address is not None: - port_data['ip4_address'] = ip_address - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - target_url = kwargs["url"] + "system/ports" - payload_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=payload_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding IPv4 Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding IPv4 Port table entry '%s' succeeded" % port_name) - return True - else: - return update_port_ipv4(port_name, ip_address, port_admin_state, vrf, **kwargs) - - -def update_port_ipv4(port_name, ipv4, port_admin_state, vrf, **kwargs): - """ - Perform GET and PUT calls to update an L3 interface's ipv4 address - - :param port_name: Alphanumeric name of the Port - :param ipv4: IPv4 address to associate with the VLAN Port - :param port_admin_state: Administratively-configured state of the port. - :param vrf: Name of the VRF to which the Port belongs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data['ip4_address'] = ipv4 - port_data['routing'] = True - port_data['admin'] = port_admin_state - port_data['vrf'] = "/rest/v1/system/vrfs/%s" % vrf - - port_data.pop('name', None) # must remove this item from the json since name can't be modified - port_data.pop('origin', None) # must remove this item from the json since origin can't be modified - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating IPv4 addresses for Port '%s' to '%s' failed with status code %d: %s" - % (port_name, repr(ipv4), response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating IPv4 addresses for Port '%s' to '%s' succeeded" - % (port_name, repr(ipv4))) - return True - - -def add_l3_ipv6_port(port_name, ip_address=None, port_desc=None, port_admin_state="up", vrf="default", **kwargs): - """ - Perform a POST call to create a Port table entry for a physical L3 interface. If the port already exists, the - function will perform a PUT call to update the Port table entry to enable routing on the port and update - the IPv6 address if given. - - :param port_name: Alphanumeric Port name - :param ip_address: IPv6 address to assign to the interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param port_admin_state: Optional administratively-configured state of the port. - Defaults to "up" if not specified - :param vrf: Name of the VRF to which the Port belongs. Defaults to "default" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = get_all_ports(**kwargs) - - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = { - "admin": port_admin_state, - "interfaces": ["/rest/v1/system/interfaces/%s" % port_name_percents], - "name": port_name, - "routing": True, - "vrf": "/rest/v1/system/vrfs/%s" % vrf - } - - if port_desc is not None: - port_data['description'] = port_desc - - if "/rest/v1/system/ports/%s" % port_name_percents not in ports_list: - target_url = kwargs["url"] + "system/ports" - payload_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=payload_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding L3 IPv6 Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding L3 IPv6 Port table entry '%s' succeeded" % port_name) - # IPv6 defaults - ipv6_data = { - "address": ip_address, - "node_address": True, - "origin": "configuration", - "ra_prefix": True, - "route_tag": 0, - "type": "global-unicast" - } - - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses" % port_name_percents - post_data = json.dumps(ipv6_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=post_data, verify=False) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Final configuration of L3 IPv6 Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Final configuration of L3 IPv6 Port table entry '%s' succeeded" % port_name) - return True - else: - port_data.pop('name', None) # must remove this item from the json since name can't be modified - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - payload_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=payload_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating L3 IPv6 Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating L3 IPv6 Port table entry '%s' succeeded" % port_name) - return update_port_ipv6(port_name, ip_address, **kwargs) - - -def update_port_ipv6(port_name, ip_address, addr_type="global-unicast", **kwargs): - """ - Perform a POST call to create an IPv6 address entry to update an L3 interface's ipv6 address - - :param port_name: Alphanumeric name of the Port - :param ipv6: IPv6 address to associate with the Port - :param addr_type: Type of IPv6 address. Defaults to "global-unicast" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - ipv6_data = { - "address": ip_address, - "node_address": True, - "origin": "configuration", - "ra_prefix": True, - "route_tag": 0, - "type": addr_type - } - - target_url = kwargs["url"] + "system/ports/%s/ip6_addresses" % port_name_percents - post_data = json.dumps(ipv6_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating IPv6 address to update Port '%s' to '%s' failed with status code %d: %s" - % (port_name, ip_address, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating IPv6 address to update Port '%s' to '%s' succeeded" - % (port_name, ip_address)) - return True - - -def delete_port(port_name, **kwargs): - """ - Perform a DELETE call to delete a Port table entry. - - :param port_name: Port table entry name - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - port_name_percents = common_ops._replace_special_characters(port_name) - - ports_list = get_all_ports(**kwargs) - - if "/rest/v1/system/ports/%s" % port_name_percents in ports_list: - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting Port '%s' succeeded" % port_name) - return True - else: - logging.info("SUCCESS: No need to remove Port '%s' since it doesn't exist" % port_name) - return True - - -def _delete_ipv6_address(port_name, ip, **kwargs): - """ - Perform a DELETE call to remove an IPv6 address from an Interface. - - :param port_name: Alphanumeric Interface name - :param ip: IPv6 address assigned to the interface that will be removed. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if ip in interface.get_ipv6_addresses(port_name, **kwargs): - port_name_percents = common_ops._replace_special_characters(port_name) - ip_address = common_ops._replace_special_characters(ip) - target_url = kwargs["url"] + "system/interfaces/%s/ip6_addresses/%s" % (port_name_percents, ip_address) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting IPv6 Address '%s' from Port table entry '%s' failed with status code %d: %s" - % (ip, port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting IPv6 Address '%s' from Port table entry '%s' succeeded" - % (ip, port_name)) - return True - else: - logging.info("SUCCESS: No need to delete IPv6 Address '%s' from Port table entry '%s' since it does not exist" - % (ip, port_name)) - return True - - -def _port_set_vlan_mode(l2_port_name, vlan_mode, **kwargs): - """ - Perform GET and PUT calls to set an L2 interface's VLAN mode (native-tagged, native-untagged, or access) - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_mode: A string, either 'native-tagged', 'native-untagged', or 'access', specifying the desired VLAN - mode - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if vlan_mode not in ['native-tagged', 'native-untagged', 'access']: - raise Exception("ERROR: VLAN mode should be 'native-tagged', 'native-untagged', or 'access'") - - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - port_data = get_port(l2_port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - port_data['vlan_mode'] = vlan_mode - port_data['routing'] = False - - target_url = kwargs["url"] + "system/ports/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting port '%s' VLAN mode to '%s' failed with status code %d: %s" - % (l2_port_name, vlan_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting port '%s' VLAN mode to '%s' succeeded" % (l2_port_name, vlan_mode)) - return True - - -def _port_set_untagged_vlan(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN on an access port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to set on access port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - port_data = get_port(l2_port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - port_data['vlan_mode'] = "access" - port_data['vlan_tag'] = "/rest/v1/system/vlans/%s" % vlan_id - port_data['routing'] = False - - target_url = kwargs["url"] + "system/ports/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not (common_ops._response_ok(response, "PUT") or common_ops._response_ok(response, "POST")): - logging.warning("FAIL: Setting Port '%s' access VLAN to VLAN ID '%d' failed with status code %d: %s" - % (l2_port_name, vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting Port '%s' access VLAN to VLAN ID '%d' succeeded" - % (l2_port_name, vlan_id)) - return True - - -def _port_add_vlan_trunks(l2_port_name, vlan_trunk_ids=[], **kwargs): - """ - Perform GET and PUT/POST calls to add specified VLANs to a trunk port. By default, this will also set the port to - have 'no routing' and if there is not a native VLAN, will set the native VLAN to VLAN 1. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_trunk_ids: List of VLANs to specify as allowed on the trunk port. If empty, the interface will - allow all VLANs on the trunk. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - # need to create port resource for physical port if it doesn't exist - ports_list = get_all_ports(**kwargs) - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - trunk_list = [] - for x in vlan_trunk_ids: - trunk_list.append("/rest/v1/system/vlans/%d" % x) - - if "/rest/v1/system/ports/%s" % l2_port_name_percents not in ports_list: - # if Port table entry doesn't exist, create it - port_data = {"name": l2_port_name, - "interfaces": - [ - "/rest/v1/system/interfaces/%s" % l2_port_name_percents - ], - "vlan_mode": "native-untagged", - "vlan_tag": "/rest/v1/system/vlans/1", - "vlan_trunks": trunk_list, - "routing": False - } - - target_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - else: - # otherwise just update the physical port - port_data = get_port(l2_port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - if 'vlan_tag' not in port_data: - port_data['vlan_tag'] = "/rest/v1/system/vlans/1" - if 'vlan_mode' not in port_data: - port_data['vlan_mode'] = "native-untagged" - port_data['routing'] = False - - if not trunk_list: - port_data['vlan_trunks'] = [] - else: - for y in trunk_list: - if y not in port_data['vlan_trunks']: - port_data['vlan_trunks'].append(y) - - target_url = kwargs["url"] + "system/ports/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not (common_ops._response_ok(response, "PUT") or common_ops._response_ok(response, "POST")): - logging.warning("FAIL: Adding VLANs '%s' to Port '%s' trunk failed with status code %d: %s" - % (vlan_trunk_ids, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VLANs '%s' to Port '%s' trunk succeeded" - % (vlan_trunk_ids, l2_port_name)) - return True - - -def _port_set_native_vlan(l2_port_name, vlan_id, tagged=True, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN to be the native VLAN on the trunk. Also gives the option to set - the VLAN as tagged. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to add to trunk port - :param tagged: Boolean to determine if the native VLAN will be set as the tagged VLAN. If False, the VLAN - will be set as the native untagged VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if tagged: - vlan_mode = "native-tagged" - else: - vlan_mode = "native-untagged" - - # need to create port resource for physical port if it doesn't exist - ports_list = get_all_ports(**kwargs) - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - if "/rest/v1/system/ports/%s" % l2_port_name_percents not in ports_list: - # if Port table entry doesn't exist, create it - port_data = {"name": l2_port_name, - "interfaces": - [ - "/rest/v1/system/interfaces/%s" % l2_port_name_percents - ], - "vlan_mode": vlan_mode, - "vlan_tag": "/rest/v1/system/vlans/%s" % vlan_id, - "vlan_trunks": [], - "routing": False - } - - target_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - else: - # otherwise just update the physical port - port_data = get_port(l2_port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - port_data['vlan_tag'] = "/rest/v1/system/vlans/%s" % vlan_id - port_data['routing'] = False - port_data['vlan_mode'] = vlan_mode - - if (port_data['vlan_trunks']) and ("/rest/v1/system/vlans/%s" % vlan_id not in port_data['vlan_trunks']): - port_data['vlan_trunks'].append("/rest/v1/system/vlans/%s" % vlan_id) - - target_url = kwargs["url"] + "system/ports/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not (common_ops._response_ok(response, "PUT") or common_ops._response_ok(response, "POST")): - logging.warning("FAIL: Setting native VLAN ID '%d' to Port '%s' failed with status code %d: %s" - % (vlan_id, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting native VLAN ID '%d' to Port '%s' succeeded" - % (vlan_id, l2_port_name)) - return True - - -def _delete_vlan_port(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT calls to remove a VLAN from a trunk port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to remove from trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - l2_port_name_percents = common_ops._replace_special_characters(l2_port_name) - - port_data = get_port(l2_port_name, depth=0, selector="configuration", **kwargs) - - port_data.pop('name', None) - port_data.pop('origin', None) - port_data.pop('vrf', None) - - if "/rest/v1/system/vlans/%s" % vlan_id in port_data['vlan_trunks']: - # remove vlan from 'vlan_trunks' - port_data['vlan_trunks'].remove("/rest/v1/system/vlans/%s" % vlan_id) - - target_url = kwargs["url"] + "system/ports/%s" % l2_port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing VLAN ID '%d' from Port '%s' trunk failed with status code %d: %s" - % (vlan_id, l2_port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing VLAN ID '%d' from Port '%s' trunk succeeded" - % (vlan_id, l2_port_name)) - return True - - -def create_loopback_port(port_name, vrf, ipv4=None, port_desc=None, **kwargs): - """ - Perform POST calls to create a Loopback Interface table entry for a logical L3 Interface. If the - Loopback Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param port_name: Alphanumeric Interface name - :param vrf: Alphanumeric name of the VRF that the loopback port is attached to - :param ipv4: IPv4 address to assign to the interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = get_all_ports(**kwargs) - - if "/rest/v1/system/ports/%s" % port_name not in ports_list: - port_data = { - "admin": "up", - "interfaces": [], - "name": port_name, - "origin": "configuration", - "ospf_if_type": "ospf_iftype_loopback", - "vrf": "/rest/v1/system/vrfs/%s" % vrf - } - - if port_desc is not None: - port_data['description'] = port_desc - - if ipv4 is not None: - port_data['ip4_address'] = ipv4 - - port_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(port_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - # If the port creation for the Loopback interface is successful, then the interface resource must be mapped. - interface_data = { - "name": port_name, - "referenced_by": "/rest/v1/system/ports/%s" % port_name, - "type": "loopback", - "user_config": { - "admin": "up" - } - } - interface_url = kwargs["url"] + "system/interfaces" - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(interface_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding Interface table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding Port and Interface table entries '%s' succeeded" % port_name) - return True - else: - return update_port_ipv4(port_name, ipv4, "up", vrf, **kwargs) - - -def _create_vxlan_port(port_name, source_ipv4=None, port_desc=None, dest_udp_port=4789, **kwargs): - """ - Perform POST calls to create a VXLAN table entry for a logical L3 Interface. If the - VXLAN Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param port_name: Alphanumeric Interface name - :param source_ipv4: Optional source IPv4 address to assign to the VXLAN interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param dest_udp_port: Optional Destination UDP Port that the VXLAN will use. Default is set to 4789 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = get_all_ports(**kwargs) - - if "/rest/v1/system/ports/%s" % port_name not in ports_list: - port_data = { - "admin": "up", - "interfaces": [], - "name": port_name, - "routing": False - } - - if port_desc is not None: - port_data['description'] = port_desc - - port_url = kwargs["url"] + "system/ports" - post_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(port_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding VXLAN Port table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - interface_data = { - "name": port_name, - "options": { - "local_ip": source_ipv4, - "vxlan_dest_udp_port": str(dest_udp_port) - }, - "referenced_by": "/rest/v1/system/ports/%s" % port_name, - "type": "vxlan", - "user_config": { - "admin": "up" - } - } - interface_url = kwargs["url"] + "system/interfaces" - post_data = json.dumps(interface_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(interface_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding VXLAN Interface table entry '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VXLAN Port and Interface table entries '%s' succeeded" % port_name) - return True - else: - return update_port_ipv4(port_name, source_ipv4, "up", "default", **kwargs) - - -def _enable_disable_port(port_name, state="up", **kwargs): - """ - Perform GET and PUT calls to either enable or disable the port by setting Port's admin state to - "up" or "down" - - :param port_name: Alphanumeric name of the interface - :param state: State to set the interface to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if state not in ['up', 'down']: - raise Exception("Administratively-configured state of interface should be 'up' or 'down'") - - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = get_port(port_name, depth=0, selector="configuration", **kwargs) - - if port_data: - port_data['admin'] = state - - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating Port '%s' with admin-configured state '%s' " - "failed with status code %d: %s" % (port_name, state, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating Port '%s' with admin-configured state '%s' " - "succeeded" % (port_name, state)) - return True - else: - logging.warning("FAIL: Unable to update Port '%s' because operation could not find existing Port" % port_name) - return False - - -def _clear_port_acl(port_name, acl_type, **kwargs): - """ - Perform GET and PUT calls to clear a Port's Ingress ACL - - :param port_name: Alphanumeric name of the Port - :param acl_type: Type of ACL: options are 'aclv4_out', 'aclv4_in', 'aclv6_in', or 'aclv6_out' - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if acl_type not in ['aclv4_out', 'aclv4_in', 'aclv6_in', 'aclv6_out']: - raise Exception("ERROR: acl_type should be 'aclv4_out', 'aclv4_in', 'aclv6_in', or 'aclv6_out'") - - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = get_port(port_name, depth=0, selector="configuration", **kwargs) - - cfg_type = acl_type + '_cfg' - cfg_version = acl_type + '_cfg_version' - - port_data.pop(cfg_type, None) - port_data.pop(cfg_version, None) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing %s ACL on Port '%s' failed with status code %d: %s" - % (cfg_type, port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing %s ACL on Port '%s' succeeded" - % (cfg_type, port_name)) - return True - - -def initialize_port_entry(port_name, **kwargs): - """ - Perform a PUT call on the Port to initialize it to it's default state, then initialize the Interface entry. - - :param port_name: Alphanumeric name of the system port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = {} - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Initializing port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Initializing port '%s' succeeded" % port_name) - return interface.initialize_interface_entry(port_name, **kwargs) \ No newline at end of file diff --git a/pyaoscx/pyaoscx_factory.py b/pyaoscx/pyaoscx_factory.py new file mode 100644 index 0000000..948fa16 --- /dev/null +++ b/pyaoscx/pyaoscx_factory.py @@ -0,0 +1,1144 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.session import Session +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.device import Device +from pyaoscx.dns import Dns +from pyaoscx.configuration import Configuration + + +class PyaoscxFactory(): + ''' + Provide a Factory class to instantiate all pyaoscx Modules + through specific methods. + Using the API Version given by the Session + ''' + + __instance__ = None + + def __init__(self, session: Session): + + self.session = session + if PyaoscxFactory.__instance__ is None: + PyaoscxFactory.__instance__ = self + else: + raise Exception("You cannot create another PyaoscxFactory class") + + @staticmethod + def get_instance(session): + """ + Static method to fetch the current instance. + """ + if not PyaoscxFactory.__instance__: + PyaoscxFactory(session) + return PyaoscxFactory.__instance__ + + def device(self): + """ + Create a Device class, to obtain common device configuration, + capacities, capabilities, among other information related. + :return: Device object + """ + + switch = Device(self.session) + # Get Partial configuration attributes + switch.get() + return switch + + def configuration(self): + """ + Create a Configuration class, to obtain device configuration + and perform other actions such as backup_config + :return: Configuration object + """ + + config = Configuration(self.session) + # Get full configuration + config.get() + return config + + def dns(self, vrf=None, + domain_name=None, + domain_list=None, + domain_servers=None, + host_v4_address_mapping=None, + host_v6_address_mapping=None): + """ + Create a DNS class, to configure a DNS inside a given VRF + :param domain_name: Domain name used for name resolution by + the DNS client, if 'dns_domain_list' is not configured + :param domain_list: dict of DNS Domain list names to be used for + address resolution, keyed by the resolution priority order + Example: + { + 0: "hpe.com" + 1: "arubanetworks.com" + } + :param domain_servers: dict of DNS Name servers to be used for address + resolution, keyed by the resolution priority order + Example: + { + 0: "4.4.4.10" + 1: "4.4.4.12" + } + :param host_v4_address_mapping: dict of static host + address configurations and the IPv4 address associated with them + Example: + { + "host1": "5.5.44.5" + "host2": "2.2.44.2" + } + :param host_v6_address_mapping: dict of static host + address configurations and the IPv6 address associated with them + Example: + { + "host1": "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + } + : return DNS object + """ + if vrf is None: + vrf = 'default' + + # Create Vrf object using Factory + vrf_obj = self.vrf(vrf) + + if domain_list is None: + domain_list = {} + if domain_servers is None: + domain_servers = {} + if host_v4_address_mapping is None: + host_v4_address_mapping = {} + if host_v6_address_mapping is None: + host_v6_address_mapping = {} + + # Ensure that all keys are integers + domain_list = { + int(k): v for k, v in domain_list.items()} + domain_servers = { + int(k): v for k, v in domain_servers.items()} + + # Create DNS object + dns = Dns(self.session, vrf, domain_name, domain_list, + domain_servers, host_v4_address_mapping, + host_v6_address_mapping) + + # Apply object into Switch + dns.apply() + + return dns + + def interface(self, name: str): + """ + Create an Interface object. + + :param name: Alphanumeric name of Interface + :return: Interface object + """ + interface_obj = self.session.api_version.get_module( + self.session, 'Interface', + name) + + try: + # Try to create, if object exists then get + interface_obj.apply() + + except GenericOperationError: + interface_obj.get() + + return interface_obj + + def ipv6(self, address: str, interface_name, + address_type=None): + """ + Create a Ipv6 object. If values differ from existing object, incoming + changes will be applied + + :param address: Alphanumeric address of IPv6. + Example: + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :param interface_name: Alphanumeric name of the Interface parent + of the IPv6 Address. + A Interface object is also accepted + :param address_type: Type of IPv6 address. Defaults to + "global-unicast" if not specified. + :return: Ipv6 object + """ + if address_type is None: + _type = "global-unicast" + else: + _type = address_type + + if isinstance(interface_name, str): + # Make Interface into an object + interface = self.session.api_version.get_module( + self.session, 'Interface', interface_name) + # Materialize interface to ensure its existence + interface.get() + + ipv6_obj = self.session.api_version.get_module( + self.session, 'Ipv6', address, + parent_int=interface, type=_type, + preferred_lifetime=604800, + valid_lifetime=2592000, + node_address=True, + ra_prefix=True, + route_tag=0) + + # Try to obtain data; if not, create + try: + ipv6_obj.get() + # Configure variables in case something changes + if address_type is not None: + ipv6_obj.type = address_type + ipv6_obj.apply() + except GenericOperationError: + # Create object inside switch + ipv6_obj.apply() + + return ipv6_obj + + def vlan(self, vlan_id: int, name=None, description=None, + vlan_type=None, admin_conf_state="up"): + """ + Create a Vlan object. + + :param vlan_id: Numeric ID for VLAN + :param name: Alphanumeric name of VLAN, Defaults to "VLAN " + :param description: Optional description to add to VLAN + :param vlan_type: VLAN type. Defaults to "static" if not + specified + :param admin_conf_state: Optional administratively-configured state of + VLAN. Only configurable for static VLANs. Defaults to "up" for + static VLANs. + :return: Vlan object + """ + if name is None: + name = "VLAN {}".format(str(vlan_id)) + + if vlan_type is None: + pvlan_type = "static" + else: + pvlan_type = vlan_type + + if pvlan_type == "static": + # admin-configured state can only be set on static VLANs + vlan_obj = self.session.api_version.get_module( + self.session, 'Vlan', vlan_id, + name=name, description=description, + admin=admin_conf_state) + else: + vlan_obj = self.session.api_version.get_module( + self.session, 'Vlan', vlan_id, name=name, + description=description) + + # Try to obtain data; if not, create + try: + vlan_obj.get() + # Configure variables in case something changes + if name is not None: + vlan_obj.name = name + if description is not None: + vlan_obj.description = description + if admin_conf_state is not None and vlan_type == "static": + vlan_obj.admin = admin_conf_state + vlan_obj.apply() + + except Exception: + # Create object inside switch + vlan_obj.apply() + + return vlan_obj + + def vrf(self, name: str, route_distinguisher=None, vrf_type=None): + """ + Create a Vrf object. If values differ from existing object, incoming + changes will be applied + :param name: VRF name + :param route_distinguisher: Optional route distinguisher to add. + Defaults to nothing if not specified. + :param vrf_type: Optional VRF type. Defaults to "user" if not + specified. + :return: Vrf object + """ + if vrf_type is None: + _type = "user" + else: + _type = vrf_type + + if route_distinguisher is not None and _type != 'default': + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', name, + rd=route_distinguisher, type=_type) + else: + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', name, type=_type) + + # Try to obtain data; if not, create + try: + vrf_obj.get() + # Configure variables in case something changes + if route_distinguisher is not None: + vrf_obj.rd = route_distinguisher + # Apply changes wanted by user + vrf_obj.apply() + except GenericOperationError: + # Create object inside switch + vrf_obj.apply() + + return vrf_obj + + def vsx(self, role=None, isl_port=None, keepalive_vrf=None, + keepalive_peer=None, keepalive_src=None, vsx_mac=None, + keepalive_port=None): + """ + Create a Vsx object. If values differ from existing object, incoming + changes will be applied + + :param role: Alphanumeric role that the system will be in the VSX pair. + The options are "primary" or "secondary" + :param isl_port: Alphanumeric name of the interface that will function + as the inter-switch link + A Interface object is also accepted + :param keepalive_vrf: Alphanumeric name of the VRF that the keepalive + connection will reside on. + A Vrf object is also accepted + :param keepalive_peer: Alphanumeric IP address of the VSX Peer that + will be reached as the keepalive connection. + Example: + '1.1.1.1' + :param keepalive_src: Alphanumeric IP address on the switch that will + function as the keepalive connection source. + Example: + '1.1.1.1' + :param vsx_mac: Alphanumeric MAC address that will function as the VSX + System MAC. + Example: + '01:02:03:04:05:06' + :param keepalive_port: Numeric Keepalive UDP port. Defaults to 7678 + :return: Vsx object + """ + if keepalive_port is None: + _keepalive_port = 7678 + else: + _keepalive_port = keepalive_port + if keepalive_vrf is not None: + if isinstance(keepalive_vrf, str): + keepalive_vrf = self.session.api_version.get_module( + self.session, 'Vrf', keepalive_vrf) + keepalive_vrf.get() + + if isl_port is not None: + if isinstance(isl_port, str): + isl_port = self.session.api_version.get_module( + self.session, 'Interface', isl_port) + isl_port.get() + # Check ISL Port routing + if isl_port.routing: + # Set routing to False + isl_port.routing = False + isl_port.apply() + + vsx_obj = self.session.api_version.get_module( + self.session, 'Vsx', device_role=role, + isl_port=isl_port, keepalive_peer_ip=keepalive_peer, + keepalive_src_ip=keepalive_src, keepalive_vrf=keepalive_vrf, + system_mac=vsx_mac, keepalive_udp_port=_keepalive_port) + + # Try to obtain data; if not, create + try: + vsx_obj.get() + # Configure variables in case something changes + if role is not None: + vsx_obj.device_role = role + if isl_port is not None: + vsx_obj.isl_port = isl_port + if keepalive_peer is not None: + vsx_obj.keepalive_peer_ip = keepalive_peer + if keepalive_src is not None: + vsx_obj.keepalive_src_ip = keepalive_src + if keepalive_vrf is not None: + vsx_obj.keepalive_vrf = keepalive_vrf + if vsx_mac is not None: + vsx_obj.system_mac = vsx_mac + if _keepalive_port is not None: + vsx_obj.keepalive_udp_port = _keepalive_port + # Apply changes + vsx_obj.apply() + + except GenericOperationError: + # Create object inside switch + vsx_obj.apply() + + return vsx_obj + + def bgp_router_asn(self, vrf, asn: int, router_id=None): + """ + Create a BgpRouter object as Autonomous System Number. + If values differ from existing object, incoming + changes will be applied + + :param vrf: Alphanumeric name of the VRF the BGP ASN belongs to. + A Vrf object is also accepted + :param asn: Integer that represents the Autonomous System Number + :param router_id: Optional IPv4 address that functions as the + BGP Router ID + :return: BgpRouter object + """ + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + bgp_router_obj = self.session.api_version.get_module( + self.session, 'BgpRouter', asn, parent_vrf=vrf, + router_id=router_id) + + # Try to obtain data; if not, create + try: + bgp_router_obj.get() + # Change attributes + if router_id is not None: + bgp_router_obj.router_id = router_id + # Apply changes + bgp_router_obj.apply() + except GenericOperationError: + # Create object inside switch + bgp_router_obj.apply() + + return bgp_router_obj + + def bgp_router_vrf(self, vrf, asn: int, redistribute): + """ + Create a BgpRouter object with a BGP VRF settings for the + associated BGP ASN. + If values differ from existing object, incoming + changes will be applied + + :param vrf: Alphanumeric name of the VRF the BGP ASN belongs to. + A Vrf object is also accepted + :param asn: Integer that represents the Autonomous System Number + :param redistribute: Alphanumeric to specify which + types of routes that should be redistributed by BGP. The + options are "ipv4-unicast" or "ipv6-unicast". + :return: BgpRouter object + """ + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + redistribute_data = {} + + if redistribute == 'ipv4-unicast': + redistribute_data = { + "ipv4-unicast": ["connected"] + } + elif redistribute == 'ipv6-unicast': + redistribute_data = { + "ipv6-unicast": ["connected"] + } + + bgp_router_obj = self.session.api_version.get_module( + self.session, 'BgpRouter', asn, parent_vrf=vrf, + redistribute=redistribute_data) + + # Try to obtain data; if not, create + try: + bgp_router_obj.get() + # Change attributes + bgp_router_obj.redistribute = redistribute_data + # Apply changes + bgp_router_obj.apply() + except GenericOperationError: + # Create object inside switch + bgp_router_obj.apply() + + return bgp_router_obj + + def bgp_neighbor(self, vrf, bgp_router_asn, group_ip, + family_type=None, reflector=None, + send_community=None, local_interface=""): + """ + Create a BgpNeighbor object. + If values differ from existing object, incoming + changes will be applied + :param vrf: Alphanumeric name of the VRF the BGP ASN belongs to. + A Vrf object is also accepted + :param bgp_router_asn: Integer that represents the Autonomous System + Number + :param group_ip: IPv4 address or name of group of the neighbors that + functions as the BGP Router link. + Example: + '1.1.1.1' + :param family_type: Alphanumeric to specify what type of neighbor + settings to configure. The options are 'l2vpn-evpn', + 'ipv4-unicast', or 'ipv6-unicast'. When setting to l2vpn-evpn, + the neighbor configurations also will add + route-reflector-client and send-community settings. + :param reflector: Boolean value to determine whether this neighbor + has route reflector enabled. Default is False. + :param send_community: Boolean value to determine whether this + neighbor has send-community enabled. Default is False. + :param local_interface: Optional alphanumeric to specify which + interface the neighbor will apply to. + + :return: BgpNeighbor object + """ + if family_type is None: + _family_type = "l2vpn-evpn" + + if _family_type not in ['l2vpn-evpn', 'ipv4-unicast', 'ipv6-unicast']: + raise Exception("ERROR: family_type should be 'l2vpn-evpn',\ + 'ipv4-unicast', or 'ipv6-unicast'") + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + if isinstance(bgp_router_asn, int): + # Make BGP Router into an object + bgp_router_obj = self.session.api_version.get_module( + self.session, 'BgpRouter', bgp_router_asn, parent_vrf=vrf) + + # Materialize BGP Router to ensure its existence + bgp_router_obj.get() + # Set asn integer + asn = bgp_router_asn + # Set variable as an object + bgp_router_asn = bgp_router_obj + else: + asn = bgp_router_asn.asn + + if local_interface != "": + if isinstance(local_interface, str): + local_interface = self.session.api_version.get_module( + self.session, 'Interface', local_interface) + local_interface.get() + + # Set values needed + activate = { + "ipv4-unicast": False, + "ipv6-unicast": False, + "l2vpn-evpn": False + } + + next_hop_unchanged = { + "l2vpn-evpn": False + } + + route_reflector_client = { + "ipv4-unicast": False, + "ipv6-unicast": False, + "l2vpn-evpn": False + } + + send_community_data = { + "ipv4-unicast": "none", + "ipv6-unicast": "none", + "l2vpn-evpn": "none" + } + + activate[_family_type] = True + + # Set incoming variables + if send_community is None: + _send_community = False + else: + _send_community = send_community + if reflector is None: + _reflector = False + else: + _reflector = reflector + + if _send_community: + send_community_data[_family_type] = "both" + + if _reflector: + route_reflector_client[_family_type] = reflector + + bgp_neighbor_obj = self.session.api_version.get_module( + self.session, 'BgpNeighbor', group_ip, + parent_bgp_router=bgp_router_asn, + remote_as=asn, shutdown=False, + local_interface=local_interface, + activate=activate, next_hop_unchanged=next_hop_unchanged, + route_reflector_client=route_reflector_client, + send_community=send_community_data + ) + + # Try to obtain data; if not, create + try: + bgp_neighbor_obj.get() + # Change attributes + if local_interface != "": + bgp_neighbor_obj.local_interface = local_interface + if family_type is not None: + bgp_neighbor_obj.activate = activate + if send_community is not None: + bgp_neighbor_obj.send_community = send_community_data + if reflector is not None: + bgp_neighbor_obj.route_reflector_client = \ + route_reflector_client + # Apply changes + bgp_neighbor_obj.apply() + except GenericOperationError: + # Create object inside switch + bgp_neighbor_obj.apply() + + return bgp_neighbor_obj + + def ospf_router_id(self, vrf, ospf_id, + redistribute=None): + """ + Create a OspfRouter object as OSPF ID. + If values differ from existing object, incoming + changes will be applied + + :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to + A Vrf object is also accepted + :param ospf_id: OSPF process ID between numbers 1-63 + :param redistribute: List of types of redistribution methods for + the OSPF Process, with the options being "bgp", + "connected", and "static" + :return: OspfRouter object + """ + if redistribute is None: + _redistribute = ["connected", "static"] + else: + _redistribute = redistribute + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + ospf_router_obj = self.session.api_version.get_module( + self.session, 'OspfRouter', ospf_id, parent_vrf=vrf, + redistribute=_redistribute) + + # Try to obtain data; if not, create + try: + ospf_router_obj.get() + # Change attributes + if redistribute is not None: + ospf_router_obj.redistribute = redistribute + # Apply changes + ospf_router_obj.apply() + + except GenericOperationError: + # Create object inside switch + ospf_router_obj.apply() + + return ospf_router_obj + + def ospf_router_area(self, vrf, ospf_id, area_id, area_type=None): + """ + Create an OspfArea object. + If values differ from existing object, incoming + changes will be applied + + :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to + :param ospf_id: OSPF process ID between numbers 1-63 + :param area_id: Unique identifier as a string in the form of x.x.x.x + :param area_type: Alphanumeric defining how the external routing and + summary LSAs for this area will be handled. + Options are "default","nssa","nssa_no_summary","stub", + "stub_no_summary" + + :return: OspfArea object + """ + if area_type is None: + _area_type = 'default' + else: + _area_type = area_type + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + if isinstance(ospf_id, int): + # Make OSPF Router into an object + ospf_router_obj = self.session.api_version.get_module( + self.session, 'OspfRouter', ospf_id, parent_vrf=vrf) + + # Materialize OSPF Router to ensure its existence + ospf_router_obj.get() + # Set variable as an object + ospf_router = ospf_router_obj + else: + # Set ospf_router variable as OspfRouter object + ospf_router = ospf_id + + # Create OspfArea object + ospf_area_obj = self.session.api_version.get_module( + self.session, 'OspfArea', area_id, parent_ospf_router=ospf_router, + area_type=_area_type, ipsec_ah={}, ipsec_esp={}) + + # Try to obtain data; if not, create + try: + ospf_area_obj.get() + # Change attributes + if area_type is not None: + ospf_area_obj.area_type = area_type + # Apply changes + ospf_area_obj.apply() + except GenericOperationError: + # Create object inside switch + ospf_area_obj.apply() + + return ospf_area_obj + + def ospf_interface(self, vrf, ospf_id, area_id, interface_name): + """ + Create a OspfInterface object. + + :param vrf: Alphanumeric name of the VRF the OSPF ID belongs to. + A Vrf object is also accepted + :param ospf_id: OSPF process ID between numbers 1-63 + A OSPF Router is accepted + :param area_id: Unique identifier as a string in the form of x.x.x.x + :param interface_name: Alphanumeric name of the interface that will be + attached to the OSPF area + :return: OspfInterface object + """ + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + if isinstance(ospf_id, int): + # Make Ospf ID into an object + ospf_router_obj = self.session.api_version.get_module( + self.session, 'OspfRouter', ospf_id, parent_vrf=vrf) + + # Materialize OSPF Router to ensure its existence + ospf_router_obj.get() + + # Set variable as an object + ospf_router = ospf_router_obj + else: + ospf_router = ospf_id + + if isinstance(area_id, str): + # Create OspfArea object + ospf_area_obj = self.session.api_version.get_module( + self.session, 'OspfArea', area_id, + parent_ospf_router=ospf_router) + # Materialize it + ospf_area_obj.get() + + # Set variable as an object + area = ospf_area_obj + else: + area = area_id + + # Make Ospf ID into an object + ospf_interface = self.session.api_version.get_module( + self.session, 'OspfInterface', interface_name, + parent_ospf_area=area) + + # Try to obtain data; if not, create + try: + ospf_interface.get() + except GenericOperationError: + # Create object inside switch + ospf_interface.apply() + + return ospf_interface + + def vlan_and_svi(self, vlan_id, vlan_name, vlan_int_name, + vlan_desc=None, ipv4=None, vrf_name="default", + vlan_port_desc=None): + """ + Create VLAN and Interface objects to represent VLAN and SVI, respectively. + + :param vlan_id: Numeric ID of VLAN + :param vlan_name: Alphanumeric name of VLAN + :param vlan_int_name: Alphanumeric name for the VLAN interface + :param vlan_desc: Optional description to add to VLAN + :param ipv4: Optional IPv4 address to assign to the interface.Defaults + to nothing if not specified.. + Example: + '1.1.1.1' + :param vrf_name: VRF to attach the SVI to. Defaults to "default" i + not specified + :param vlan_port_desc: Optional description for the interface. + Defaults to nothing if not specified. + :return: A tuple with a Vlan object and a Interface SVI object + """ + # Create Vlan object + vlan_obj = self.vlan(vlan_id, vlan_name, vlan_desc) + + # Create Interface Object + interface_obj = self.interface(vlan_int_name) + # Set Interface as an SVI + interface_obj.configure_svi(vlan_id, ipv4, vrf_name, vlan_port_desc) + + return vlan_obj, interface_obj + + def dhcp_relay(self, vrf, port): + """ + Create a DhcpRelay object. + + :param vrf: Alphanumeric name of VRF + :param port: Alphanumeric name of Port + + :return: DhcpRelay object + """ + port_obj = self.session.api_version.get_module( + self.session, 'Interface', + port) + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + + dhcp_relay = self.session.api_version.get_module( + self.session, 'DhcpRelay', index_id=vrf_obj, port=port_obj) + + # Try to obtain data; if not, create + try: + dhcp_relay.get() + except GenericOperationError: + # Create object inside switch + dhcp_relay.apply() + + return dhcp_relay + + def acl(self, list_name, list_type): + """ + Create an Acl object. + + :param list_name: Alphanumeric name of ACL + :param list_type: Alphanumeric type of ACL. + Type should be one of "ipv4," "ipv6," or "mac" + + :return: Acl object + """ + + acl = self.session.api_version.get_module( + self.session, 'ACL', index_id=list_name, list_type=list_type) + + # Try to obtain data; if not, create + try: + acl.get() + except GenericOperationError: + # Create object inside switch + acl.apply() + + return acl + + def acl_entry(self, list_name, list_type, sequence_num, action='permit', + count=None, protocol=None, src_ip=None, dst_ip=None, + dst_l4_port_min=None, dst_l4_port_max=None, src_mac=None, + dst_mac=None, ethertype=None): + """ + Create an AclEntry object + + :param list_name: Alphanumeric name of the ACL + :param list_type: Type should be one of "ipv4," "ipv6," or "mac" + :param sequence_num: Integer number of the sequence + :param action: Action should be either "permit" or "deny" + :param count: Optional boolean flag that when true, will make entry + increment hit count for matched packets + :param protocol: Optional integer IP protocol number + :param src_ip: Optional source IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_ip: Optional destination IP address. Both IPv4 and IPv6 are supported. + Example: + 10.10.12.11/255.255.255.255 + 2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + :param dst_l4_port_min: Optional minimum L4 port number in range; used + in conjunction with dst_l4_port_max. + :param dst_l4_port_max: Optional maximum L4 port number in range; used + in conjunction with dst_l4_port_min. + :param src_mac: Optional source MAC address + Example: + '01:02:03:04:05:06' + :param dst_mac: Optional destination MAC address + Example: + '01:02:03:04:05:06' + :param ethertype: Optional integer EtherType number + + :return acl_entry: A AclEntry object + + """ + # Create Acl object + acl = self.session.api_version.get_module( + self.session, 'ACL', index_id=list_name, list_type=list_type) + + # Get ACL data + acl.get() + + # Create ACL Entry + acl_entry_obj = self.session.api_version.get_module( + self.session, 'AclEntry', index_id=sequence_num, parent_acl=acl, + action=action, count=count, protocol=protocol, src_ip=src_ip, + dst_ip=dst_ip, dst_l4_port_min=dst_l4_port_min, + dst_l4_port_max=dst_l4_port_max, src_mac=src_mac, + dst_mac=dst_mac, ethertype=ethertype) + + # Try to obtain data; if not, create + try: + acl_entry_obj.get() + # Change attributes + if dst_l4_port_min is not None: + acl_entry_obj.dst_l4_port_min = dst_l4_port_min + if dst_l4_port_max is not None: + acl_entry_obj.dst_l4_port_max = dst_l4_port_max + if src_mac is not None: + acl_entry_obj.src_mac = src_mac + if dst_mac is not None: + acl_entry_obj.dst_mac = dst_mac + if ethertype is not None: + acl_entry_obj.ethertype = ethertype + # Apply changes + acl_entry_obj.apply() + except GenericOperationError: + # Create object inside switch + acl_entry_obj.apply() + + return acl_entry_obj + + def vrf_address_family(self, vrf, address_family='ipv4_unicast'): + """ + Create a VrfAddressFamily object with a VRF + + :param vrf: Alphanumeric name of the VRF the Family Address belongs to. + A Vrf object is also accepted + :param address_family: Alphanumeric type of the Address Family. + The options are 'ipv4_unicast' and 'ipv6_unicast'. + The default value is set to 'ipv4_unicast'. + :return: VRF_Address_Family object + """ + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + vrf_address_fam_obj = self.session.api_version.get_module( + self.session, 'VrfAddressFamily', address_family, + parent_vrf=vrf) + + # Try to obtain data; if not, create + try: + vrf_address_fam_obj.get() + except GenericOperationError: + # Create object inside switch + vrf_address_fam_obj.apply() + + return vrf_address_fam_obj + + def aggregate_address(self, vrf, bgp_router_asn, family_type, + ip_prefix): + """ + Create an AggregateAddress object. + :param vrf: Alphanumeric name of the VRF the BGP ASN belongs to. + A Vrf object is also accepted + :param bgp_router_asn: Integer that represents the Autonomous System + Number + :param family_type: Address Family type for the Aggregate Address. + Either 'ipv4-unicast', 'ipv6-unicast' + :param ip_prefix: IP address and mask used to key Aggregate Address. + Example: + '1.1.1.1/24' + + :return: AggregateAddress object + """ + + if family_type not in ['ipv4-unicast', 'ipv6-unicast']: + raise Exception("ERROR: family_type should be\ + 'ipv4-unicast', or 'ipv6-unicast'") + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + if isinstance(bgp_router_asn, int): + # Make BGP Router into an object + bgp_router_obj = self.session.api_version.get_module( + self.session, 'BgpRouter', bgp_router_asn, parent_vrf=vrf) + + # Materialize interface to ensure its existence + bgp_router_obj.get() + # Set variable as an object + bgp_router_asn = bgp_router_obj + + aggregate_add_obj = self.session.api_version.get_module( + self.session, 'AggregateAddress', + family_type, + ip_prefix=ip_prefix, + parent_bgp_router=bgp_router_asn + ) + + # Try to obtain data; if not, create + try: + aggregate_add_obj.get() + except GenericOperationError: + # Create object inside switch + aggregate_add_obj.apply() + + return aggregate_add_obj + + def static_route(self, vrf, destination_address_prefix): + """ + Create a StaticRoute object with a VRF. + + :param vrf: Name of the VRF on which the static route + is to be configured. Defaults to default vrf + A Vrf object is also accepted + :param destination_address_prefix: String IPv4 or IPv6 destination + prefix and mask in the address/mask format. + Example: + '1.1.1.1' + or + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :return: StaticRoute object + """ + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + + static_route_obj = self.session.api_version.get_module( + self.session, 'StaticRoute', destination_address_prefix, + parent_vrf=vrf) + + # Try to obtain data; if not, create + try: + static_route_obj.get() + except GenericOperationError: + # Create object inside switch + static_route_obj.apply() + + return static_route_obj + + def static_nexthop(self, vrf, destination_address_prefix, + next_hop_ip_address=None, + nexthop_type=None, + distance=None, + next_hop_interface=None, + bfd_enable=None): + """ + Create a Static Nexthop, with a VRF and a Destination Address + related to a Static Route. + + :param vrf: Name of the VRF on which the static route + is to be configured. Defaults to default vrf + A Vrf object is also accepted + :param destination_address_prefix: String IPv4 or IPv6 destination + prefix and mask in the address/mask format + A StaticRoute object is also accepted. + Example: + '1.1.1.1' + or + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :param next_hop_ip_address: The IPv4 address or the IPv6 address of + next hop. + Example: + '1.1.1.1' + or + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :param nexthop_type: Specifies whether the static route is a forward, + blackhole or reject route. + :param distance: Administrative distance to be used for the next + hop in the static route instead of default value. + :param next_hop_interface: The interface through which the next hop + can be reached. + :param bfd_enable: Boolean to enable BFD + :return: StaticNexthop object + """ + + if isinstance(vrf, str): + # Make VRF into an object + vrf_obj = self.session.api_version.get_module( + self.session, 'Vrf', vrf) + # Materialize VRF to ensure its existence + vrf_obj.get() + vrf = vrf_obj + static_route = destination_address_prefix + if isinstance(destination_address_prefix, str): + # Make a Static Route Object + static_route_obj = self.session.api_version.get_module( + self.session, 'StaticRoute', destination_address_prefix, + parent_vrf=vrf) + # Materialize Object to ensure its existence + static_route_obj.get() + static_route = static_route_obj + + if distance is None: + distance = 1 + # Set variable + next_hop_interface_obj = None + if next_hop_interface is not None: + next_hop_interface_obj = self.session.api_version.get_module( + self.session, 'Interface', + next_hop_interface) + if nexthop_type is None: + nexthop_type = 'forward' + + if nexthop_type == 'forward': + bfd_enable = False + + static_nexthop_obj = self.session.api_version.get_module( + self.session, 'StaticNexthop', 0, + parent_static_route=static_route, + ) + + # Try to obtain data; if not, create + try: + static_nexthop_obj.get() + # Delete previous static nexthop + static_nexthop_obj.delete() + except GenericOperationError: + # Catch error + pass + + finally: + static_nexthop_obj = self.session.api_version.get_module( + self.session, 'StaticNexthop', + 0, + parent_static_route=static_route_obj, + ip_address=next_hop_ip_address, + distance=distance, + port=next_hop_interface_obj, + type=nexthop_type, + bfd_enable=bfd_enable + ) + # Create object inside switch + static_nexthop_obj.apply() + + return static_nexthop_obj diff --git a/pyaoscx/pyaoscx_module.py b/pyaoscx/pyaoscx_module.py new file mode 100644 index 0000000..3211fad --- /dev/null +++ b/pyaoscx/pyaoscx_module.py @@ -0,0 +1,120 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from abc import ABC, abstractmethod +from pyaoscx.utils.connection import connected + + +class PyaoscxModule(ABC): + ''' + Provide an Interface class for pyaoscx Modules + ''' + + base_uri = "" + indices = [] + + @abstractmethod + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + pass + + @abstractmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system and create a dictionary + of each object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing object IDs as keys their respective objects as + values + ''' + pass + + @abstractmethod + @connected + def apply(self): + ''' + Main method used to either create or update an existing + . + Checks whether the exists in the switch + Calls self.update() if object being updated + Calls self.create() if a new is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + pass + + @abstractmethod + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing + table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + pass + + @abstractmethod + @connected + def create(self): + ''' + Perform a POST call to create a new + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + + ''' + pass + + @abstractmethod + @connected + def delete(self): + ''' + Perform DELETE call to delete table entry. + + ''' + pass + + def get_uri(self): + ''' + Method used to obtain the specific URI + return: Object's URI + ''' + pass + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + pass + + @abstractmethod + def from_uri(cls, session, uri): + ''' + Create a object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return index, : tuple containing both the object and the + 's ID + ''' + pass diff --git a/pyaoscx/qos.py b/pyaoscx/qos.py deleted file mode 100644 index 408bd39..0000000 --- a/pyaoscx/qos.py +++ /dev/null @@ -1,3511 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops -from pyaoscx import system -from pyaoscx import port -from pyaoscx import interface - -import json -import random -import logging - - -def get_all_queue_profiles(**kwargs): - """ - Perform a GET call to get a list of all QoS queue profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS queue profiles in the table - """ - - if kwargs["url"].endswith("/v1/"): - queue_profiles = _get_all_queue_profiles_v1(**kwargs) - else: # Updated else for when version is v10.04 - queue_profiles = _get_all_queue_profiles(**kwargs) - return queue_profiles - - -def _get_all_queue_profiles_v1(**kwargs): - """ - Perform a GET call to get a list of all QoS queue profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS queue profiles in the table - """ - target_url = kwargs["url"] + "system/q_profiles" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all QoS queue profiles failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all QoS queue profiles succeeded") - - queue_profiles_list = response.json() - return queue_profiles_list - - -def _get_all_queue_profiles(**kwargs): - """ - Perform a GET call to get a dictionary containing all QoS queue profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS queue profiles in the table - """ - target_url = kwargs["url"] + "system/q_profiles" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all QoS queue profiles failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all QoS queue profiles succeeded") - - queue_profiles_dict = response.json() - return queue_profiles_dict - - -def create_queue_profile(profile_name, **kwargs): - """ - Perform a POST call to create a QoS queue profile with no entries - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_queue_profile_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _create_queue_profile(profile_name, **kwargs) - - -def _create_queue_profile_v1(profile_name, **kwargs): - """ - Perform a POST call to create a QoS queue profile with no entries - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - queue_profiles_list = get_all_queue_profiles(**kwargs) - - # Queue profile doesn't exist; create it - if "/rest/v1/system/q_profiles/%s" % profile_name not in queue_profiles_list: - - queue_profile_data = { - "name": profile_name - } - - target_url = kwargs["url"] + "system/q_profiles" - post_data = json.dumps(queue_profile_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating queue profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to create queue profile '%s' since it already exists" - % profile_name) - return True - - -def _create_queue_profile(profile_name, **kwargs): - """ - Perform a POST call to create a QoS queue profile with no entries - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - queue_profiles_dict = _get_all_queue_profiles(**kwargs) - - # Queue profile doesn't exist; create it - if profile_name not in queue_profiles_dict: - - queue_profile_data = { - "name": profile_name - } - - target_url = kwargs["url"] + "system/q_profiles" - post_data = json.dumps(queue_profile_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating queue profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to create queue profile '%s' since it already exists" - % profile_name) - return True - - -def get_all_queue_profile_entries(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing queue profile entry URIs - """ - - if kwargs["url"].endswith("/v1/"): - queue_profile_entries = _get_all_queue_profile_entries_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - queue_profile_entries = _get_all_queue_profile_entries(profile_name, **kwargs) - return queue_profile_entries - - -def _get_all_queue_profile_entries_v1(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing queue profile entry URIs - """ - - target_url = kwargs["url"] + "system/q_profiles/%s/q_profile_entries" % profile_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIs of entries in QoS queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in QoS queue profile '%s' succeeded" % profile_name) - - queue_profile_entries = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary, - if not queue_profile_entries: - return {} - else: - return queue_profile_entries - - -def _get_all_queue_profile_entries(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing queue profile entry URIs - """ - - target_url = kwargs["url"] + "system/q_profiles/%s/q_profile_entries" % profile_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIs of entries in QoS queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in QoS queue profile '%s' succeeded" % profile_name) - - queue_profile_entries_dict = response.json() - - return queue_profile_entries_dict - - -# Same for both v1 and v3? -def create_queue_profile_entry(profile_name, queue_num, local_priorities, desc=None, **kwargs): - """ - Perform a POST call to create a QoS queue profile entry - - :param profile_name: Alphanumeric name of the queue profile - :param queue_num: Integer number of the entry - :param local_priorities: List of integers, each item being a local priority - :param desc: Optional description for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - queue_profile_entry_data = { - "queue_number": queue_num, - "local_priorities": local_priorities - } - - if desc is not None: - queue_profile_entry_data["description"] = desc - - target_url = kwargs["url"] + "system/q_profiles/%s/q_profile_entries" % profile_name - post_data = json.dumps(queue_profile_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for queue profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for queue profile '%s' succeeded" % (queue_num, profile_name)) - return True - - -def get_all_schedule_profiles(**kwargs): - """ - Perform a GET call to get a list of all QoS schedule profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS schedule profiles in the table - """ - - if kwargs["url"].endswith("/v1/"): - schedule_profiles = _get_all_schedule_profiles_v1(**kwargs) - else: # Updated else for when version is v10.04 - schedule_profiles = _get_all_schedule_profiles(**kwargs) - return schedule_profiles - - -def _get_all_schedule_profiles_v1(**kwargs): - """ - Perform a GET call to get a list of all QoS schedule profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS schedule profiles in the table - """ - target_url = kwargs["url"] + "system/qos" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all QoS schedule profiles failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all QoS schedule profiles succeeded") - - schedule_profiles_list = response.json() - return schedule_profiles_list - - -def _get_all_schedule_profiles(**kwargs): - """ - Perform a GET call to get a dictionary containing all QoS schedule profiles - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all QoS schedule profiles in the table - """ - target_url = kwargs["url"] + "system/qos" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all QoS schedule profiles failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all QoS schedule profiles succeeded") - - schedule_profiles_dict = response.json() - return schedule_profiles_dict - - -def create_schedule_profile(profile_name, **kwargs): - """ - Perform a POST call to create a QoS schedule profile with no entries - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_schedule_profile_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _create_schedule_profile(profile_name, **kwargs) - - -def _create_schedule_profile_v1(profile_name, **kwargs): - """ - Perform a POST call to create a QoS schedule profile with no entries - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profiles_list = get_all_schedule_profiles(**kwargs) - - # Schedule profile doesn't exist; create it - if "/rest/v1/system/qos/%s" % profile_name not in schedule_profiles_list: - - schedule_profile_data = { - "name": profile_name - } - - target_url = kwargs["url"] + "system/qos" - post_data = json.dumps(schedule_profile_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating schedule profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to create schedule profile '%s' since it already exists" - % profile_name) - return True - - -def _create_schedule_profile(profile_name, **kwargs): - """ - Perform a POST call to create a QoS schedule profile with no entries - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profiles_dict = _get_all_schedule_profiles(**kwargs) - - # Schedule profile doesn't exist; create it - if profile_name not in schedule_profiles_dict: - - schedule_profile_data = { - "name": profile_name - } - - target_url = kwargs["url"] + "system/qos" - post_data = json.dumps(schedule_profile_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating schedule profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to create schedule profile '%s' since it already exists" - % profile_name) - return True - - -def get_all_schedule_profile_entries(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing schedule profile entry URIs - """ - - if kwargs["url"].endswith("/v1/"): - schedule_profile_entries = _get_all_schedule_profile_entries_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - schedule_profile_entries = _get_all_schedule_profile_entries(profile_name, **kwargs) - return schedule_profile_entries - - -def _get_all_schedule_profile_entries_v1(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing schedule profile entry URIs - """ - - target_url = kwargs["url"] + "system/qos/%s/queues" % profile_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIs of entries in QoS schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in QoS schedule profile '%s' succeeded" % profile_name) - - schedule_profile_entries = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary - if not schedule_profile_entries: - return {} - else: - return schedule_profile_entries - - -def _get_all_schedule_profile_entries(profile_name, **kwargs): - """ - Perform a GET call to get all entries of a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing schedule profile entry URIs - """ - - target_url = kwargs["url"] + "system/qos/%s/queues" % profile_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of URIs of entries in QoS schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting dictionary of URIs of entries in QoS schedule profile '%s' succeeded" % profile_name) - - schedule_profile_entries = response.json() - - return schedule_profile_entries - - -def create_schedule_profile_entry(profile_name, queue_num, algorithm="strict", bandwidth=None, - burst_size=None, weight=None, **kwargs): - """ - Perform a POST call to create a QoS schedule profile entry - - :param profile_name: Alphanumeric name of the schedule profile - :param queue_num: Integer number of queue - :param algorithm: Algorithm type should be "strict," "dwrr," or "wfq." Defaults to "strict" if not specified. - :param bandwidth: Optional bandwidth limit (in kilobits/s) to apply to egress queue traffic - :param burst_size: Optional burst size (in kilobytes) allowed per bandwidth-limited queue - :param weight: Optional weight value for the queue. The maximum weight is hardware-dependent. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profile_entry_data = { - "queue_number": queue_num, - "algorithm": algorithm - } - - if bandwidth is not None: - schedule_profile_entry_data['bandwidth'] = bandwidth - - if burst_size is not None: - schedule_profile_entry_data['burst'] = burst_size - - if weight is not None: - schedule_profile_entry_data['weight'] = weight - - target_url = kwargs["url"] + "system/qos/%s/queues" % profile_name - post_data = json.dumps(schedule_profile_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for schedule profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for schedule profile '%s' succeeded" % (queue_num, profile_name)) - return True - - -def apply_profiles_globally(queue_profile_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply a QoS queue profile and schedule profile on all interfaces. - - :param queue_profile_name: Alphanumeric name of the queue profile - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _apply_profiles_globally_v1(queue_profile_name, schedule_profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _apply_profiles_globally(queue_profile_name, schedule_profile_name, **kwargs) - - -def _apply_profiles_globally_v1(queue_profile_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply a QoS queue profile and schedule profile on all interfaces. - - :param queue_profile_name: Alphanumeric name of the queue profile - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['q_profile_default'] = queue_profile_name - system_data['qos_default'] = schedule_profile_name - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying queue profile '%s' and schedule profile '%s' globally failed with status code %d: %s" - % (queue_profile_name, schedule_profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying queue profile '%s' and schedule profile '%s' globally succeeded" - % (queue_profile_name, schedule_profile_name)) - return True - - -def _apply_profiles_globally(queue_profile_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply a QoS queue profile and schedule profile on all interfaces. - - :param queue_profile_name: Alphanumeric name of the queue profile - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['q_profile_default'] = queue_profile_name - system_data['qos_default'] = schedule_profile_name - - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Applying queue profile '%s' and schedule profile '%s' globally failed with status code %d: %s" - % (queue_profile_name, schedule_profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Applying queue profile '%s' and schedule profile '%s' globally succeeded" - % (queue_profile_name, schedule_profile_name)) - return True - - -def apply_profile_interface(port_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply QoS schedule profile on an interface. If there is a globally applied - schedule profile, this function will override the specified interface with the specified schedule profile. - - :param port_name: Alphanumeric name of the Port on which the schedule profile is to be applied - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _apply_profile_interface_v1(port_name, schedule_profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _apply_profile_interface(port_name, schedule_profile_name, **kwargs) - - -def _apply_profile_interface_v1(port_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply QoS schedule profile on an interface. If there is a globally applied - schedule profile, this function will override the specified interface with the specified schedule profile. - - :param port_name: Alphanumeric name of the Port on which the schedule profile is to be applied - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data['qos'] = schedule_profile_name - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating QoS schedule profile for Port '%s' to '%s' failed with status code %d: %s" - % (port_name, schedule_profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating QoS schedule profile for Port '%s' to '%s' succeeded" - % (port_name, schedule_profile_name)) - return True - - -# This is going to have to change since it's a port-interface thing -def _apply_profile_interface(port_name, schedule_profile_name, **kwargs): - """ - Perform GET and PUT calls to apply QoS schedule profile on an interface. If there is a globally applied - schedule profile, this function will override the specified interface with the specified schedule profile. - - :param port_name: Alphanumeric name of the Port on which the schedule profile is to be applied - :param schedule_profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data['qos'] = schedule_profile_name - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating QoS schedule profile for Port '%s' to '%s' failed with status code %d: %s" - % (port_name, schedule_profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating QoS schedule profile for Port '%s' to '%s' succeeded" - % (port_name, schedule_profile_name)) - return True - - -def set_trust_globally(trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on all interfaces. - - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _set_trust_globally_v1(trust_mode, **kwargs) - else: # Updated else for when version is v10.04 - return _set_trust_globally(trust_mode, **kwargs) - - -def _set_trust_globally_v1(trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on all interfaces. - - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data['qos_config'] = {"qos_trust": trust_mode} - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting QoS trust mode globally to '%s' failed with status code %d: %s" - % (trust_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting QoS trust mode globally to '%s' succeeded" % trust_mode) - return True - - -def _set_trust_globally(trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on all interfaces. - - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data['qos_config'] = {"qos_trust": trust_mode} - - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting QoS trust mode globally to '%s' failed with status code %d: %s" - % (trust_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting QoS trust mode globally to '%s' succeeded" % trust_mode) - return True - - -def set_trust_interface(port_name, trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on an interface. If there is a globally applied - trust mode, this function will override the specified interface with the specified trust mode. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _set_trust_interface_v1(port_name, trust_mode, **kwargs) - else: # Updated else for when version is v10.04 - return _set_trust_interface(port_name, trust_mode, **kwargs) - - -def _set_trust_interface_v1(port_name, trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on an interface. If there is a globally applied - trust mode, this function will override the specified interface with the specified trust mode. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data['qos_config'] = {'qos_trust': trust_mode} - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating QoS trust mode for Port '%s' to '%s' failed with status code %d: %s" - % (port_name, trust_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating QoS trust mode for Port '%s' to '%s' succeeded" - % (port_name, trust_mode)) - return True - - -def _set_trust_interface(port_name, trust_mode, **kwargs): - """ - Perform GET and PUT calls to set QoS trust mode on an interface. If there is a globally applied - trust mode, this function will override the specified interface with the specified trust mode. - - :param port_name: Alphanumeric name of the Interface on which the trust mode is to be set - :param trust_mode: Trust mode should be one of "none," "cos," or "dscp." - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - int_data = interface.get_interface(port_name_percents, 1, "writable", **kwargs) - - if port_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - int_data['qos_config'] = {'qos_trust': trust_mode} - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating QoS trust mode for Interface '%s' to '%s' failed with status code %d: %s" - % (port_name, trust_mode, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating QoS trust mode for Interface '%s' to '%s' succeeded" - % (port_name, trust_mode)) - return True - -# Same for v1 and v3? -def remap_dscp_entry(code_point, color=None, desc=None, local_priority=None, **kwargs): - """ - Perform PUT call to modify the DSCP code point entry. - - :param code_point: Integer identifying the DSCP map code point entry. - :param color: Optional color used for packet-drop decisions. Should be one of "red," "yellow," or "green." - If not specified, defaults to the factory default color for the given code point. - :param desc: Optional description for the DSCP code point entry. If not specified, defaults to the factory default - description for the given code point. - :param local_priority: Optional local priority to associate to incoming packets. If not specified, defaults to the - factory default local priority for the given code point. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - code_point_entry_data = {} - if color is not None: - code_point_entry_data['color'] = color - - if desc is not None: - code_point_entry_data['description'] = desc - - if local_priority is not None: - code_point_entry_data['local_priority'] = local_priority - - target_url = kwargs["url"] + "system/qos_dscp_map_entries/%d" % code_point - put_data = json.dumps(code_point_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating QoS DSCP map entry for code point '%d' failed with status code %d: %s" - % (code_point, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating QoS DSCP map entry for code point '%d' succeeded" - % code_point) - return True - - -def get_all_classes(**kwargs): - """ - Perform a GET call to get a list of all traffic classes - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic classes in the table - """ - - if kwargs["url"].endswith("/v1/"): - traffic_classes = _get_all_classes_v1(**kwargs) - else: # Updated else for when version is v10.04 - traffic_classes = _get_all_classes(**kwargs) - return traffic_classes - - -def _get_all_classes_v1(**kwargs): - """ - Perform a GET call to get a list of all traffic classes - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic classes in the table - """ - target_url = kwargs["url"] + "system/classes" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all traffic classes failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all traffic classes succeeded") - - traffic_classes_list = response.json() - return traffic_classes_list - - -def _get_all_classes(**kwargs): - """ - Perform a GET call to get a dictionary containing all traffic classes - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic classes in the table - """ - target_url = kwargs["url"] + "system/classes" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all traffic classes failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all traffic classes succeeded") - - traffic_classes_dict = response.json() - return traffic_classes_dict - - -def get_traffic_class(class_name, class_type, **kwargs): - """ - Perform a GET call to get details of a particular traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular traffic class - """ - if kwargs["url"].endswith("/v1/"): - traffic_class = _get_traffic_class_v1(class_name, class_type, **kwargs) - else: # Updated else for when version is v10.04 - traffic_class = _get_traffic_class(class_name, class_type, **kwargs) - return traffic_class - - -def _get_traffic_class_v1(class_name, class_type, **kwargs): - """ - Perform a GET call to get details of a particular traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular traffic class - """ - target_url = kwargs["url"] + "system/classes/%s/%s" % (class_name, class_type) - - payload = {"selector": "configuration"} - - response = kwargs["s"].get(target_url, params=payload, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting '%s' traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting '%s' traffic class '%s' succeeded" % (class_type, class_name)) - - traffic_class = response.json() - return traffic_class - - -def _get_traffic_class(class_name, class_type, **kwargs): - """ - Perform a GET call to get details of a particular traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular traffic class - """ - target_url = kwargs["url"] + "system/classes/%s,%s" % (class_name, class_type) - - payload = {"selector": "writable", "depth": 2} - - response = kwargs["s"].get(target_url, params=payload, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting '%s' traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting '%s' traffic class '%s' succeeded" % (class_type, class_name)) - - traffic_class = response.json() - return traffic_class - - -def create_traffic_class(class_name, class_type, **kwargs): - """ - Perform a POST call to create a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_traffic_class_v1(class_name, class_type, **kwargs) - else: # Updated else for when version is v10.04 - return _create_traffic_class(class_name, class_type, **kwargs) - - -def _create_traffic_class_v1(class_name, class_type, **kwargs): - """ - Perform a POST call to create a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_classes_list = get_all_classes(**kwargs) - - # Traffic class doesn't exist; create it - if "/rest/v1/system/classes/%s/%s" % (class_name, class_type) not in traffic_classes_list: - - traffic_class_data = { - "name": class_name, - "type": class_type - } - - target_url = kwargs["url"] + "system/classes" - post_data = json.dumps(traffic_class_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating %s traffic class '%s' succeeded" - % (class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to create %s traffic class '%s' since it already exists" - % (class_type, class_name)) - return True - - -def _create_traffic_class(class_name, class_type, **kwargs): - """ - Perform a POST call to create a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_classes_dict = _get_all_classes(**kwargs) - - # Traffic class doesn't exist; create it - if "%s,%s" % (class_name, class_type) not in traffic_classes_dict: - - traffic_class_data = { - "name": class_name, - "type": class_type - } - - target_url = kwargs["url"] + "system/classes" - post_data = json.dumps(traffic_class_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating %s traffic class '%s' succeeded" - % (class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to create %s traffic class '%s' since it already exists" - % (class_type, class_name)) - return True - - -def update_traffic_class(class_name, class_type, **kwargs): - """ - Perform a PUT call to version-up a traffic class. This is required whenever entries of a traffic class are changed - in any way. - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _update_traffic_class_v1(class_name, class_type, **kwargs) - else: # Updated else for when version is v10.04 - return _update_traffic_class(class_name, class_type, **kwargs) - - -def _update_traffic_class_v1(class_name, class_type, **kwargs): - """ - Perform a PUT call to version-up a traffic class. This is required whenever entries of a traffic class are changed - in any way. - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - traffic_class_data = get_traffic_class(class_name, class_type, **kwargs) - - # must remove these fields from the data since they can't be modified - traffic_class_data.pop('origin', None) - traffic_class_data.pop('name', None) - traffic_class_data.pop('type', None) - - traffic_class_data['cfg_version'] = random.randrange(9007199254740991) - - target_url = kwargs["url"] + "system/classes/%s/%s" % (class_name, class_type) - put_data = json.dumps(traffic_class_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s traffic class '%s' succeeded" % (class_type, class_name)) - return True - - -def _update_traffic_class(class_name, class_type, **kwargs): - """ - Perform a PUT call to version-up a traffic class. This is required whenever entries of a traffic class are changed - in any way. - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - traffic_class_data = _get_traffic_class(class_name, class_type, **kwargs) - - # # must remove these fields from the data since they can't be modified - # traffic_class_data.pop('origin', None) - # traffic_class_data.pop('name', None) - # traffic_class_data.pop('type', None) - - traffic_class_data['cfg_version'] = random.randrange(9007199254740991) - - target_url = kwargs["url"] + "system/classes/%s,%s" % (class_name, class_type) - put_data = json.dumps(traffic_class_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s traffic class '%s' succeeded" % (class_type, class_name)) - return True - - -def get_all_traffic_class_entries(class_name, class_type, **kwargs): - """ - Perform a GET call to get a list of all traffic class entries - - :param class_name: Alphanumeric name of traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic class entries in the table - """ - - if kwargs["url"].endswith("/v1/"): - traffic_class_entries = _get_all_traffic_class_entries_v1(class_name, class_type, **kwargs) - else: # Updated else for when version is v10.04 - traffic_class_entries = _get_all_traffic_class_entries(class_name, class_type, **kwargs) - return traffic_class_entries - - -def _get_all_traffic_class_entries_v1(class_name, class_type, **kwargs): - """ - Perform a GET call to get a list of all traffic class entries - - :param class_name: Alphanumeric name of traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic class entries in the table - """ - target_url = kwargs["url"] + "system/classes/%s/%s/cfg_entries" % (class_name, class_type) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all entries of %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all entries of %s traffic class '%s' succeeded" % (class_type, class_name)) - - traffic_class_entries_list = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary - if not traffic_class_entries_list: - return {} - else: - return traffic_class_entries_list - - -def _get_all_traffic_class_entries(class_name, class_type, **kwargs): - """ - Perform a GET call to get a dictionary containing all traffic class entries - - :param class_name: Alphanumeric name of traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all traffic class entries in the table - """ - target_url = kwargs["url"] + "system/classes/%s,%s/cfg_entries" % (class_name, class_type) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all entries of %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all entries of %s traffic class '%s' succeeded" % (class_type, class_name)) - - traffic_class_entries_dict = response.json() - - return traffic_class_entries_dict - - -def create_traffic_class_entry(class_name, class_type, action, sequence_num, ip_protocol=None, src_ip=None, - dest_ip=None, **kwargs): - """ - Perform a POST call to create a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param action: Action should be either "match" or "ignore" - :param sequence_num: Integer ID for the entry. - :param ip_protocol: Optional integer IP protocol number. Defaults to None if not specified. Excluding this parameter - will make the entry associate to all IP protocols. - :param src_ip: Optional source IP address. Defaults to None if not specified. - :param dest_ip: Optional destination IP address. Defaults to None if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _create_traffic_class_entry_v1(class_name, class_type, action, sequence_num, ip_protocol, src_ip, - dest_ip, **kwargs) - else: # Updated else for when version is v10.04 - return _create_traffic_class_entry(class_name, class_type, action, sequence_num, ip_protocol, src_ip, - dest_ip, **kwargs) - - -def _create_traffic_class_entry_v1(class_name, class_type, action, sequence_num, ip_protocol=None, src_ip=None, - dest_ip=None, **kwargs): - """ - Perform a POST call to create a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param action: Action should be either "match" or "ignore" - :param sequence_num: Integer ID for the entry. - :param ip_protocol: Optional integer IP protocol number. Defaults to None if not specified. Excluding this parameter - will make the entry associate to all IP protocols. - :param src_ip: Optional source IP address. Defaults to None if not specified. - :param dest_ip: Optional destination IP address. Defaults to None if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_class_entries_dict = get_all_traffic_class_entries(class_name, class_type, **kwargs) - - # Traffic class entry doesn't exist; create it - if "/rest/v1/system/classes/%s/%s/cfg_entries/%d" % (class_name, class_type, sequence_num) \ - not in traffic_class_entries_dict.values(): - - traffic_class_entry_data = { - "type": action, - "sequence_number": sequence_num - } - - if ip_protocol is not None: - traffic_class_entry_data['protocol'] = ip_protocol - - if src_ip is not None: - traffic_class_entry_data['src_ip'] = src_ip - - if dest_ip is not None: - traffic_class_entry_data['dst_ip'] = dest_ip - - target_url = kwargs["url"] + "system/classes/%s/%s/cfg_entries" % (class_name, class_type) - post_data = json.dumps(traffic_class_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating traffic class entry %d for '%s' traffic class '%s' failed with status code %d: %s" - % (sequence_num, class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating traffic class entry %d for '%s' traffic class '%s' succeeded" - % (sequence_num, class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for %s traffic class '%s' since it already exists" - % (sequence_num, class_type, class_name)) - return True - - -def _create_traffic_class_entry(class_name, class_type, action, sequence_num, ip_protocol=None, src_ip=None, - dest_ip=None, **kwargs): - """ - Perform a POST call to create a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param action: Action should be either "match" or "ignore" - :param sequence_num: Integer ID for the entry. - :param ip_protocol: Optional integer IP protocol number. Defaults to None if not specified. Excluding this parameter - will make the entry associate to all IP protocols. - :param src_ip: Optional source IP address. Defaults to None if not specified. - :param dest_ip: Optional destination IP address. Defaults to None if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_class_entries_dict = _get_all_traffic_class_entries(class_name, class_type, **kwargs) - - # Traffic class entry doesn't exist; create it - if "%d" % sequence_num not in traffic_class_entries_dict: - - traffic_class_entry_data = { - "type": action, - "sequence_number": sequence_num - } - - if ip_protocol is not None: - traffic_class_entry_data['protocol'] = ip_protocol - - if src_ip is not None: - traffic_class_entry_data['src_ip'] = src_ip - - if dest_ip is not None: - traffic_class_entry_data['dst_ip'] = dest_ip - - target_url = kwargs["url"] + "system/classes/%s,%s/cfg_entries" % (class_name, class_type) - post_data = json.dumps(traffic_class_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating traffic class entry %d for '%s' traffic class '%s' failed with status code %d: %s" - % (sequence_num, class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating traffic class entry %d for '%s' traffic class '%s' succeeded" - % (sequence_num, class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for %s traffic class '%s' since it already exists" - % (sequence_num, class_type, class_name)) - return True - - -def get_all_policies(**kwargs): - """ - Perform a GET call to get a list of all classifier policies - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all classifier policies in the table - """ - - if kwargs["url"].endswith("/v1/"): - policies = _get_all_policies_v1(**kwargs) - else: # Updated else for when version is v10.04 - policies = _get_all_policies(**kwargs) - return policies - - -def _get_all_policies_v1(**kwargs): - """ - Perform a GET call to get a list of all classifier policies - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all classifier policies in the table - """ - target_url = kwargs["url"] + "system/policies" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all classifier policies failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all classifier policies succeeded") - - policies_list = response.json() - return policies_list - - -def _get_all_policies(**kwargs): - """ - Perform a GET call to get a dictionary containing all classifier policies - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing all classifier policies in the table - """ - target_url = kwargs["url"] + "system/policies" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all classifier policies failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all classifier policies succeeded") - - policies_dict = response.json() - return policies_dict - - -def create_policy(policy_name, **kwargs): - """ - Perform a POST call to create a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_policy_v1(policy_name, **kwargs) - else: # Updated else for when version is v10.04 - return _create_policy(policy_name, **kwargs) - - -def _create_policy_v1(policy_name, **kwargs): - """ - Perform a POST call to create a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policies_list = get_all_policies(**kwargs) - - # Policy doesn't exist; create it - if "/rest/v1/system/policies/%s" % policy_name not in policies_list: - - policy_data = {"name": policy_name} - - target_url = kwargs["url"] + "system/policies" - post_data = json.dumps(policy_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating classifier policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating classifier policy '%s' succeeded" % policy_name) - return True - else: - logging.info("SUCCESS: No need to create classifier policy '%s' since it already exists" - % policy_name) - return True - - -def _create_policy(policy_name, **kwargs): - """ - Perform a POST call to create a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policies_dict = _get_all_policies(**kwargs) - - # Policy doesn't exist; create it - if policy_name not in policies_dict: - - policy_data = {"name": policy_name} - - target_url = kwargs["url"] + "system/policies" - post_data = json.dumps(policy_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating classifier policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating classifier policy '%s' succeeded" % policy_name) - return True - else: - logging.info("SUCCESS: No need to create classifier policy '%s' since it already exists" - % policy_name) - return True - - -def get_all_policy_entries(policy_name, **kwargs): - """ - Perform a GET call to get a list of all policy entries - - :param policy_name: Alphanumeric name of the classifier policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all policy entries in the table - """ - if kwargs["url"].endswith("/v1/"): - policy_entries = _get_all_policy_entries_v1(policy_name, **kwargs) - else: # Updated else for when version is v10.04 - policy_entries = _get_all_policy_entries(policy_name, **kwargs) - return policy_entries - - -def _get_all_policy_entries_v1(policy_name, **kwargs): - """ - Perform a GET call to get a list of all policy entries - - :param policy_name: Alphanumeric name of the classifier policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of all policy entries in the table - """ - target_url = kwargs["url"] + "system/policies/%s/cfg_entries" % policy_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all entries of policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all entries of policy '%s' succeeded" % policy_name) - - policy_entries_list = response.json() - - # for some reason, this API returns a list when empty, and a dictionary when there is data - # make this function always return a dictionary - if not policy_entries_list: - return {} - else: - return policy_entries_list - - -def _get_all_policy_entries(policy_name, **kwargs): - """ - Perform a GET call to get a dictionary containing all policy entries - - :param policy_name: Alphanumeric name of the classifier policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing all policy entries in the table - """ - target_url = kwargs["url"] + "system/policies/%s/cfg_entries" % policy_name - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all entries of policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list of all entries of policy '%s' succeeded" % policy_name) - - policy_entries_dict = response.json() - - return policy_entries_dict - - -def create_policy_entry(policy_name, class_name, class_type, sequence_num, **kwargs): - """ - Perform a POST call to create a policy entry - - :param policy_name: Alphanumeric name of the policy - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_policy_entry_v1(policy_name, class_name, class_type, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - return _create_policy_entry(policy_name, class_name, class_type, sequence_num, **kwargs) - - -def _create_policy_entry_v1(policy_name, class_name, class_type, sequence_num, **kwargs): - """ - Perform a POST call to create a policy entry - - :param policy_name: Alphanumeric name of the policy - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = get_all_policy_entries(policy_name, **kwargs) - - # Policy entry doesn't exist; create it - if "/rest/v1/system/policies/%s/cfg_entries/%d" % (policy_name, sequence_num) not in policy_entries_dict.values(): - - policy_entry_data = { - "class": "/rest/v1/system/classes/%s/%s" % (class_name, class_type), - "sequence_number": sequence_num - } - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries" % policy_name - post_data = json.dumps(policy_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for policy '%s' since it already exists" - % (sequence_num, policy_name)) - return True - - -def _create_policy_entry(policy_name, class_name, class_type, sequence_num, **kwargs): - """ - Perform a POST call to create a policy entry - - :param policy_name: Alphanumeric name of the policy - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = _get_all_policy_entries(policy_name, **kwargs) - - # Policy entry doesn't exist; create it - if "%d" % sequence_num not in policy_entries_dict: - - policy_entry_data = { - "class": "/rest/v10.04/system/classes/%s,%s" % (class_name, class_type), - "sequence_number": sequence_num - } - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries" % policy_name - post_data = json.dumps(policy_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating entry %d for policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating entry %d for policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to create entry %d for policy '%s' since it already exists" - % (sequence_num, policy_name)) - return True - - -def get_policy_entry_action(policy_name, sequence_num, **kwargs): - """ - Perform a GET call to get the action set on a particular policy entry - - :param policy_name: Alphanumeric name of policy - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about the action of a particular policy entry - """ - if kwargs["url"].endswith("/v1/"): - policy_entry_action = _get_policy_entry_action_v1(policy_name, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - policy_entry_action = _get_policy_entry_action(policy_name, sequence_num, **kwargs) - return policy_entry_action - - -def _get_policy_entry_action_v1(policy_name, sequence_num, **kwargs): - """ - Perform a GET call to get the action set on a particular policy entry - - :param policy_name: Alphanumeric name of policy - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about the action of a particular policy entry - """ - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%s/policy_action_set" % (policy_name, sequence_num) - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting action of entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting action of entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - - policy_entry_action = response.json() - - # for some reason, the GET API for policy entry action returns an list if there is no data, - # and a dictionary if there is data - # make it always returna dictionary - if not policy_entry_action: - return {} - else: - return policy_entry_action - - -def _get_policy_entry_action(policy_name, sequence_num, **kwargs): - """ - Perform a GET call to get the action set on a particular policy entry - - :param policy_name: Alphanumeric name of policy - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about the action of a particular policy entry - """ - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%s/policy_action_set" % (policy_name, sequence_num) - - payload = { - "depth": 2, - "selector": "writable" - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if response: - policy_entry_action_dict = response.json() - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting action of entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting action of entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - else: - policy_entry_action_dict = {} - logging.info("SUCCESS: Getting action of entry %d in policy '%s' succeeded with an empty value" - % (sequence_num, policy_name)) - - return policy_entry_action_dict - - -def create_policy_entry_action(policy_name, sequence_num, dscp=None, pcp=None, **kwargs): - """ - Perform a POST call to create a policy entry action - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param dscp: Optional integer DSCP value to set matched packets to - :param pcp: Optional integer PCP value to set matched packets to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_policy_entry_action_v1(policy_name, sequence_num, dscp, pcp, **kwargs) - else: # Updated else for when version is v10.04 - return _create_policy_entry_action(policy_name, sequence_num, dscp, pcp, **kwargs) - - -def _create_policy_entry_action_v1(policy_name, sequence_num, dscp=None, pcp=None, **kwargs): - """ - Perform a POST call to create a policy entry action - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param dscp: Optional integer DSCP value to set matched packets to - :param pcp: Optional integer PCP value to set matched packets to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - policy_entry_action = get_policy_entry_action(policy_name, sequence_num, **kwargs) - - # Policy entry action doesn't exist; create it - if policy_entry_action == {}: - - policy_entry_action_data = {} - - if dscp is not None: - policy_entry_action_data['dscp'] = dscp - - if pcp is not None: - policy_entry_action_data['pcp'] = pcp - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%s/policy_action_set" % (policy_name, sequence_num) - post_data = json.dumps(policy_entry_action_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating action for entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating action for entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to create action for entry %d in policy '%s' since it already exists" - % (sequence_num, policy_name)) - return True - - -def _create_policy_entry_action(policy_name, sequence_num, dscp=None, pcp=None, **kwargs): - """ - Perform a POST call to create a policy entry action - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param dscp: Optional integer DSCP value to set matched packets to - :param pcp: Optional integer PCP value to set matched packets to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - policy_entry_action = get_policy_entry_action(policy_name, sequence_num, **kwargs) - - # Policy entry action doesn't exist; create it - if policy_entry_action == {}: - - policy_entry_action_data = {} - - if dscp is not None: - policy_entry_action_data['dscp'] = dscp - - if pcp is not None: - policy_entry_action_data['pcp'] = pcp - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%s/policy_action_set" % (policy_name, sequence_num) - post_data = json.dumps(policy_entry_action_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating action for entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating action for entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to create action for entry %d in policy '%s' since it already exists" - % (sequence_num, policy_name)) - return True - - -def get_policy(policy_name, **kwargs): - """ - Perform a GET call to get details of a particular policy - - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular policy - """ - - if kwargs["url"].endswith("/v1/"): - policy = _get_policy_v1(policy_name, **kwargs) - else: # Updated else for when version is v10.04 - policy = _get_policy(policy_name, **kwargs) - return policy - - -def _get_policy_v1(policy_name, **kwargs): - """ - Perform a GET call to get details of a particular policy - - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular policy - """ - target_url = kwargs["url"] + "system/policies/%s" % policy_name - - payload = {"selector": "configuration"} - - response = kwargs["s"].get(target_url, params=payload, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting policy '%s' succeeded" % policy_name) - - policy = response.json() - return policy - - -def _get_policy(policy_name, **kwargs): - """ - Perform a GET call to get details of a particular policy - - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing data about a particular policy - """ - target_url = kwargs["url"] + "system/policies/%s" % policy_name - - payload = {"selector": "writable", "depth": 2} - - response = kwargs["s"].get(target_url, params=payload, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting policy '%s' succeeded" % policy_name) - - policy = response.json() - return policy - - -def update_policy(policy_name, **kwargs): - """ - Perform a PUT call to version-up a policy - - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - policy_data = get_policy(policy_name, **kwargs) - - policy_data.pop('origin', None) - policy_data.pop('name', None) - policy_data.pop('type', None) - - policy_data['cfg_version'] = random.randrange(9007199254740991) - - target_url = kwargs["url"] + "system/policies/%s" % policy_name - put_data = json.dumps(policy_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating policy '%s' failed with status code %d: %s" % ( - policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating policy '%s' succeeded" % policy_name) - return True - - -def delete_queue_profile(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_queue_profile_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_queue_profile(profile_name, **kwargs) - - -def _delete_queue_profile_v1(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - queue_profiles_list = get_all_queue_profiles(**kwargs) - - if "/rest/v1/system/q_profiles/%s" % profile_name in queue_profiles_list: - - target_url = kwargs["url"] + "system/q_profiles/%s" % profile_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting QoS queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting QoS queue profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to remove QoS queue profile '%s' since it doesn't exist" % profile_name) - return True - - -def _delete_queue_profile(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile - - :param profile_name: Alphanumeric name of the queue profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - queue_profiles_dict = _get_all_queue_profiles(**kwargs) - - if profile_name in queue_profiles_dict: - - target_url = kwargs["url"] + "system/q_profiles/%s" % profile_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting QoS queue profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting QoS queue profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to remove QoS queue profile '%s' since it doesn't exist" % profile_name) - return True - - -def delete_queue_profile_entry(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile entry - - :param profile_name: Alphanumeric name of the queue profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_queue_profile_entry_v1(profile_name, queue_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_queue_profile_entry(profile_name, queue_num, **kwargs) - - -def _delete_queue_profile_entry_v1(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile entry - - :param profile_name: Alphanumeric name of the queue profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - queue_profiles_entries_dict = get_all_queue_profile_entries(profile_name, **kwargs) - - if "/rest/v1/system/q_profiles/%s/q_profile_entries/%d" % (profile_name, queue_num) in \ - queue_profiles_entries_dict.values(): - - target_url = kwargs["url"] + "system/q_profiles/%s/q_profile_entries/%d" % (profile_name, queue_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in QoS queue profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in QoS queue profile '%s' succeeded" % (queue_num, profile_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in QoS queue profile '%s' since it doesn't exist" - % (queue_num, profile_name)) - return True - - -def _delete_queue_profile_entry(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS queue profile entry - - :param profile_name: Alphanumeric name of the queue profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - queue_profiles_entries_dict = _get_all_queue_profile_entries(profile_name, **kwargs) - - if "%d" % queue_num in queue_profiles_entries_dict: - - target_url = kwargs["url"] + "system/q_profiles/%s/q_profile_entries/%d" % (profile_name, queue_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in QoS queue profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in QoS queue profile '%s' succeeded" % (queue_num, profile_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in QoS queue profile '%s' since it doesn't exist" - % (queue_num, profile_name)) - return True - - -def delete_schedule_profile(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_schedule_profile_v1(profile_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_schedule_profile(profile_name, **kwargs) - - -def _delete_schedule_profile_v1(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profiles_list = get_all_schedule_profiles(**kwargs) - - if "/rest/v1/system/qos/%s" % profile_name in schedule_profiles_list: - - target_url = kwargs["url"] + "system/qos/%s" % profile_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting QoS schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting QoS schedule profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to remove QoS schedule profile '%s' since it doesn't exist" % profile_name) - return True - - -def _delete_schedule_profile(profile_name, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile - - :param profile_name: Alphanumeric name of the schedule profile - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profiles_dict = _get_all_schedule_profiles(**kwargs) - - if profile_name in schedule_profiles_dict: - - target_url = kwargs["url"] + "system/qos/%s" % profile_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting QoS schedule profile '%s' failed with status code %d: %s" - % (profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting QoS schedule profile '%s' succeeded" % profile_name) - return True - else: - logging.info("SUCCESS: No need to remove QoS schedule profile '%s' since it doesn't exist" % profile_name) - return True - - -def delete_schedule_profile_entry(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile entry - - :param profile_name: Alphanumeric name of the schedule profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_schedule_profile_entry_v1(profile_name, queue_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_schedule_profile_entry(profile_name, queue_num, **kwargs) - - -def _delete_schedule_profile_entry_v1(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile entry - - :param profile_name: Alphanumeric name of the schedule profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - schedule_profile_entries_dict = get_all_schedule_profile_entries(profile_name, **kwargs) - - if "/rest/v1/system/qos/%s/queues/%d" % (profile_name, queue_num) in schedule_profile_entries_dict.values(): - - target_url = kwargs["url"] + "system/qos/%s/queues/%d" % (profile_name, queue_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in QoS schedule profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in QoS schedule profile '%s' succeeded" - % (queue_num, profile_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in QoS schedule profile '%s' since it doesn't exist" - % (queue_num, profile_name)) - return True - - -def _delete_schedule_profile_entry(profile_name, queue_num, **kwargs): - """ - Perform a DELETE call to delete a QoS schedule profile entry - - :param profile_name: Alphanumeric name of the schedule profile - :param queue_num: Integer number of the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - schedule_profile_entries_dict = _get_all_schedule_profile_entries(profile_name, **kwargs) - - if "%d" % queue_num in schedule_profile_entries_dict: - - target_url = kwargs["url"] + "system/qos/%s/queues/%d" % (profile_name, queue_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in QoS schedule profile '%s' failed with status code %d: %s" - % (queue_num, profile_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in QoS schedule profile '%s' succeeded" - % (queue_num, profile_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in QoS schedule profile '%s' since it doesn't exist" - % (queue_num, profile_name)) - return True - - -def unapply_profiles_globally(**kwargs): - """ - Perform GET and PUT calls to remove global application of QoS queue profile and schedule profile on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _unapply_profiles_globally_v1(**kwargs) - else: # Updated else for when version is v10.04 - return _unapply_profiles_globally(**kwargs) - - -def _unapply_profiles_globally_v1(**kwargs): - """ - Perform GET and PUT calls to remove global application of QoS queue profile and schedule profile on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data.pop('q_profile_default', None) - system_data.pop('qos_default', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing global application of queue profile and schedule profile failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing global application of queue profile and schedule profile succeeded") - return True - - -def _unapply_profiles_globally(**kwargs): - """ - Perform GET and PUT calls to remove global application of QoS queue profile and schedule profile on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data.pop('q_profile_default', None) - system_data.pop('qos_default', None) - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing global application of queue profile and schedule profile failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing global application of queue profile and schedule profile succeeded") - return True - - -def clear_trust_globally(**kwargs): - """ - Perform GET and PUT calls to remove global setting of QoS trust mode on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _clear_trust_globally_v1(**kwargs) - else: # Updated else for when version is v10.04 - return _clear_trust_globally(**kwargs) - - -def _clear_trust_globally_v1(**kwargs): - """ - Perform GET and PUT calls to remove global setting of QoS trust mode on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"selector": "configuration"}, **kwargs) - - system_data.pop('qos_config', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing global setting QoS trust mode failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing global setting QoS trust mode failed succeeded") - return True - - -def _clear_trust_globally(**kwargs): - """ - Perform GET and PUT calls to remove global setting of QoS trust mode on all interfaces. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_data = system.get_system_info(params={"depth": 1, "selector": "writable"}, **kwargs) - - system_data.pop('qos_config', None) - system_data.pop('syslog_remotes', None) - system_data.pop('vrfs', None) - system_data.pop('mirrors', None) - system_data.pop('all_user_copp_policies', None) - - target_url = kwargs["url"] + "system" - put_data = json.dumps(system_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing global setting QoS trust mode failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing global setting QoS trust mode failed succeeded") - return True - -# Same for v1 and v3? -def reset_dscp_entry(code_point, **kwargs): - """ - Perform a PUT call to reset the DSCP code point entry to its default setting. - - :param code_point: Integer identifying the DSCP map code point entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - code_point_entry_data = {} - - target_url = kwargs["url"] + "system/qos_dscp_map_entries/%d" % code_point - put_data = json.dumps(code_point_entry_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Resetting QoS DSCP map entry for code point '%d' to default failed with status code %d: %s" - % (code_point, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Resetting QoS DSCP map entry for code point '%d' to default succeeded" - % code_point) - return True - - -def delete_traffic_class(class_name, class_type, **kwargs): - """ - Perform a DELETE call to delete a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_traffic_class_v1(class_name, class_type, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_traffic_class(class_name, class_type, **kwargs) - - -def _delete_traffic_class_v1(class_name, class_type, **kwargs): - """ - Perform a DELETE call to delete a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_classes_list = get_all_classes(**kwargs) - - if "/rest/v1/system/classes/%s/%s" % (class_name, class_type) in traffic_classes_list: - - target_url = kwargs["url"] + "system/classes/%s/%s" % (class_name, class_type) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting %s traffic class '%s' succceeded" - % (class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to delete %s traffic class '%s' since it doesn't exist" - % (class_type, class_name)) - return True - - -def _delete_traffic_class(class_name, class_type, **kwargs): - """ - Perform a DELETE call to delete a traffic class - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_classes_dict = _get_all_classes(**kwargs) - - if "%s,%s" % (class_name, class_type) in traffic_classes_dict: - - target_url = kwargs["url"] + "system/classes/%s,%s" % (class_name, class_type) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting %s traffic class '%s' failed with status code %d: %s" - % (class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting %s traffic class '%s' succceeded" - % (class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to delete %s traffic class '%s' since it doesn't exist" - % (class_type, class_name)) - return True - - -def delete_traffic_class_entry(class_name, class_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac"= - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_traffic_class_entry_v1(class_name, class_type, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_traffic_class_entry(class_name, class_type, sequence_num, **kwargs) - - -def _delete_traffic_class_entry_v1(class_name, class_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac"= - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_class_entries_dict = get_all_traffic_class_entries(class_name, class_type, **kwargs) - - if "/rest/v1/system/classes/%s/%s/cfg_entries/%d" % (class_name, class_type, sequence_num) \ - in traffic_class_entries_dict.values(): - - target_url = kwargs["url"] + "system/classes/%s/%s/cfg_entries/%d" % (class_name, class_type, sequence_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in %s traffic class '%s' failed with status code %d: %s" - % (sequence_num, class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in %s traffic class '%s' succeeded" - % (sequence_num, class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in %s traffic class '%s' since it doesn't exist" - % (sequence_num, class_type, class_name)) - return True - - -def _delete_traffic_class_entry(class_name, class_type, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a traffic class entry - - :param class_name: Alphanumeric name of the traffic class - :param class_type: Class type should be one of "ipv4," "ipv6," or "mac" - :param sequence_num: Integer ID for the entry. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - traffic_class_entries_dict = _get_all_traffic_class_entries(class_name, class_type, **kwargs) - - if "%d" % sequence_num in traffic_class_entries_dict: - - target_url = kwargs["url"] + "system/classes/%s,%s/cfg_entries/%d" % (class_name, class_type, sequence_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in %s traffic class '%s' failed with status code %d: %s" - % (sequence_num, class_type, class_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in %s traffic class '%s' succeeded" - % (sequence_num, class_type, class_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in %s traffic class '%s' since it doesn't exist" - % (sequence_num, class_type, class_name)) - return True - - -def delete_policy(policy_name, **kwargs): - """ - Perform a DELETE call to delete a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_policy_v1(policy_name, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_policy(policy_name, **kwargs) - - -def _delete_policy_v1(policy_name, **kwargs): - """ - Perform a DELETE call to delete a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policies_list = get_all_policies(**kwargs) - - if "/rest/v1/system/policies/%s" % policy_name in policies_list: - - target_url = kwargs["url"] + "system/policies/%s" % policy_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting classifier policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting classifier policy '%s' succeeded" % policy_name) - return True - else: - logging.info("SUCCESS: No need to delete classifier policy '%s' since it doesn't exist" % policy_name) - return True - - -def _delete_policy(policy_name, **kwargs): - """ - Perform a DELETE call to delete a classifier policy - - :param policy_name: Alphanumeric name of policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policies_dict = _get_all_policies(**kwargs) - - if policy_name in policies_dict: - - target_url = kwargs["url"] + "system/policies/%s" % policy_name - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting classifier policy '%s' failed with status code %d: %s" - % (policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting classifier policy '%s' succeeded" % policy_name) - return True - else: - logging.info("SUCCESS: No need to delete classifier policy '%s' since it doesn't exist" % policy_name) - return True - - -def delete_policy_entry(policy_name, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_policy_entry_v1(policy_name, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_policy_entry(policy_name, sequence_num, **kwargs) - - -def _delete_policy_entry_v1(policy_name, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = get_all_policy_entries(policy_name, **kwargs) - - if "/rest/v1/system/policies/%s/cfg_entries/%d" % (policy_name, sequence_num) in policy_entries_dict.values(): - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%d" % (policy_name, sequence_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in policy '%s' since it doesn't exist" - % (sequence_num, policy_name)) - return True - - -def _delete_policy_entry(policy_name, sequence_num, **kwargs): - """ - Perform a DELETE call to delete a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = _get_all_policy_entries(policy_name, **kwargs) - - if "%d" % sequence_num in policy_entries_dict: - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%d" % (policy_name, sequence_num) - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to delete entry %d in policy '%s' since it doesn't exist" - % (sequence_num, policy_name)) - return True - - -def delete_policy_entry_action(policy_name, sequence_num, **kwargs): - """ - Perform a PUT call to set no action on a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_policy_entry_action_v1(policy_name, sequence_num, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_policy_entry_action(policy_name, sequence_num, **kwargs) - - -def _delete_policy_entry_action_v1(policy_name, sequence_num, **kwargs): - """ - Perform a PUT call to set no action on a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = get_all_policy_entries(policy_name, **kwargs) - - if "/rest/v1/system/policies/%s/cfg_entries/%d" % (policy_name, sequence_num) in policy_entries_dict.values(): - - policy_entry_action_data = {} - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%d/policy_action_set" % (policy_name, sequence_num) - put_data = json.dumps(policy_entry_action_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting no action on entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting no action on entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to set no action on entry %d in policy '%s' since it doesn't exist" - % (sequence_num, policy_name)) - return True - - -def _delete_policy_entry_action(policy_name, sequence_num, **kwargs): - """ - Perform a PUT call to set no action on a policy entry - - :param policy_name: Alphanumeric name of the policy - :param sequence_num: Integer ID for the entry - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - policy_entries_dict = _get_all_policy_entries(policy_name, **kwargs) - - if "%d" % sequence_num in policy_entries_dict: - - policy_entry_action_data = {} - - target_url = kwargs["url"] + "system/policies/%s/cfg_entries/%d/policy_action_set" % (policy_name, sequence_num) - put_data = json.dumps(policy_entry_action_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Setting no action on entry %d in policy '%s' failed with status code %d: %s" - % (sequence_num, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Setting no action on entry %d in policy '%s' succeeded" % (sequence_num, policy_name)) - return True - else: - logging.info("SUCCESS: No need to set no action on entry %d in policy '%s' since it doesn't exist" - % (sequence_num, policy_name)) - return True - - -def clear_trust_interface(port_name, **kwargs): - """ - Perform GET and PUT calls to clear QoS trust mode on an interface. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _clear_trust_interface_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _clear_trust_interface(port_name, **kwargs) - - -def _clear_trust_interface_v1(port_name, **kwargs): - """ - Perform GET and PUT calls to clear QoS trust mode on an interface. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name_percents, depth=0, selector="configuration", **kwargs) - - port_data.pop('qos_config', None) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing QoS trust mode on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing QoS trust mode on Port '%s' succeeded" % port_name) - return True - - -def _clear_trust_interface(port_name, **kwargs): - """ - Perform GET and PUT calls to clear QoS trust mode on an interface. - - :param port_name: Alphanumeric name of the Port on which the trust mode is to be set - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - port_name_percents = common_ops._replace_special_characters(port_name) - int_data = interface.get_interface(port_name_percents, 1, "writable", **kwargs) - - if port_name.startswith('lag'): - if int_data['interfaces']: - int_data['interfaces'] = common_ops._dictionary_to_list_values(int_data['interfaces']) - - int_data.pop('qos_config', None) - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing QoS trust mode for Interface '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing QoS trust mode for Interface '%s' succeeded" - % port_name) - return True - - -def update_port_rate_limits(port_name, broadcast_limit=None, broadcast_units=None, - multicast_limit=None, multicast_units=None, unknown_unicast_limit=None, - unknown_unicast_units=None, **kwargs): - """ - Perform GET and PUT calls to update a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param broadcast_limit: Rate limit for broadcast ingress traffic - :param broadcast_units: Units for broadcast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param multicast_limit: Rate limit in pps for multicast ingress traffic - :param multicast_units: Units for multicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param unknown_unicast_limit: Rate limit in pps for unknown_unicast ingress traffic - :param unknown_unicast_units: Units for unknown unicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _update_port_rate_limits_v1(port_name, broadcast_limit, broadcast_units, - multicast_limit, multicast_units, unknown_unicast_limit, - unknown_unicast_units, **kwargs) - else: # Updated else for when version is v10.04 - return _update_port_rate_limits(port_name, broadcast_limit, broadcast_units, - multicast_limit, multicast_units, unknown_unicast_limit, - unknown_unicast_units, **kwargs) - - -def _update_port_rate_limits_v1(port_name, broadcast_limit=None, broadcast_units=None, - multicast_limit=None, multicast_units=None, unknown_unicast_limit=None, - unknown_unicast_units=None, **kwargs): - """ - Perform GET and PUT calls to update a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param broadcast_limit: Rate limit for broadcast ingress traffic - :param broadcast_units: Units for broadcast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param multicast_limit: Rate limit in pps for multicast ingress traffic - :param multicast_units: Units for multicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param unknown_unicast_limit: Rate limit in pps for unknown_unicast ingress traffic - :param unknown_unicast_units: Units for unknown unicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data['rate_limits'] = {} - - if broadcast_limit is not None and broadcast_units is not None: - port_data['rate_limits']['broadcast'] = broadcast_limit - port_data['rate_limits']['broadcast_units'] = broadcast_units - - if multicast_limit is not None and multicast_units is not None: - port_data['rate_limits']['multicast'] = multicast_limit - port_data['rate_limits']['multicast_units'] = multicast_units - - if unknown_unicast_limit is not None and unknown_unicast_units is not None: - port_data['rate_limits']['unknown-unicast'] = unknown_unicast_limit - port_data['rate_limits']['unknown-unicast_units'] = unknown_unicast_units - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating rate limits for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating rate limits for Port '%s' succeeded" - % port_name) - return True - - -def _update_port_rate_limits(port_name, broadcast_limit=None, broadcast_units=None, - multicast_limit=None, multicast_units=None, unknown_unicast_limit=None, - unknown_unicast_units=None, **kwargs): - """ - Perform GET and PUT calls to update a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param broadcast_limit: Rate limit for broadcast ingress traffic - :param broadcast_units: Units for broadcast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param multicast_limit: Rate limit in pps for multicast ingress traffic - :param multicast_units: Units for multicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param unknown_unicast_limit: Rate limit in pps for unknown_unicast ingress traffic - :param unknown_unicast_units: Units for unknown unicast rate limit; should be either "kbps" (kilobits/second) or - "pps" (packets/second) - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name, 2, "writable", **kwargs) - - int_data['rate_limits'] = {} - - if broadcast_limit is not None and broadcast_units is not None: - int_data['rate_limits']['broadcast'] = broadcast_limit - int_data['rate_limits']['broadcast_units'] = broadcast_units - - if multicast_limit is not None and multicast_units is not None: - int_data['rate_limits']['multicast'] = multicast_limit - int_data['rate_limits']['multicast_units'] = multicast_units - - if unknown_unicast_limit is not None and unknown_unicast_units is not None: - int_data['rate_limits']['unknown-unicast'] = unknown_unicast_limit - int_data['rate_limits']['unknown-unicast_units'] = unknown_unicast_units - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating rate limits for Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating rate limits for Port '%s' succeeded" - % port_name) - return True - - -def update_port_policy(port_name, policy_name, **kwargs): - """ - Perform GET and PUT calls to update a Port's policy - - :param port_name: Alphanumeric name of the Port - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_port_policy_v1(port_name, policy_name, **kwargs) - else: # Updated else for when version is v10.04 - return _update_port_policy(port_name, policy_name, **kwargs) - - -def _update_port_policy_v1(port_name, policy_name, **kwargs): - """ - Perform GET and PUT calls to update a Port's policy - - :param port_name: Alphanumeric name of the Port - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - port_data = port.get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data['policy_in_cfg'] = "/rest/v1/system/policies/%s" % policy_name - port_data['policy_in_cfg_version'] = random.randrange(9007199254740991) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating policy on Port '%s' to '%s' failed with status code %d: %s" - % (port_name, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating policy on Port '%s' to '%s' succeeded" - % (port_name, policy_name)) - return True - - -def _update_port_policy(port_name, policy_name, **kwargs): - """ - Perform GET and PUT calls to update a Port's policy - - :param port_name: Alphanumeric name of the Port - :param policy_name: Alphanumeric name of the policy - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name, 1, "writable", **kwargs) - - int_data['policy_in_cfg'] = "/rest/v10.04/system/policies/%s" % policy_name - int_data['policy_in_cfg_version'] = random.randrange(9007199254740991) - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating policy on Port '%s' to '%s' failed with status code %d: %s" - % (port_name, policy_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating policy on Port '%s' to '%s' succeeded" - % (port_name, policy_name)) - return True - - -def clear_port_policy(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's policy - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _clear_port_policy_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _clear_port_policy(port_name, **kwargs) - - -def _clear_port_policy_v1(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's policy - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data.pop('policy_in_cfg', None) - port_data.pop('policy_in_cfg_version', None) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing policy on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing policy on Port '%s' succeeded" - % port_name) - return True - - -def _clear_port_policy(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's policy - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name, 2, "writable", **kwargs) - - int_data.pop('policy_in_cfg', None) - int_data.pop('policy_in_cfg_version', None) - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing policy on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing policy on Port '%s' succeeded" - % port_name) - return True - - -def clear_port_rate_limits(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _clear_port_rate_limits_v1(port_name, **kwargs) - else: # Updated else for when version is v10.04 - return _clear_port_rate_limits(port_name, **kwargs) - - -def _clear_port_rate_limits_v1(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - port_name_percents = common_ops._replace_special_characters(port_name) - - port_data = port.get_port(port_name, depth=0, selector="configuration", **kwargs) - - port_data.pop('rate_limits', None) - - # must remove these fields from the data since they can't be modified - port_data.pop('name', None) - port_data.pop('origin', None) - - target_url = kwargs["url"] + "system/ports/%s" % port_name_percents - put_data = json.dumps(port_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing rate limits on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing rate limits on Port '%s' succeeded" - % port_name) - return True - - -def _clear_port_rate_limits(port_name, **kwargs): - """ - Perform GET and PUT calls to clear a Port's rate limits - - :param port_name: Alphanumeric name of the Port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - port_name_percents = common_ops._replace_special_characters(port_name) - - int_data = interface.get_interface(port_name, 2, "writable", **kwargs) - - int_data.pop('rate_limits', None) - - target_url = kwargs["url"] + "system/interfaces/%s" % port_name_percents - put_data = json.dumps(int_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Clearing rate limits on Port '%s' failed with status code %d: %s" - % (port_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Clearing rate limits on Port '%s' succeeded" - % port_name) - return True diff --git a/pyaoscx/rest/__init__.py b/pyaoscx/rest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyaoscx/rest/v1/__init__.py b/pyaoscx/rest/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyaoscx/rest/v1/api.py b/pyaoscx/rest/v1/api.py new file mode 100644 index 0000000..41d4b88 --- /dev/null +++ b/pyaoscx/rest/v1/api.py @@ -0,0 +1,163 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from datetime import date +import re + +from pyaoscx.api import API + + +class v1(API): + ''' + Represents a REST API Version 1. It keeps all the information + needed for the version and methods related to it. + ''' + + def __init__(self): + self.release_date = date(2017, 1, 1) + self.version = '1' + self.default_selector = 'configuration' + self.default_depth = 0 + self.default_facts_depth = 1 + self.default_subsystem_facts_depth = 4 + self.valid_selectors = ['configuration', 'status', 'statistics'] + self.configurable_selectors = ['configuration'] + self.compound_index_separator = '/' + self.valid_depths = [0, 1, 2, 3] + + def valid_depth(self, depth): + ''' + Verifies if given depth is valid for the current API version + :param depth: Integer + :return valid: Boolean True if depth is valid + ''' + valid = True + if depth not in self.valid_depths: + valid = False + return valid + + def get_index(self, obj): + ''' + Method used to obtain the correct format of the objects information + which depends on the Current API version + Example: + + 1) "keepalive_vrf" : "Resource uri" + :param obj: PyaoscxModule object + :return: Resource URI + ''' + # use object indices + return obj.get_uri() + + def get_module(self, session, module, index_id=None, **kwargs): + ''' + Create a module object given a response data and the module's type. + + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param module: Name representing the module which is about to be + created + :param index_id: The module ID + :return object: Return object same as module + ''' + + if module == 'Interface': + from pyaoscx.rest.v1.interface import Interface + + elif module == 'Ipv6': + from pyaoscx.ipv6 import Ipv6 + + elif module == 'Vlan': + from pyaoscx.vlan import Vlan + + elif module == 'Vrf': + from pyaoscx.vrf import Vrf + + elif module == 'Vsx': + from pyaoscx.vsx import Vsx + return Vsx(session, **kwargs) + + elif module == 'BgpRouter': + from pyaoscx.bgp_router import BgpRouter + + elif module == 'BgpNeighbor': + from pyaoscx.bgp_neighbor import BgpNeighbor + + elif module == 'VrfAddressFamily': + from pyaoscx.vrf_address_family import VrfAddressFamily + + elif module == 'OspfRouter': + from pyaoscx.ospf_router import OspfRouter + + elif module == 'OspfArea': + from pyaoscx.ospf_area import OspfArea + + elif module == 'OspfInterface': + from pyaoscx.ospf_interface import OspfInterface + + elif module == 'DhcpRelay': + from pyaoscx.dhcp_relay import DhcpRelay + + elif module == 'ACL': + from pyaoscx.acl import ACL + + elif module == 'AclEntry': + from pyaoscx.acl_entry import AclEntry + + elif module == 'AggregateAddress': + from pyaoscx.aggregate_address import AggregateAddress + + elif module == 'StaticRoute': + from pyaoscx.static_route import StaticRoute + + elif module == 'StaticNexthop': + from pyaoscx.static_nexthop import StaticNexthop + + else: + raise Exception("Invalid Module Name") + + return locals()[module](session, index_id, **kwargs) + + def get_keys(self, response_data, module_name): + ''' + Given a response_data String obtain the keys + of said String and return them + :param response_data: a String in the form of + "/rest/v1/system///" + + :return name_arr: List of keys + ''' + # Create regex string + regex_str = r'(.*)/' + re.escape(module_name) + r'/(?P.+)' + # Pattern expected + ids_pattern = re.compile(regex_str) + # Match pattern + indices = ids_pattern.match(response_data).group('ids') + # Get all indices + indices = indices.split('/') + + return indices + + def get_uri_from_data(self, data): + ''' + Given a response data, create a list of URI items. In this Version the + data is a list, string or dict. + :param data: String, List or Dictionary containing URI items + :return uri_list: Return the list of URIs + ''' + + if isinstance(data, list): + return data + elif isinstance(data, str): + return [data] + elif isinstance(data, dict): + uri_list = [] + for k, v in data.items(): + item = self.get_uri_from_data(v) + # if value is a list, then concatenate. + if isinstance(item, list): + uri_list += item + elif isinstance(item, str): + uri_list.append(item) + + return uri_list diff --git a/pyaoscx/rest/v1/interface.py b/pyaoscx/rest/v1/interface.py new file mode 100644 index 0000000..7b54dd2 --- /dev/null +++ b/pyaoscx/rest/v1/interface.py @@ -0,0 +1,974 @@ + +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + +from pyaoscx.ipv6 import Ipv6 +from pyaoscx.interface import Interface as AbstractInterface +from pyaoscx.vlan import Vlan +from pyaoscx.vrf import Vrf +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class Interface(AbstractInterface): + ''' + Provide configuration management for Interface and Ports for REST API Version 1. + Uses methods inside AbstractInterface and any ones different are + overridden by this class. + ''' + base_uri = "system/ports" + base_uri_ports = "system/ports" + base_uri_interface = "system/interfaces" + + indices = ['name'] + + ip6_addresses = ListDescriptor('ip6_addresses') + + def __init__(self, session, name, uri=None, **kwargs): + self.session = session + self._uri = None + + # List used to determine attributes related to the port configuration + self.config_attrs = [] + # List used to determine attributes related to the interface + # configuration + self.config_attrs_int = [] + self.materialized = False + + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes_int = {} + self.__original_attributes_port = {} + + # Set name, percents name and determine if Interface is a LAG + self.__set_name(name) + + # List of previous interfaces before update + # used to verify if a interface is deleted from lag + self.__prev_interfaces = [] + + # Use to manage IPv6 addresses + self.ip6_addresses = [] + + # Type required for configuration + self.type = None + # Set type + self.__set_type() + + # Check if data should be added to object + if self.__is_special_type: + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a Port table entry, a Interface + table entry and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. The options are 'configuration', 'status', or 'statistics. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving Port") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}" .format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri_ports = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_ports, + name=self.percents_name + ) + + # Bring Ports information + try: + response_ports = self.session.s.get( + uri_ports, verify=False, params=payload, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response_ports, "GET"): + raise GenericOperationError( + response_ports.text, + response_ports.status_code) + + data_port = json.loads(response_ports.text) + # Adding ACL attributes to data_port + # to then be added as attributes to the object + acl_names = [ + 'aclv6_in_cfg', + 'aclv4_in_cfg', + 'aclmac_in_cfg', + 'aclv4_out_cfg'] + + for acl_attr in acl_names: + data_port[acl_attr] = None + + # Delete ip6 addresses from incoming data + data_port.pop('ip6_addresses') + # Add Port dictionary as attributes for the object + utils.create_attrs(self, data_port) + + # Determines if the module is configurable + if selector in self.session.api_version.configurable_selectors: + # Get list of keys and create a list with the given keys + utils.set_config_attrs( + self, data_port, 'config_attrs', + ['name', 'origin', 'other_config', 'ip6_addresses']) + # Set original attributes + self.__original_attributes_port = data_port + # Delete unwanted attributes + if 'name' in self.__original_attributes_port: + self.__original_attributes_port.pop('name') + if 'ip6_addresses' in self.__original_attributes_port: + self.__original_attributes_port.pop('ip6_addresses') + if 'origin' in self.__original_attributes_port: + self.__original_attributes_port.pop('origin') + if 'other_config' in self.__original_attributes_port: + self.__original_attributes_port.pop('other_config') + + # Check if port is a LAG + # If not, makes get request to system/interfaces + if self.type != 'lag': + uri_interfaces = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_interface, + name=self.percents_name + ) + # Bring Interface information + try: + response_ints = self.session.s.get( + uri_interfaces, verify=False, + params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response_ints, "GET"): + raise GenericOperationError(response_ints.text, + response_ints.status_code) + + data_int = json.loads(response_ints.text) + # Add Interface dictionary as attributes for the object + utils.create_attrs(self, data_int) + + # Determines if the module is configurable + if selector in self.session.api_version.configurable_selectors: + # Get list of keys and create a list with the given keys + utils.set_config_attrs( + self, data_int, 'config_attrs_int', ['name', 'origin']) + # Set original attributes + self.__original_attributes_int = data_int + # Delete unwanted attributes + if 'name' in self.__original_attributes_int: + self.__original_attributes_int.pop('name') + if 'origin' in self.__original_attributes_int: + self.__original_attributes_int.pop('origin') + + # Set a list of interfaces as an attribute + if hasattr(self, 'interfaces') and self.interfaces is not None: + interfaces_list = [] + # Get all URI elements in the form of a list + uri_list = self.session.api_version.get_uri_from_data( + self.interfaces) + for uri in uri_list: + # Create an Interface object + name, interface = Interface.from_uri(self.session, uri) + + # Check for circular reference + # No need to get() if it's circular; it is already + # materialized. Just set flag + if name == self.name: + interface.materialized = True + else: + # Materialize interface + interface.get() + + # Add interface to list + interfaces_list.append(interface) + + # Set list as Interfaces + self.interfaces = interfaces_list + # Set list of interfaces + self.__prev_interfaces = list(self.interfaces) + + # Set VRF + if hasattr(self, 'vrf') and self.vrf is not None: + # Set keepalive VRF as a Vrf object + vrf_obj = Vrf.from_response(self.session, self.vrf) + self.vrf = vrf_obj + # Materialized VRF + self.vrf.get() + + # Set VLAN + if hasattr(self, 'vlan_tag') and self.vlan_tag is not None: + # Set VLAN as a Vlan Object + vlan_obj = Vlan.from_response(self.session, self.vlan_tag) + self.vlan_tag = vlan_obj + # Materialized VLAN + self.vlan_tag.get() + + # vlan_trunks + # Set a list of vlans as an attribute + if hasattr(self, 'vlan_trunks') and self.vlan_trunks is not None: + vlan_trunks = [] + # Get all URI elements in the form of a list + uri_list = self.session.api_version.get_uri_from_data( + self.vlan_trunks) + + for uri in uri_list: + # Create a Vlan object + vlan_id, vlan = Vlan.from_uri(self.session, uri) + # Materialize VLAN + vlan.get() + # Add VLAN to dictionary + vlan_trunks.append(vlan) + # Set list as VLANs + self.vlan_trunks = vlan_trunks + + # Set all ACLs + from pyaoscx.acl import ACL + if hasattr(self, 'aclmac_in_cfg') and self.aclmac_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclmac_in_cfg) + # Materialize Acl object + acl.get() + self.aclmac_in_cfg = acl + + if hasattr(self, 'aclmac_out_cfg') and self.aclmac_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclmac_out_cfg) + # Materialize Acl object + acl.get() + self.aclmac_out_cfg = acl + + if hasattr(self, 'aclv4_in_cfg') and self.aclv4_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_in_cfg) + # Materialize Acl object + acl.get() + self.aclv4_in_cfg = acl + + if hasattr(self, 'aclv4_out_cfg') and self.aclv4_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_out_cfg) + # Materialize Acl object + acl.get() + self.aclv4_out_cfg = acl + + if hasattr( + self, + 'aclv4_routed_in_cfg') and self.aclv4_routed_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_routed_in_cfg) + # Materialize Acl object + acl.get() + self.aclv4_routed_in_cfg = acl + + if hasattr( + self, + 'aclv4_routed_out_cfg') and self.aclv4_routed_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_routed_out_cfg) + # Materialize Acl object + acl.get() + self.aclv4_routed_out_cfg = acl + + if hasattr(self, 'aclv6_in_cfg') and self.aclv6_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_in_cfg) + # Materialize Acl object + acl.get() + self.aclv6_in_cfg = acl + + if hasattr(self, 'aclv6_out_cfg') and self.aclv6_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_out_cfg) + # Materialize Acl object + acl.get() + self.aclv6_out_cfg = acl + + if hasattr( + self, + 'aclv6_routed_in_cfg') and self.aclv6_routed_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_routed_in_cfg) + # Materialize Acl object + acl.get() + self.aclv6_routed_in_cfg = acl + + if hasattr( + self, + 'aclv6_routed_out_cfg') and self.aclv6_routed_out_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_routed_out_cfg) + # Materialize Acl object + acl.get() + self.aclv6_routed_out_cfg = acl + + # Set IPv6 addresses if any + if self.ip6_addresses == []: + # Set IPv6 addresses if any + # Loads IPv6 already into the Interface + Ipv6.get_all(self.session, self) + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + return True + + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system Ports and return a list + of them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing ports IDs as keys and a port objects + as values + ''' + logging.info("Retrieving the switch Interfaces and Ports") + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=Interface.base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + interfaces_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a Interface object + name, interface = Interface.from_uri(session, uri) + + interfaces_dict[name] = interface + + return interfaces_dict + + @classmethod + def from_uri(cls, session, uri): + ''' + Create an Interface object given a interface URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return name, Interface: tuple containing both the interface's name + and an Interface object + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)/(?P.+)') + name_percents = index_pattern.match(uri).group('index') + name = utils._replace_percents(name_percents) + # Create Interface object + interface_obj = Interface(session, name, uri=uri) + + return name, interface_obj + + @classmethod + def get_facts(cls, session): + ''' + Perform a GET call to retrieve all Interfaces and their respective data + :param cls: Class reference. + :param session: pyaoscx.Session object used to represent a logical + connection to the device. + :return facts: Dictionary containing Interface IDs as keys and Interface + objects as values + ''' + # Log + logging.info("Retrieving the switch interfaces facts") + + depth = session.api_version.default_facts_depth + + payload = { + "depth": depth + } + + # Build ports URI + uri_ports = "{base_url}{class_uri}".format( + base_url=session.base_url, + class_uri=Interface.base_uri_ports + ) + + # Get Ports information + try: + response_ports = session.s.get( + uri_ports, verify=False, params=payload, + proxies=session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response_ports, "GET"): + raise GenericOperationError( + response_ports.text, + response_ports.status_code) + + ports_data = json.loads(response_ports.text) + + # Build interface URI + uri_interface =\ + '{base_url}{class_uri}?depth={depth}'.format( + base_url=session.base_url, + class_uri=Interface.base_uri_interface, + depth=depth + ) + + # Get Interface information + try: + response_interface = session.s.get( + uri_interface, verify=False, params=payload, + proxies=session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response_interface, "GET"): + raise GenericOperationError( + response_interface.text, + response_interface.status_code) + + # Load response text into json format + interfaces_data = json.loads(response_interface.text) + facts_dict = {} + + # Merge Ports and Interfaces by key name + for port in ports_data: + if 'name' in port: + facts_dict[port['name']] = port + for interface in interfaces_data: + if 'name' in interface: + if interface['name'] in facts_dict: + facts_dict[interface['name']].update(interface) + else: + facts_dict[interface['name']] = interface + + return facts_dict + + def from_response(cls, session, response_data): + ''' + Create an Interface object given a response_data related to the + Interface object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + string: "/rest/v1/system/interfaces/1" + :return: Interface object + + ''' + try: + # Try using interfaces + interfaces_id_arr = session.api_version.get_keys( + response_data, 'interfaces') + except AttributeError: + # If AttributeError for Nonetype, try with ports + interfaces_id_arr = session.api_version.get_keys( + response_data, 'ports') + interface_name = interfaces_id_arr[0] + return session.api_version.get_module( + session, 'Interface', + interface_name) + + @connected + def delete(self): + ''' + Perform DELETE call to delete Interface. + + ''' + if not self.__is_special_type: + raise VerificationError('Interface', "Can't be deleted") + + # Delete Interface via a DELETE request to Ports Table + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_ports, + id=self.name + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + # DELETE Interface via DELETE request to Interface Table + # Check if port is a LAG + # If not, DELETE request to Interface Table + if self.type != 'lag': + uri_interfaces = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_interface + ) + + try: + response_ints = self.session.s.delete( + uri_interfaces, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response_ints, "DELETE"): + raise GenericOperationError( + response_ints.text, + response_ints.status_code) + + # Clean LAG from interfaces + # Delete interface references + for interface in self.__prev_interfaces: + # If interface name is not the same as the current one + if interface.name != self.name and self.type == 'lag': + interface.__delete_lag(self) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @connected + def update(self): + ''' + Perform a PUT call to update data for a Port and Interface table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + ''' + # Flag used to determine if Object was modified + modified_port = True + modified_int = True + # Check if Object is a LAG + if self.type != 'lag': + uri_interfaces = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_interface, + name=self.percents_name + ) + # get Interface data related to configuration + int_data = utils.get_attrs(self, self.config_attrs_int) + # Remove type + if 'type' in int_data: + int_data.pop('type') + if 'type' in self.__original_attributes_int: + self.__original_attributes_int.pop('type') + # Set put_int_data + put_int_data = json.dumps(int_data, sort_keys=True, indent=4) + # Compare dictionaries + if int_data == self.__original_attributes_int: + # Object was not modified + modified_port = False + else: + # Bring Interface information + try: + response_ints = self.session.s.put( + uri_interfaces, verify=False, + data=put_int_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response_ints, "PUT"): + raise GenericOperationError( + response_ints.text, + response_ints.status_code) + + # Set new original attributes + self.__original_attributes_int = int_data + + uri_ports = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_ports, + name=self.percents_name + ) + + # get Port data related to configuration + port_data = utils.get_attrs(self, self.config_attrs) + + # Check for Ipv4 + try: + if self.ip4_address is not None: + port_data['ip4_address'] = self.ip4_address + except AttributeError: + pass + # Check if vrf is inside the data related to Port + if "vrf" in port_data: + # Set VRF in the correct format for PUT + port_data['vrf'] = self.vrf.get_info_format() + + # Check if vlan_tag is inside the data related to Port + if "vlan_tag" in port_data: + # Set VLAN in the correct format for PUT + port_data["vlan_tag"] = self.vlan_tag.get_info_format() + + # Set interfaces into correct form + if "interfaces" in port_data: + # Check for interfaces no longer in LAG + if self.__is_special_type and self.type == 'lag': + for element in self.__prev_interfaces: + # If element was deleted from interfaces + if element not in self.interfaces: + # Delete element reference to current LAG + element.__delete_lag(self) + + # Set prev interfaces with current ones + # Copies interfaces + self.__prev_interfaces = list(self.interfaces) + + formated_interfaces = [] + # Set interfaces into correct form + for element in self.interfaces: + # If element is the same as current, ignore + if element.name == self.name and self.type == 'lag': + pass + else: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Interface {}'.format(element.name), + 'Object inside interfaces not materialized') + # Only in V1 get_uri() is used, + # In any other version, element.get_info_format() + # is used + formated_element = element.get_uri(True) + formated_interfaces.append(formated_element) + + if self.type == 'lag': + # New element being added to LAG + element.__add_member_to_lag(self) + + # Set values in correct form + port_data["interfaces"] = formated_interfaces + + # Set VLANs into correct form + if "vlan_trunks" in port_data: + formated_vlans = [] + # Set interfaces into correct form + for element in self.vlan_trunks: + # Verify object is materialized + if not element.materialized: + raise VerificationError( + 'Vlan {}'.format(element), + 'Object inside vlan trunks not materialized') + formated_element = element.get_info_format() + formated_vlans.append(formated_element) + + # Set values in correct form + port_data["vlan_trunks"] = formated_vlans + + # Set all ACLs + if "aclmac_in_cfg" in port_data and self.aclmac_in_cfg is not None: + # Set values in correct form + port_data["aclmac_in_cfg"] = self.aclmac_in_cfg.get_info_format() + + if "aclmac_out_cfg" in port_data and self.aclmac_out_cfg is not None: + # Set values in correct form + port_data["aclmac_out_cfg"] = self.aclmac_out_cfg.get_info_format() + + if "aclv4_in_cfg" in port_data and self.aclv4_in_cfg is not None: + # Set values in correct form + port_data["aclv4_in_cfg"] = self.aclv4_in_cfg.get_info_format() + + if "aclv4_out_cfg" in port_data and self.aclv4_out_cfg is not None: + # Set values in correct form + port_data["aclv4_out_cfg"] = self.aclv4_out_cfg.get_info_format() + + if "aclv4_routed_in_cfg" in port_data and self.aclv4_routed_in_cfg is not None: + # Set values in correct form + port_data["aclv4_routed_in_cfg"] = self.aclv4_routed_in_cfg.get_info_format() + + if "aclv4_routed_out_cfg" in port_data and self.aclv4_routed_out_cfg is not None: + # Set values in correct form + port_data["aclv4_routed_out_cfg"] = self.aclv4_routed_out_cfg.get_info_format() + + if "aclv6_in_cfg" in port_data and self.aclv6_in_cfg is not None: + # Set values in correct form + port_data["aclv6_in_cfg"] = self.aclv6_in_cfg.get_info_format() + + if "aclv6_out_cfg" in port_data and self.aclv6_out_cfg is not None: + # Set values in correct form + port_data["aclv6_out_cfg"] = self.aclv6_out_cfg.get_info_format() + + if "aclv6_routed_in_cfg" in port_data and self.aclv6_routed_in_cfg is not None: + # Set values in correct form + port_data["aclv6_routed_in_cfg"] = self.aclv6_routed_in_cfg.get_info_format() + + if "aclv6_routed_out_cfg" in port_data and self.aclv6_routed_out_cfg is not None: + # Set values in correct form + port_data["aclv6_routed_out_cfg"] = self.aclv6_routed_out_cfg.get_info_format() + + # Set addresses the correct way + if self.ip6_addresses is not None: + ip6_addresses_dict = {} + + for ip in self.ip6_addresses: + ip6_addresses_dict[ip.address] = ip.get_uri() + + # Set values in correct form + port_data["ip6_addresses"] = ip6_addresses_dict + + # Delete type from Port data + if 'type' in port_data: + port_data.pop('type') + + if 'type' in self.__original_attributes_port: + self.__original_attributes_port.pop('type') + # Special case, if dictionary is empty + if port_data["ip6_addresses"] == {}: + self.__original_attributes_port["ip6_addresses"] = {} + + # Compare dictionaries + if port_data == self.__original_attributes_port: + # Object was not modified + modified_int = False + + else: + + # Set put_port_data + put_port_data = json.dumps(port_data, sort_keys=True, indent=4) + + # Bring Port information + try: + response_ports = self.session.s.put( + uri_ports, verify=False, + data=put_port_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response_ports, "PUT"): + raise GenericOperationError( + response_ports.text, + response_ports.status_code) + # Set new original attributes + self.__original_attributes_port = port_data + + return modified_int or modified_port + + @connected + def create(self): + """ + Perform a POST call to create a Port table entry for Interface. + Only returns if an exception is not raise + + :return True if entry was created inside Device + """ + + port_data = {} + port_data = utils.get_attrs(self, self.config_attrs) + port_data['name'] = self.name + + # Creating in the Ports Table + uri_ports = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_ports + ) + + post_data_ports = json.dumps(port_data, sort_keys=True, indent=4) + try: + response = self.session.s.post( + uri_ports, verify=False, data=post_data_ports, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding {} Port table entry succeeded".format( + self.name)) + + # Check if port is a LAG + # If not, POST Request to Interface Table + if self.type != 'lag': + uri_interfaces = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_interface + ) + + interface_data = {} + # Set data for Interface Table + interface_data = utils.get_attrs(self, self.config_attrs_int) + interface_data['name'] = self.name + interface_data['type'] = self.type + interface_data['referenced_by'] = self.get_uri() + + # Set post_int_data + post_int_data = json.dumps( + interface_data, sort_keys=True, indent=4) + + # Bring Interface information + try: + response_ints = self.session.s.post( + uri_interfaces, verify=False, + data=post_int_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response_ints, "POST"): + raise GenericOperationError( + response_ints.text, + response_ints.status_code) + + # Get all objects data + self.get() + # Object was created + return True + + def get_uri(self, interface=False): + ''' + Method used to obtain the specific Interface URI + :param interface: Boolean, if true URI would contain + a /interfaces/interface_name instead of + /ports/interface_name + ''' + uri = '' + if not interface: + uri = '{resource_prefix}{class_uri}/{name}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Interface.base_uri_ports, + name=self.percents_name + ) + else: + uri = '{resource_prefix}{class_uri}/{name}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Interface.base_uri_interface, + name=self.percents_name + ) + + return uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing + inside other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def __str__(self): + return "Port name: '{}'".format(self.name) + + def __set_to_default(self): + ''' + Perform a PUT call to set Interface to default settings + :return: True if object was changed + ''' + + uri_ports = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Interface.base_uri_ports, + name=self.percents_name + ) + + # get Port data related to configuration + port_data = {} + + # Set interfaces into correct form + if "interfaces" in port_data: + if self.__is_special_type and self.name == 'lag': + self.interfaces = [] + for element in self.__prev_interfaces: + # If element was deleted from interfaces + if element not in self.interfaces: + # Delete element reference to current LAG + element.__delete_lag(self) + else: + self.interfaces = [self] + + # Set prev interfaces with current ones + # Copies interfaces + self.__prev_interfaces = list(self.interfaces) + + # Set put_port_data + put_port_data = json.dumps(port_data, sort_keys=True, indent=4) + + # Update Port information + try: + response_ports = self.session.s.put( + uri_ports, verify=False, + data=put_port_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response_ports, "PUT"): + raise GenericOperationError(response_ports.text, + response_ports.status_code) + + # Update values with new ones + self.get() + return True + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def set_state(self, state="up"): + """ + Either enable or disable the Interface by setting Interface's + admin_state to "up" or "down" + + :param state: State to set the interface to + :return: True if object was changed + + """ + # Set interface to default settings + self.admin = state + if "lag" not in self.name: + try: + self.user_config["admin"] = state + except AttributeError: + # For loopback + pass + + # Apply Changes + return self.apply() diff --git a/pyaoscx/rest/v10_04/__init__.py b/pyaoscx/rest/v10_04/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyaoscx/rest/v10_04/api.py b/pyaoscx/rest/v10_04/api.py new file mode 100644 index 0000000..39579f8 --- /dev/null +++ b/pyaoscx/rest/v10_04/api.py @@ -0,0 +1,186 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from datetime import date + +from pyaoscx.api import API + + +class v10_04(API): + ''' + Represents a REST API Version 10.04. It keeps all the information + needed for the version and methods related to it. + ''' + + def __init__(self): + self.release_date = date(2019, 1, 1) + self.version = '10.04' + self.default_selector = 'writable' + self.default_depth = 1 + self.default_facts_depth = 2 + self.default_subsystem_facts_depth = 4 + self.valid_selectors = [ + 'configuration', 'status', 'statistics', 'writable'] + self.configurable_selectors = ['writable'] + self.compound_index_separator = ',' + self.valid_depths = [0, 1, 2, 3, 4] + + def valid_depth(self, depth): + ''' + Verifies if given depth is valid for the current API version + :param depth: Integer + :return valid: Boolean True if depth is valid + ''' + valid = True + if depth not in self.valid_depths: + valid = False + return valid + + def get_index(self, obj): + ''' + Method used to obtain the correct format of the objects information + which depends on the Current API version + :param obj: PyaoscxModule object + :return info: Dictionary in the form of + Example: + "keepalive_vrf": { + "keepalive_name": "Resource uri", + } + + ''' + key_str = "" + length = len(obj.indices) + attributes = [] + for i in range(length): + attr_name = obj.indices[i] + attr_value = getattr(obj, attr_name) + if not isinstance(attr_value, str): + attr_value = str(attr_value) + attributes.append(attr_value) + + key_str = ','.join(attributes) + info = { + key_str: obj.get_uri() + } + return info + + def get_module(self, session, module, index_id=None, **kwargs): + ''' + Create a module object given a response data and the module's type. + + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param module: Name representing the module which is about to be + created + :param index_id: The module index_id or ID + :return object: Return object same as module + + ''' + + if module == 'Interface': + from pyaoscx.rest.v10_04.interface import Interface + + elif module == 'Ipv6': + from pyaoscx.ipv6 import Ipv6 + + elif module == 'Vlan': + from pyaoscx.vlan import Vlan + + elif module == 'Vrf': + from pyaoscx.vrf import Vrf + + elif module == 'Vsx': + from pyaoscx.vsx import Vsx + return Vsx(session, **kwargs) + + elif module == 'BgpRouter': + from pyaoscx.bgp_router import BgpRouter + + elif module == 'BgpNeighbor': + from pyaoscx.bgp_neighbor import BgpNeighbor + + elif module == 'VrfAddressFamily': + from pyaoscx.vrf_address_family import VrfAddressFamily + + elif module == 'OspfRouter': + from pyaoscx.ospf_router import OspfRouter + + elif module == 'OspfArea': + from pyaoscx.ospf_area import OspfArea + # Add data for correct Ospf Area creation + other_config = { + "stub_default_cost": 1, + "stub_metric_type": "metric_non_comparable" + } + return OspfArea(session, index_id, other_config=other_config, + **kwargs) + + elif module == 'OspfInterface': + from pyaoscx.ospf_interface import OspfInterface + + elif module == 'DhcpRelay': + from pyaoscx.dhcp_relay import DhcpRelay + + elif module == 'ACL': + from pyaoscx.acl import ACL + + elif module == 'AclEntry': + from pyaoscx.acl_entry import AclEntry + + elif module == 'AggregateAddress': + from pyaoscx.aggregate_address import AggregateAddress + + elif module == 'StaticRoute': + from pyaoscx.static_route import StaticRoute + + elif module == 'StaticNexthop': + from pyaoscx.static_nexthop import StaticNexthop + + else: + raise Exception("Invalid Module Name") + + return locals()[module](session, index_id, **kwargs) + + def get_keys(self, response_data, module_name=None): + ''' + Given a response_data obtain the indices of said dictionary and return + them. + Get keys should be used for only one element in the dictionary. + :param response_data: a dictionary object in the form of + { + "index_1,index_2": "/rest/v10.04/system//,", + } + :return indices: List of indices + ''' + + indices = None + for k, v in response_data.items(): + indices = k + + indices = indices.split(',') + return indices + + def get_uri_from_data(self, data): + ''' + Given a response data, create a list of URI items. In this Version the + data is a dict. + + :param data: Dictionary containing URI data in the form of + example: + {'': '/rest/v10.04/system//', + '': '/rest/v10.04/system//', + '': '/rest/v10.04/system//'} + + :return uri_list: a list containing the input dictionary's values + example: + [ + '/rest/v10.04/system//', + '/rest/v10.04/system//' + ] + + ''' + uri_list = [] + for k, v in data.items(): + uri_list.append(v) + + return uri_list diff --git a/pyaoscx/rest/v10_04/interface.py b/pyaoscx/rest/v10_04/interface.py new file mode 100644 index 0000000..8e1f4ed --- /dev/null +++ b/pyaoscx/rest/v10_04/interface.py @@ -0,0 +1,13 @@ + +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.interface import Interface as AbstractInterface + + +class Interface(AbstractInterface): + ''' + Provide configuration management for Interface for Version 10.04 + ''' + + pass diff --git a/pyaoscx/session.py b/pyaoscx/session.py index 67fea6f..c59f182 100644 --- a/pyaoscx/session.py +++ b/pyaoscx/session.py @@ -1,60 +1,284 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import common_ops +from pyaoscx.api import API +from pyaoscx.exceptions.login_error import LoginError +import pyaoscx.utils.util as utils -import getpass -import requests import json +import getpass import logging +import re +import requests + +# Global Variables +ZEROIZED = 268 +UNAUTHORIZED = 401 + + +class Session: + ''' + Represents a logical connection to the device. It keeps all the information + needed to login/logout to an AOS-CX device, including parameters like the + proxy, device's IP address (both IPv4 and IPv6 are supported), and the API + version. + IP address should be: + '1.1.1.1' + or + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + ''' + + def __init__(self, ip_address, api_version, proxy=None): + + self.api_version = API.create(api_version) + self.connected = False + self.ip = ip_address + self.proxy = { + 'no_proxy': self.ip + } if proxy is None else { + 'https': proxy + } + self.base_url = "https://{}/rest/v{}/".format( + self.ip, self.api_version) + self.resource_prefix = "/rest/v{}/".format(self.api_version) + self.s = requests.Session() + self.s.verify = False + self.__username = self.__password = '' + + def is_connected(self): + return self.connected + + def cookies(self): + ''' + Return the cookie stored in the requests' session + ''' + return self.s.cookies._cookies + + @classmethod + def from_session(cls, req_session, base_url, credentials=None): + ''' + Create a session from an existing request Session. It allows to create + an internal session from an already-authenticated and serialized session + :param req_session: Existing Request Session object + :param base_url: Url needed to create Session Object + :param credentials: Dictionary with user and password credentials + Example: { + username: , + password: + } + :return session: Request Session Object + ''' + ip_address = version = '' + + # From base url retrieve the ip address and the version + url_pattern = re.compile( + r'https://(?P.+)/rest/v(?P.+)/') + match = url_pattern.match(base_url) + if match: + ip_address = match.group('ip_address') + version = match.group('version') + + else: + raise Exception("Error creating Session") + + # Create Session + session = Session(ip_address, version) + + # Determine proxy + # If the proxy is not {} then it would replace the proxy + # previously created inside the __init__ method + if req_session.proxies != {}: + session.proxy = req_session.proxies + + # Set request.Session() + session.s.cookies = req_session.cookies + session.connected = True + + # Set credentials + if credentials is not None: + session.__username = credentials['username'] + session.__password = credentials['password'] + + return session + + def open(self, username=None, password=None): + ''' + Perform a POST call to login and gain access to other API calls. + If either username or password is not specified, user will be prompted + to enter the missing credential(s). + + :param username: username + :param password: password + ''' + if self.__username is None: + if username is None: + username = input('Enter username: ') + else: + self.__username = username + else: + if username is None: + username = input('Enter username: ') + + if self.__password is None: + if password is None: + password = getpass.getpass() + else: + self.__password = password + else: + if password is None: + password = getpass.getpass() + + login_data = {'username': username, 'password': password} + self.__username = username + self.__password = password + try: + login_uri = '%s%s' % (self.base_url, 'login') + response = self.s.post(login_uri, data=login_data, verify=False, + timeout=5, proxies=self.proxy) + except requests.exceptions.ConnectTimeout: + raise Exception( + 'ERROR: Error connecting to host: connection attempt timed out.') + + if response.status_code != 200: + raise Exception('FAIL: Login failed with status code %d: %s' % + (response.status_code, response.text)) + + logging.info('SUCCESS: Login succeeded') + self.connected = True + + def close(self): + ''' + Perform a POST call to logout and end the session. + Given all the required information to perform the operation is already + stored within the session, no parameters are required. + ''' + if self.connected: + logout_uri = "%s%s" % (self.base_url, 'logout') + + try: + response = self.s.post( + logout_uri, verify=False, proxies=self.proxy) + except BaseException: + raise Exception("Unable to process the request: (%u) %s" % + (response.status_code, response.text)) + + if response.status_code != 200: + raise Exception('FAIL: Logout failed with status code %d: %s' % + (response.status_code, response.text)) + + logging.info('SUCCESS: Logout succeeded') + self.connected = False + + # Session Login and Logout + # Used for Connection within Ansible + + @classmethod + def login(cls, base_url, username=None, password=None, use_proxy=True, + handle_zeroized_device=False): + """ + + Perform a POST call to login and gain access to other API calls. + If either username or password is not specified, user will be + prompted to enter the missing credential(s). + + :param base_url: URL in main() function + :param username: username + :param password: password + :param use_proxy: Whether the system proxy should be used, defaults + to True. + :param handle_zeroized_device: Whether a zeroized device should + be initialized, if so sets the admin password to the provided one, + defaults to False. + :return: requests.session object with loaded cookie jar + """ + if username is None and password is None: + username = input('Enter username: ') + password = getpass.getpass() + + login_data = {"username": username, "password": password} + + s = requests.Session() + + if use_proxy is False: + s.proxies["https"] = None + s.proxies["http"] = None + try: + print(base_url + 'login') + response = s.post( + base_url + "login", data=login_data, verify=False, + timeout=5, proxies=s.proxies) + except requests.exceptions.ConnectTimeout: + logging.warning('ERROR: Error connecting to host: connection\ + attempt timed out.') + raise LoginError('ERROR: Error connecting to host:\ + connection attempt timed out.') + except requests.exceptions.ProxyError as err: + logging.warning('ERROR: %s' % str(err)) + raise LoginError('ERROR: %s' % err) + # Response OK check needs to be passed "PUT" since this + # POST call returns 200 instead of conventional 201 + if not utils._response_ok(response, "PUT"): + if response.status_code == UNAUTHORIZED \ + and handle_zeroized_device: + # Try to login with default credentials: + ztp_login_data = {"username": username} + response = s.post( + base_url + "login", data=ztp_login_data, verify=False, + timeout=5, proxies=s.proxies) + if response.status_code == ZEROIZED: + data = { + "password": password + } + response = s.put( + base_url + "system/users/admin", data=json.dumps(data), + verify=False, timeout=5, proxies=s.proxies) + if utils._response_ok(response, "PUT"): + logging.info("SUCCESS: Login succeeded") + return s + logging.warning("FAIL: Login failed\ + with status code %d: %s" % ( + response.status_code, response.text)) + raise LoginError("FAIL: Login failed with\ + status code %d: %s" % ( + response.status_code, response.text), response.status_code) + else: + logging.info("SUCCESS: Login succeeded") + return s + + @classmethod + def logout(cls, **kwargs): + """ + Perform a POST call to logout and end session. + + :param kwargs: + keyword s: requests.session object with loaded cookie jar + keyword url: URL in main() function + :return: True if successful, False otherwise + """ + response = kwargs["s"].post( + kwargs["url"] + "logout", verify=False, + proxies=kwargs["s"].proxies) + # Response OK check needs to be passed "PUT" since this POST + # call returns 200 instead of conventional 201 + if not utils._response_ok(response, "PUT"): + logging.warning( + "FAIL: Logout failed with status code %d: %s" % ( + response.status_code, response.text)) + return False + else: + logging.info("SUCCESS: Logout succeeded") + return True + def username(self): + """ + Get username + :return username + """ + return self.__username -def login(base_url, username=None, password=None): - """ - - Perform a POST call to login and gain access to other API calls. - If either username or password is not specified, user will be prompted to enter the missing credential(s). - - :param base_url: URL in main() function - :param username: username - :param password: password - :return: requests.session object with loaded cookie jar - """ - if username is None and password is None: - username = input('Enter username: ') - password = getpass.getpass() - - login_data = {"username": username, "password": password} - - s = requests.Session() - try: - response = s.post(base_url + "login", data=login_data, verify=False, timeout=5) - except requests.exceptions.ConnectTimeout: - logging.warning('ERROR: Error connecting to host: connection attempt timed out.') - exit(-1) - # Response OK check needs to be passed "PUT" since this POST call returns 200 instead of conventional 201 - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Login failed with status code %d: %s" % (response.status_code, response.text)) - exit(-1) - else: - logging.info("SUCCESS: Login succeeded") - return s - - -def logout(**kwargs): - """ - Perform a POST call to logout and end session. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - response = kwargs["s"].post(kwargs["url"] + "logout", verify=False) - # Response OK check needs to be passed "PUT" since this POST call returns 200 instead of conventional 201 - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Logout failed with status code %d: %s" % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Logout succeeded") - return True \ No newline at end of file + def password(self): + """ + Get password + :return password + """ + return self.__password diff --git a/pyaoscx/static_nexthop.py b/pyaoscx/static_nexthop.py new file mode 100644 index 0000000..8b3d06d --- /dev/null +++ b/pyaoscx/static_nexthop.py @@ -0,0 +1,506 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class StaticNexthop(PyaoscxModule): + ''' + Provide configuration management for Static Nexthop settings on AOS-CX devices. + ''' + + indices = ['id'] + resource_uri_name = 'static_nexthops' + + def __init__(self, session, _id, parent_static_route, uri=None, **kwargs): + + self.session = session + # Assign IP + self.id = _id + # Assign parent StaticRoute object + self.__set_static_route(parent_static_route) + self._uri = uri + # List used to determine attributes related to the Static Nexthop + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_static_route(self, parent_static_route): + ''' + Set parent StaticRoute object as an attribute for the StaticNexthop object + :param parent_static_route a Static_Route object + ''' + + # Set parent Static Route + self.__parent_static_route = parent_static_route + + # Set URI + self.base_uri = '{base_static_route_uri}/{static_route_name}/static_nexthops'.format( + base_static_route_uri=self.__parent_static_route.base_uri, + static_route_name=self.__parent_static_route.reference_address) + + # Verify Static Nexthop data doesn't exist already for Static Route + for static_nexthop in self.__parent_static_route.static_nexthops: + if static_nexthop.id == self.id: + # Make list element point to current object + static_nexthop = self + else: + # Add self to static_nexthops list in parent Static Route + self.__parent_static_route.static_nexthops.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a Static Nexthop table + entry and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch static_nexthop routers") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.id + ) + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the Static Nexthop is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', + ['id']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'id' in self.__original_attributes: + self.__original_attributes.pop('id') + + # If the Static Route has a port inside the switch + if 'port' in data and \ + self.port is not None: + port_response = self.port + interface_cls = self.session.api_version.get_module( + self.session, 'Interface', '') + # Set port as a Interface Object + self.port = interface_cls.from_response( + self.session, port_response) + # Materialize port + self.port.get() + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + return True + + @classmethod + def get_all(cls, session, parent_static_route): + ''' + Perform a GET call to retrieve all system Static Nexthop related to a Static Route , + and create a dictionary containing StaticNexthop objects. + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_static_route: StaticRoute object, parent for the Static + Nexthops + :return: Dictionary containing Static Nexthop IDs as keys and a Static + NexthopThis objects as values + ''' + + logging.info("Retrieving the switch static_nexthop") + + base_uri = '{base_static_route_uri}/{static_route_name}/static_nexthops'.format( + base_static_route_uri=parent_static_route.base_uri, + static_route_name=parent_static_route.reference_address) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + static_nexthop_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a StaticNexthop object and adds it to parent static_route + # list + _id, static_nexthop = StaticNexthop.from_uri( + session, parent_static_route, uri) + # Load all Static Nexthop data from within the Switch + static_nexthop.get() + static_nexthop_dict[_id] = static_nexthop + + return static_nexthop_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an + existing Static Nexthop table entry. + Checks whether the static_nexthop exists in the switch + Calls self.update() if Static Nexthop being updated + Calls self.create() if a new Static Nexthop is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + if not self.__parent_static_route.materialized: + self.__parent_static_route.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing static_nexthop + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + static_nexthop_data = {} + + static_nexthop_data = utils.get_attrs(self, self.config_attrs) + + # Get port uri + if hasattr(self, 'port') and \ + self.port is not None: + static_nexthop_data["port"] = \ + self.port.get_info_format() + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.id + ) + + # Compare dictionaries + if static_nexthop_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps( + static_nexthop_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, + data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update static_nexthop table entry {} succeeded".format( + self.id)) + + # Set new original attributes + self.__original_attributes = static_nexthop_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new static_nexthop + Only returns if an exception is not raise + + :return modified: Boolean, True if entry was created + ''' + static_nexthop_data = {} + + static_nexthop_data = utils.get_attrs(self, self.config_attrs) + static_nexthop_data['id'] = self.id + + # Get port uri + if hasattr(self, 'port') and \ + self.port is not None: + static_nexthop_data["port"] = \ + self.port.get_info_format() + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(static_nexthop_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding static_nexthop table entry {} succeeded".format( + self.id)) + + # Get all object's data + self.get() + # Object was created, thus modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete StaticNexthop table entry. + + ''' + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + id=self.id + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete static_nexthop table entry {} succeeded".format( + self.id)) + + # Delete back reference from VRF + for static_nexthop in self.__parent_static_route.static_nexthops: + if static_nexthop.id == self.id: + self.__parent_static_route.static_nexthops.remove( + static_nexthop) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_static_route, response_data): + ''' + Create a StaticNexthop object given a response_data related to the + Static Nexthop ID object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a + logical connection to the device + :param parent_static_route: parent StaticRoute object where + Static Nexthop is stored + :param response_data: The response can be either a + dictionary: { + id: "/rest/v10.04/system/static_routes/static_nexthops/id" + } + or a + string: "/rest/v10.04/system/static_routes/static_nexthops/id" + :return: StaticNexthop object + ''' + static_nexthop_arr = session.api_version.get_keys( + response_data, StaticNexthop.resource_uri_name) + _id = static_nexthop_arr[0] + return StaticNexthop(session, _id, parent_static_route) + + @classmethod + def from_uri(cls, session, parent_static_route, uri): + ''' + Create a StaticNexthop object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_static_route: parent static_route class where + static_nexthop is stored + :param uri: a String with a URI + + :return index, static_nexthop: tuple containing both the Static Nexthop + object and the Static Nexthop's ID + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)static_nexthops/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create static_nexthop object + static_nexthop_obj = StaticNexthop( + session, index, parent_static_route, uri=uri) + + return index, static_nexthop_obj + + def __str__(self): + return "Static Nexthop: {}".format(self.id) + + def get_uri(self): + ''' + Method used to obtain the specific Static Nexthop URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_id}{class_uri}/{id}'.format( + resource_id=self.session.resource_id, + class_uri=self.base_uri, + id=self.id + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + @classmethod + def get_next_id(cls, session, parent_static_route): + ''' + Method used to obtain the ID for the next Static Nexthop. Thus + Perform a GET call to retrieve all system Static Nexthop inside a + Static Route, and with it determine the next ID + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_static_route: StaticRoute object, parent for the Static + Nexthops + :return new_id: Integer with the new Id for the next Static Nexthop + ''' + + logging.info("Retrieving the switch static_nexthop") + + base_uri = '{base_static_route_uri}/{static_route_name}/static_nexthops'.format( + base_static_route_uri=parent_static_route.base_uri, + static_route_name=parent_static_route.reference_address) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + next_id = None + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Obtain ID from uri + _id, static_nexthop = StaticNexthop.from_uri( + session, parent_static_route, uri) + next_id = int(_id) + + # Set next ID + if next_id is not None: + next_id += 1 + else: + next_id = 0 + return next_id + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### diff --git a/pyaoscx/static_route.py b/pyaoscx/static_route.py new file mode 100644 index 0000000..bf765d1 --- /dev/null +++ b/pyaoscx/static_route.py @@ -0,0 +1,531 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor +from pyaoscx.static_nexthop import StaticNexthop + + +class StaticRoute(PyaoscxModule): + ''' + Provide configuration management for Static Route on AOS-CX devices. + ''' + + indices = ['prefix'] + static_nexthops = ListDescriptor('static_nexthops') + resource_uri_name = 'static_routes' + + def __init__(self, session, prefix, parent_vrf, uri=None, **kwargs): + + self.session = session + # Assign IP + self.__set_name(prefix) + # Assign parent Vrf object + self.__set_vrf(parent_vrf) + self._uri = uri + # List used to determine attributes related to the Static Route + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Use to manage Ospf routers + self.static_nexthops = [] + + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_name(self, address): + ''' + Set name attribute in the proper form for Static Route + :param address: Static Route IP address + ''' + + # Add attributes to class + self.prefix = None + self.reference_address = None + + if r'%2F' in address or r'%2C' in address or r'%3A' in address: + self.prefix = utils._replace_percents_ip(address) + self.reference_address = address + else: + self.prefix = address + self.reference_address = utils._replace_special_characters_ip( + self.prefix) + + def __set_vrf(self, parent_vrf): + ''' + Set parent Vrf object as an attribute for the StaticRoute object + :param parent_vrf: a Vrf object + ''' + + # Set parent Vrf object + self.__parent_vrf = parent_vrf + + # Set URI + self.base_uri = '{base_vrf_uri}/{vrf_name}/static_routes'.format( + base_vrf_uri=self.__parent_vrf.base_uri, + vrf_name=self.__parent_vrf.name) + + # Verify Static Route doesn't exist already inside VRF + for static_route in self.__parent_vrf.static_routes: + if static_route.prefix == self.prefix: + # Make list element point to current object + static_route = self + else: + # Add self to static_routes list in parent Vrf object + self.__parent_vrf.static_routes.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a Static Route table + entry and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch Static Routes") + + depth = self.session.api_version.default_depth\ + if depth is None else depth + + selector = self.session.api_version.default_selector\ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{prefix}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + prefix=self.reference_address + ) + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + # Delete unwanted data + if 'static_nexthops' in data: + data.pop('static_nexthops') + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the Static Route is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', + ['prefix', 'static_nexthops']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'prefix' in self.__original_attributes: + self.__original_attributes.pop('prefix') + # Remove Static Nexthops + if 'static_nexthops' in self.__original_attributes: + self.__original_attributes.pop('static_nexthops') + + # Clean Static Nexthops settings + if self.static_nexthops == []: + # Set Static Nexthops if any + # Adds Static Nexthop to parent Vrf object + StaticNexthop.get_all(self.session, self) + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + return True + + @classmethod + def get_all(cls, session, parent_vrf): + ''' + Perform a GET call to retrieve all system Static Routes inside a VRF, + and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where vrf is stored + :return: Dictionary containing Static Route IDs as keys and a Static + Route objects as values + ''' + + logging.info("Retrieving the switch Static Routes") + + base_uri = '{base_vrf_uri}/{vrf_name}/static_routes'.format( + base_vrf_uri=parent_vrf.base_uri, + vrf_name=parent_vrf.name) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + static_route_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a StaticRoute object and adds it to parent_vrf list + prefix, static_route = StaticRoute.from_uri( + session, parent_vrf, uri) + # Load all Static Route data from within the Switch + static_route.get() + static_route_dict[prefix] = static_route + + return static_route_dict + + @connected + def apply(self): + ''' + Main method used to either create a new or update an + existing Static Route table entry. + Checks whether the Static Route exists in the switch + Calls self.update() if object being updated + Calls self.create() if a new Static Route is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + + ''' + if not self.__parent_vrf.materialized: + self.__parent_vrf.apply() + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing Static Route table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + # Variable returned + modified = False + + static_route_data = {} + + static_route_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{prefix}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + prefix=self.reference_address + ) + + # Compare dictionaries + if static_route_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps(static_route_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, + data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update static_route table entry {} succeeded".format( + self.prefix)) + + # Set new original attributes + self.__original_attributes = static_route_data + + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new Static Route table entry + Only returns if an exception is not raise + + :return: Boolean, True if entry was created + ''' + static_route_data = {} + + static_route_data = utils.get_attrs(self, self.config_attrs) + static_route_data['prefix'] = self.prefix + static_route_data['vrf'] = self.__parent_vrf.get_uri() + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps(static_route_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Adding Static Route table entry {} succeeded".format( + self.prefix)) + + # Get all object's data + self.get() + # Object was created, thus modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete specified Static Route table entry. + + ''' + + uri = "{base_url}{class_uri}/{prefix}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + prefix=self.reference_address + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete static_route table entry {} succeeded".format( + self.prefix)) + + # Delete back reference from VRF + for static_route in self.__parent_vrf.static_routes: + if static_route.prefix == self.prefix: + self.__parent_vrf.static_routes.remove(static_route) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_vrf, response_data): + ''' + Create a StaticRoute object given a response_data related to the + Static Route prefix object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a + logical connection to the device + :param parent_vrf: parent Vrf object where Static Route is stored + :param response_data: The response can be either a + dictionary: { + prefix: "/rest/v10.04/system/vrfs/static_routes/prefix" + } + or a + string: "/rest/v10.04/system/vrfs/static_routes/prefix" + :return: Static Route Object + ''' + static_route_arr = session.api_version.get_keys( + response_data, StaticRoute.resource_uri_name) + prefix = static_route_arr[0] + return StaticRoute(session, prefix, parent_vrf) + + @classmethod + def from_uri(cls, session, parent_vrf, uri): + ''' + Create a StaticRoute object given a URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where Static Route is stored + :param uri: a String with a URI + + :return index, static_route: tuple containing both the static_route object + and the static_route's prefix + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)static_routes/(?P.+)') + index = index_pattern.match(uri).group('index') + + # Create StaticRoute object + static_route_obj = StaticRoute(session, index, parent_vrf, uri=uri) + + return index, static_route_obj + + def __str__(self): + return "Static Route: {}".format(self.prefix) + + def get_uri(self): + ''' + Method used to obtain the specific static route URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{prefix}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + prefix=self.reference_address + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def add_static_nexthop(self, + next_hop_ip_address=None, + next_hop_interface=None, + distance=None, + nexthop_type=None, + bfd_enable=None): + ''' + Create a Static Nexthop, with a VRF and a Destination Address + related to a Static Route. + + :param next_hop_ip_address: The IPv4 address or the IPv6 address of + next hop. + Example: + '1.1.1.1' + or + '2001:db8::11/ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff' + :param next_hop_interface: The interface through which the next hop + can be reached. + :param distance: Administrative distance to be used for the next + hop in the static route instead of default value. + :param nexthop_type: Specifies whether the static route is a forward, + blackhole or reject route. + :param bfd_enable: Boolean to enable BFD + :return: StaticNexthop object + ''' + + if distance is None: + distance = 1 + # Set variable + next_hop_interface_obj = None + if next_hop_interface is not None: + next_hop_interface_obj = self.session.api_version.get_module( + self.session, 'Interface', + next_hop_interface) + + if nexthop_type is None: + nexthop_type = 'forward' + + if nexthop_type == 'forward': + bfd_enable = False + + static_nexthop_obj = self.session.api_version.get_module( + self.session, 'StaticNexthop', 0, + parent_static_route=self, + ) + + # Try to obtain data; if not, create + try: + static_nexthop_obj.get() + # Delete previous static nexthop + static_nexthop_obj.delete() + except GenericOperationError: + # Catch error + pass + + finally: + static_nexthop_obj = self.session.api_version.get_module( + self.session, 'StaticNexthop', + 0, + parent_static_route=self, + ip_address=next_hop_ip_address, + distance=distance, + port=next_hop_interface_obj, + type=nexthop_type, + bfd_enable=bfd_enable + ) + # Create object inside switch + static_nexthop_obj.apply() + + return static_nexthop_obj diff --git a/pyaoscx/system.py b/pyaoscx/system.py deleted file mode 100644 index d43fe7a..0000000 --- a/pyaoscx/system.py +++ /dev/null @@ -1,89 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import logging - - -def get_system_info(params={}, **kwargs): - """ - Perform a GET call to get system information - - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing system information - """ - target_url = kwargs["url"] + "system" - - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of system information failed with status code %d: %s" - % (response.status_code, response.text)) - system_info_dict = {} - else: - logging.info("SUCCESS: Getting dictionary of system information succeeded") - system_info_dict = response.json() - - return system_info_dict - -def get_chassis_info(params={}, **kwargs): - """ - Perform a GET call to get the chassis information, such as product info, reboot statistics, selftest info, and more. - - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing chassis information - """ - - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/subsystems/chassis/1" - else: - # Else logic designed for v10.04 and later - target_url = kwargs["url"] + "system/subsystems/chassis,1" - - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of chassis information failed with status code %d: %s" - % (response.status_code, response.text)) - chassis_info_dict = {} - else: - logging.info("SUCCESS: Getting dictionary of chassis information succeeded") - chassis_info_dict = response.json() - - return chassis_info_dict - -def get_product_info(params={}, **kwargs): - """ - Perform a GET call to get the product information, such as MAC, Part Number, Model, and Serial - - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing product information - """ - - if kwargs["url"].endswith("/v1/"): - target_url = kwargs["url"] + "system/subsystems/chassis/1?attributes=product_info" - else: - # Else logic designed for v10.04 and later - target_url = kwargs["url"] + "system/subsystems/chassis,1?attributes=product_info" - - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of product information failed with status code %d: %s" - % (response.status_code, response.text)) - product_info_dict = {} - else: - logging.info("SUCCESS: Getting dictionary of product information succeeded") - product_info_dict = response.json() - - return product_info_dict diff --git a/pyaoscx/utils/__init__.py b/pyaoscx/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyaoscx/utils/connection.py b/pyaoscx/utils/connection.py new file mode 100644 index 0000000..860a5c6 --- /dev/null +++ b/pyaoscx/utils/connection.py @@ -0,0 +1,20 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +import functools + + +def connected(fnct): + ''' + Function used as a decorator to ensure the module has a established + connection + + :param fnct: function which behavior is modified + :return ensure_connected: Function + ''' + @functools.wraps(fnct) + def ensure_connected(self, *args): + if not self.session.is_connected(): + self.session.open() + return fnct(self, *args) + return ensure_connected diff --git a/pyaoscx/utils/list_attributes.py b/pyaoscx/utils/list_attributes.py new file mode 100644 index 0000000..dc52391 --- /dev/null +++ b/pyaoscx/utils/list_attributes.py @@ -0,0 +1,127 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.generic_op_error import GenericOperationError + + +class ListDescriptor(list): + ''' + Attribute descriptor class to keep track of a list that contains pyaoscx_module + objects simulating a Reference to a resource. + If the list changes, then every pyaoscx_module object has to be changed. + ''' + + def __init__(self, name,): + self.name = name + + def __get__(self, instance, owner): + """ + Method called when current attribute is used. + :param instance: Instance of the current Object + """ + return instance.__dict__[self.name] + + def __set__(self, instance, new_list): + """ + Method called when current attribute is set. + :param instance: Instance of the current Object + :param new_list: new list being set to current attribute object + """ + new_list = ReferenceList(new_list) + prev_list = instance.__dict__[ + self.name] if self.name in instance.__dict__ else None + + # Update value inside the instance dictionary + instance.__dict__[self.name] = new_list + + # Check changes and delete + if prev_list is not None and prev_list != new_list: + + # Reflect changes made inside the list + for element in prev_list: + if element not in new_list: + # Delete element reference + try: + element.delete() + except AttributeError: + # Ignore + pass + + +class ReferenceList(list): + """ + Wrapper class for a Python List object. + Modifies remove() method to use the pyaoscx.pyaoscx_module.delete() method + when using remove on this special type list. + """ + + def __init__(self, value): + list.__init__(self, value) + + def __setitem__(self, key, value): + """ + Intercept the l[key]=value operations. + Also covers slice assignment. + """ + try: + oldvalue = self.__getitem__(key) + except KeyError: + list.__setitem__(self, key, value) + else: + list.__setitem__(self, key, value) + + def __delitem__(self, key): + """ + Delete self.key + """ + oldvalue = list.__getitem__(self, key) + list.__delitem__(self, key) + + def pop(self): + """ + Remove and return item at index (default last). + """ + oldvalue = list.pop(self) + return oldvalue + + def extend(self, newvalue): + """ + Extend list by appending elements from iterable + """ + list.extend(self, newvalue) + + def insert(self, i, element): + """ + Insert object before index + """ + list.insert(self, i, element) + + def remove(self, element): + """ + Remove first occurrence of value + """ + index = list.index(self, element) + list.remove(self, element) + try: + # Delete element with a DELETE request + element.delete() + + # If delete fails because table entry + # is already deleted: IGNORE + except GenericOperationError as error: + # In case error is not 404, raise + if error.response_code != 404: + raise error + + def reverse(self): + """ + Reverse *IN PLACE* + """ + list.reverse(self) + + def sort(self, cmpfunc=None): + """ + Stable sort *IN PLACE* + """ + oldlist = self[:] + list.sort(self, cmpfunc) diff --git a/pyaoscx/utils/util.py b/pyaoscx/utils/util.py new file mode 100644 index 0000000..b1ccdeb --- /dev/null +++ b/pyaoscx/utils/util.py @@ -0,0 +1,291 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.response_error import ResponseError + + +def create_attrs(obj, data_dictionary): + ''' + Given a dictionary object creates class attributes. + The methods implements setattr() which sets the value of the specified + attribute of the specified object. + If the attribute is already created within the object. It's state changes + only if the current value is not None. Otherwise it keeps the previous + value. + + :param data_dictionary: dictionary containing the keys being used as + attributes + ''' + import copy + # Used to create a deep copy of the dictionary + dictionary_var = copy.deepcopy(data_dictionary) + + # K is the argument and V is the value of the given argument + for k, v in dictionary_var.items(): + # In case a key has '-' inside it's name. + k = k.replace('-', '_') + obj.__dict__[k] = v + + +def get_dict_keys(dict): + ''' + Function used to get a list of all the keys of the respective dictionary + + :param dict: Dictionary object used to obtain the keys + :return: List containing the keys of the given dictionary + ''' + + list = [] + for key in dict.keys(): + list.append(key) + + return list + + +def check_args(obj, **kwargs): + ''' + Given a object determines if the coming arguments are not already inside + the object + If attribute is inside the config_attrs, it is ignored + + :param obj: object in which the attributes are being set to + :param **kwargs list of arguments used to create the attributes + + :return correct: True if all arguments are correct, False otherwise + ''' + + arguments = get_dict_keys(kwargs) + correct = True + for argument in arguments: + if hasattr(obj, argument): + correct = False + return correct + + +def delete_attrs(obj, attr_list): + ''' + Given an object and a list of strings, delete attributes with the same name + as the one inside the list + :param attr_list: List of attribute names that will be deleted from object + ''' + + for attr in attr_list: + if hasattr(obj, attr): + delattr(obj, attr) + + +def get_attrs(obj, config_attrs): + ''' + Given an object obtains the attributes different to None + + :param obj: object containing the attributes + :param config_attrs: a list of all the configurable attributes within the + object + :return attr_data_dict: A dictionary containing all the attributes of the given + object that have a value different to None + ''' + attr_data_dict = {} + for attr_name in config_attrs: + attr_data_dict[attr_name] = getattr(obj, attr_name) + return attr_data_dict + + +def set_creation_attrs(obj, **kwargs): + ''' + Used when instantiating the class with new attributes. + Sets the configuration attributes list, for proper management of + attributes related to configuration. + + :param obj: Python object in which attributes are being set + :param **kwargs: a dictionary containing the possible future arguments for + the object + ''' + + if check_args(obj, **kwargs): + obj.__dict__.update(kwargs) + set_config_attrs(obj, kwargs) + else: + raise Exception("ERROR: Trying to create already existing attributes\ + inside the object") + + +def set_config_attrs(obj, config_dict, config_attrs='config_attrs', + unwanted_attrs=[]): + ''' + Add a list of strings inside the object to represent each attribute for + config + purposes. + + :param config_dict: Dictionary where each key represents an attribute + :param config_attrs: String containing the name of the attribute referring + to a list + :param unwanted_attrs: Attributes that should be deleted, since they can't be + modified + ''' + # Set new configuration attributes list + new_config_attrs = get_dict_keys(config_dict) + + # Delete unwanted attributes from configuration attributes list + for element in unwanted_attrs: + if element in new_config_attrs: + # Remove all occurrences of element inside + # the list representing the attributes related + # to configuration + new_config_attrs = list( + filter( + (element).__ne__, new_config_attrs + ) + ) + # Set config attributes list with new values + obj.__setattr__(config_attrs, new_config_attrs) + + +def _replace_special_characters(str_special_chars): + """ + Replaces special characters in a string with their percent-encoded + counterparts + ':' -> '%3A' + '/' -> '%2F' + ',' -> '%2C' + (e.g. "1/1/9" -> "1%2F1%2F9") + + :param str_special_chars: string in which to substitute characters + :return str_percents: new string with characters replaced by their + percent-encoded counterparts + """ + str_percents = str_special_chars.replace( + ":", "%3A").replace( + "/", "%2F").replace( + ",", "%2C") + return str_percents + + +def _replace_percents(str_percents): + """ + Replaces percent-encoded pieces in a string with their special-character + counterparts + '%3A' -> ':' + '%2F' -> '/' + '%2C' -> ',' + (e.g. "1%2F1%2F9" -> "1/1/9") + + :param str_percents: string in which to substitute characters + :return str_special_chars: new string with percent phrases replaced by their + special-character counterparts + """ + str_special_chars = str_percents.replace( + "%3A", ":").replace( + "%2F", "/").replace( + "%2C", ",") + return str_special_chars + + +def _response_ok(response, call_type): + """ + Checks whether API HTTP response contains the associated OK code. + + :param response: Response object + :param call_type: String containing the HTTP request type + :return: True if response was OK, False otherwise + """ + ok_codes = { + "GET": [200], + "PUT": [200, 204], + "POST": [201], + "DELETE": [204] + } + + return response.status_code in ok_codes[call_type] + + +def _replace_percents_ip(str_percents): + """ + Replaces percent-encoded pieces in a string with their special-character + counterparts + '%3A' -> ':' + '%2F' -> '/' + '%2C' -> ',' + (e.g. "1/1/9" -> "1%2F1%2F9") + + :param str_percents: string in which to substitute characters + :return str_special_chars: new string with percent phrases replaced by their special- + character counterparts + """ + str_special_chars = str_percents.replace("%2F", "/").replace("%3A", ":") + return str_special_chars + + +def _replace_special_characters_ip(str_special_chars): + """ + Replaces special characters in a string with their percent-encoded + counterparts + '/' -> '%2F' + (e.g. "2001::fe80/64" -> "/2001::fe80%2F64") + + :param str_special_chars: string in which to substitute characters + :return str_percents: new string with characters replaced by their percent-encoded + counterparts + """ + str_percents = str_special_chars.replace("/", "%2F").replace(":", "%3A") + return str_percents + + +def file_upload(session, file_path, complete_uri): + """ + Upload any file given a URI and the path to a file located on the local machine. + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param file_path: File name and path for local file uploading + :param complete_uri: Complete URI to perform the POST Request + And upload the file + Example: + https://172.25.0.2/rest/v10.04/firmware?image=primary + + :return True if successful + """ + + try: + import requests + HAS_REQUESTS_LIB = True + except ImportError: + HAS_REQUESTS_LIB = False + + # Open File + with open(file_path, 'rb') as file: + file_param = {'fileupload': file} + try: + # User session login + # Perform Login + response_login = requests.post( + session.base_url + + "login?username={}&password={}".format( + session.username(), + session.password() + ), + verify=False, timeout=5, + proxies=session.proxy) + + # Perform File Upload + response_file_upload = requests.post( + url=complete_uri, files=file_param, verify=False, + proxies=session.proxy, + cookies=response_login.cookies + ) + + # Perform Logout + requests.post( + session.base_url + "logout", + verify=False, + proxies=session.proxy, + cookies=response_login.cookies) + + except Exception as e: + raise ResponseError('POST', e) + + if response_file_upload.status_code != 200: + raise GenericOperationError( + response_file_upload.text, + response_file_upload.status_code) + + # Return true if successful + return True diff --git a/pyaoscx/vlan.py b/pyaoscx/vlan.py index 4cfe9af..df40839 100644 --- a/pyaoscx/vlan.py +++ b/pyaoscx/vlan.py @@ -1,835 +1,618 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import interface, common_ops -from pyaoscx import common_ops -from pyaoscx import port -from pyaoscx import mac +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.utils.connection import connected import json -import random import logging - - -def get_vlan(vlan_id, depth=0, selector="configuration", **kwargs): - """ - Perform a GET call to retrieve data for a VLAN table entry - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - if kwargs["url"].endswith("/v1/"): - return _get_vlan_v1(vlan_id, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_vlan(vlan_id, depth, selector, **kwargs) - - -def _get_vlan_v1(vlan_id, depth=0, selector="configuration", **kwargs): - """ - Perform a GET call to retrieve data for a VLAN table entry - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - target_url = kwargs["url"] + "system/vlans/%d" % vlan_id - - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VLAN ID '%d' table entry failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - output = {} - else: - logging.info("SUCCESS: Getting VLAN ID '%d' table entry succeeded" % vlan_id) - output = response.json() - - return output - - -def _get_vlan(vlan_id, depth=1, selector="writable", **kwargs): - """ - Perform a GET call to retrieve data for a VLAN table entry - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing port data - """ - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - target_url = kwargs["url"] + "system/vlans/%d" % vlan_id - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VLAN ID '%d' table entry failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - output = {} - else: - logging.info("SUCCESS: Getting VLAN ID '%d' table entry succeeded" % vlan_id) - output = response.json() - - return output - - -def vlan_get_all_mac_info(vlan_id, **kwargs): - """ - Perform GET calls to get info for all MAC address(es) of VLAN - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of dictionaries containing MAC info - """ - - mac_uris_percents = mac.get_all_mac_addrs(vlan_id, **kwargs) - - mac_data_list = [] - for mac_uri_percent in mac_uris_percents: - uri_split = mac_uri_percent.split('/') - mac_addr = common_ops._replace_percents(uri_split[-1]) - mac_type = uri_split[-2] - vlan_id = int(uri_split[-4]) - - mac_data = mac.get_mac_info(vlan_id, mac_type, mac_addr, **kwargs) - mac_data_list.append(mac_data) - - return mac_data_list - - -def get_all_vlans(**kwargs): - """ - Perform a GET call to get a list (or dictionary) of all entries in VLANs table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dict of all VLANs in the table - """ - target_url = kwargs["url"] + "system/vlans" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dict of all VLAN table entries failed with status code %d: %s" - % (response.status_code, response.text)) - else: - logging.info("SUCCESS: Getting list/dict of all VLAN table entries succeeded") - - vlans = response.json() - return vlans - - -def create_vlan(vlan_id, vlan_name, vlan_desc=None, vlan_type="static", admin_conf_state="up", **kwargs): - """ - Perform a POST call to create a new VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_desc: Optional description to add to VLAN - :param vlan_type: VLAN type. Defaults to "static" if not specified - :param admin_conf_state: Optional administratively-configured state of VLAN. - Only configurable for static VLANs. Defaults to "up" for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_vlan_v1(vlan_id, vlan_name, vlan_desc, vlan_type, admin_conf_state, **kwargs) - else: # Updated else for when version is v10.04 - return _create_vlan(vlan_id, vlan_name, vlan_desc, vlan_type, admin_conf_state, **kwargs) - - -def _create_vlan_v1(vlan_id, vlan_name, vlan_desc=None, vlan_type="static", admin_conf_state="up", **kwargs): - """ - Perform a POST call to create a new VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_desc: Optional description to add to VLAN - :param vlan_type: VLAN type. Defaults to "static" if not specified - :param admin_conf_state: Optional administratively-configured state of VLAN. - Only configurable for static VLANs. Defaults to "up" for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlans_list = get_all_vlans(**kwargs) - - if "/rest/v1/system/vlans/%s" % vlan_id not in vlans_list: - vlan_data = {"id": vlan_id, "name": vlan_name, "type": vlan_type} - - if vlan_desc is not None: - vlan_data["description"] = vlan_desc - - if vlan_type == "static": - # admin-configured state can only be set on static VLANs - vlan_data["admin"] = admin_conf_state - - target_url = kwargs["url"] + "system/vlans" - post_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding VLAN table entry '%s' failed with status code %d: %s" - % (vlan_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VLAN table entry '%s' succeeded" % vlan_name) - return True - else: - logging.info("SUCCESS: No need to create VLAN ID '%d' since it already exists" % vlan_id) +import re +import pyaoscx.utils.util as utils + + +class Vlan(PyaoscxModule): + ''' + Provide configuration management for VLANs on AOS-CX devices. + ''' + + base_uri = 'system/vlans' + resource_uri_name = 'vlans' + indices = ['id'] + + def __init__(self, session, vlan_id, uri=None, **kwargs): + + self.session = session + self._uri = uri + self.id = vlan_id + # List used to determine attributes related to the VLAN configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a VLAN table entry and fill + the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch VLANs") + + depth = self.session.api_version.default_depth if depth is None \ + else depth + selector = self.session.api_version.default_selector if selector \ + is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=Vlan.base_uri, + id=self.id + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError( + response.text, response.status_code, "GET VLAN") + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the VLAN is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, data, 'config_attrs', ['id']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'id' in self.__original_attributes: + self.__original_attributes.pop('id') + + # Set all ACLs + from pyaoscx.acl import ACL + if hasattr(self, 'aclmac_in_cfg') and self.aclmac_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclmac_in_cfg) + # Materialize Acl object + acl.get() + self.aclmac_in_cfg = acl + + if hasattr(self, 'aclv4_in_cfg') and self.aclv4_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv4_in_cfg) + # Materialize Acl object + acl.get() + self.aclv4_in_cfg = acl + + if hasattr(self, 'aclv6_in_cfg') and self.aclv6_in_cfg is not None: + # Create Acl object + acl = ACL.from_response(self.session, self.aclv6_in_cfg) + # Materialize Acl object + acl.get() + self.aclv6_in_cfg = acl + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True return True + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system VLAN and create a dictionary + containing each respective VLAN + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing VLAN IDs as keys and a Vlan object as + value + ''' + + logging.info("Retrieving the switch VLANs") + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=Vlan.base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + vlans_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a Vlan object + vlan_id, vlan = Vlan.from_uri(session, uri) + + vlans_dict[vlan_id] = vlan + + return vlans_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing + VLAN table entry. + Checks whether the VLAN exists in the switch + Calls self.update() if VLAN is being updated + Calls self.create() if a new VLAN is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified -def _create_vlan(vlan_id, vlan_name, vlan_desc=None, vlan_type="static", admin_conf_state="up", **kwargs): - """ - Perform a POST call to create a new VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_desc: Optional description to add to VLAN - :param vlan_type: VLAN type. Defaults to "static" if not specified - :param admin_conf_state: Optional administratively-configured state of VLAN. - Only configurable for static VLANs. Defaults to "up" for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlans_dict = get_all_vlans(**kwargs) + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing VLAN table entry - if str(vlan_id) not in vlans_dict: - vlan_data = {"id": vlan_id, "name": vlan_name, "type": vlan_type} + :return modified: True if Object was modified and a PUT request was made. + False otherwise - if vlan_desc is not None: - vlan_data["description"] = vlan_desc + ''' + # Variable returned + modified = False - if vlan_type == "static": - # admin-configured state can only be set on static VLANs - vlan_data["admin"] = admin_conf_state + vlan_data = {} - target_url = kwargs["url"] + "system/vlans" - post_data = json.dumps(vlan_data, sort_keys=True, indent=4) + vlan_data = utils.get_attrs(self, self.config_attrs) - response = kwargs["s"].post(target_url, data=post_data, verify=False) + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=Vlan.base_uri, + id=self.id + ) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Adding VLAN table entry '%s' failed with status code %d: %s" - % (vlan_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VLAN table entry '%s' succeeded" % vlan_name) - return True - else: - logging.info("SUCCESS: No need to create VLAN ID '%d' since it already exists" % vlan_id) - return True + # Set all ACLs + if "aclmac_in_cfg" in vlan_data and self.aclmac_in_cfg is not None: + # Set values in correct form + vlan_data["aclmac_in_cfg"] = \ + self.aclmac_in_cfg.get_info_format() + if "aclv4_in_cfg" in vlan_data and self.aclv4_in_cfg is not None: + # Set values in correct form + vlan_data["aclv4_in_cfg"] = self.aclv4_in_cfg.get_info_format() -def modify_vlan(vlan_id, vlan_name=None, vlan_desc=None, admin_conf_state=None, **kwargs): - """ - Perform GET and PUT calls to modify an existing VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Optional Alphanumeric name of VLAN. Won't be modified if not specified. - :param vlan_desc: Optional description to add to VLAN. Won't be modified if not specified. - :param admin_conf_state: Optional administratively-configured state of VLAN. Won't be modified if not specified. - Only configurable for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _modify_vlan_v1(vlan_id, vlan_name, vlan_desc, admin_conf_state, **kwargs) - else: # Updated else for when version is v10.04 - return _modify_vlan(vlan_id, vlan_name, vlan_desc, admin_conf_state, **kwargs) - - -def _modify_vlan_v1(vlan_id, vlan_name=None, vlan_desc=None, admin_conf_state=None, **kwargs): - """ - Perform GET and PUT calls to modify an existing VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Optional Alphanumeric name of VLAN. Won't be modified if not specified. - :param vlan_desc: Optional description to add to VLAN. Won't be modified if not specified. - :param admin_conf_state: Optional administratively-configured state of VLAN. Won't be modified if not specified. - Only configurable for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - vlan_data = get_vlan(vlan_id, depth=0, selector="configuration", **kwargs) - vlan_data.pop('id', None) # id cannot be modified - - if vlan_name is not None: - vlan_data["name"] = vlan_name - - if vlan_desc is not None: - vlan_data["description"] = vlan_desc - - if vlan_data['type'] == "static" and admin_conf_state is not None: - # admin-configured state can only be set on static VLANs - vlan_data["admin"] = admin_conf_state - - target_url = kwargs["url"] + "system/vlans/%d" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Modifying VLAN ID '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Modifying VLAN ID '%d' succeeded" % vlan_id) - return True + if "aclv6_in_cfg" in vlan_data and self.aclv6_in_cfg is not None: + # Set values in correct form + vlan_data["aclv6_in_cfg"] = self.aclv6_in_cfg.get_info_format() + # Compare dictionaries + if vlan_data == self.__original_attributes: + # Object was not modified + modified = False -def _modify_vlan(vlan_id, vlan_name=None, vlan_desc=None, admin_conf_state=None, **kwargs): - """ - Perform GET and PUT calls to modify an existing VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Optional Alphanumeric name of VLAN. Won't be modified if not specified. - :param vlan_desc: Optional description to add to VLAN. Won't be modified if not specified. - :param admin_conf_state: Optional administratively-configured state of VLAN. Won't be modified if not specified. - Only configurable for static VLANs. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ + else: - vlan_data = get_vlan(vlan_id, depth=1, selector="writable", **kwargs) + post_data = json.dumps(vlan_data, sort_keys=True, indent=4) - if vlan_name is not None: - vlan_data["name"] = vlan_name + try: + response = self.session.s.put( + uri, verify=False, data=post_data, + proxies=self.session.proxy) - if vlan_desc is not None: - vlan_data["description"] = vlan_desc + except Exception as e: + raise ResponseError('PUT', e) - if vlan_data['type'] == "static" and admin_conf_state is not None: - # admin-configured state can only be set on static VLANs - vlan_data["admin"] = admin_conf_state + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code, "UPDATE VLAN") - target_url = kwargs["url"] + "system/vlans/%d" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) + else: + logging.info("SUCCESS: Adding VLAN table entry '{}' \ + succeeded".format(self.id)) + # Set new original attributes + self.__original_attributes = vlan_data - response = kwargs["s"].put(target_url, data=put_data, verify=False) + # Object was modified, returns True + modified = True - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Modifying VLAN ID '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Modifying VLAN ID '%d' succeeded" % vlan_id) - return True + return modified + @connected + def create(self): + ''' + Perform a POST call to create a new VLAN using the object's attributes + as POST body. Exception is raised if object is unable to be created -def create_vlan_and_svi(vlan_id, vlan_name, vlan_port_name, vlan_int_name, vlan_desc=None, ipv4=None, - vrf_name="default", vlan_port_desc=None, **kwargs): - """ - Perform POST and PUT calls to create a new VLAN and SVI. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_port_name: Alphanumeric Port name - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_desc: Optional description to add to VLAN - :param ipv4: Optional IPv4 address to assign to the interface.Defaults to nothing if not specified. - :param vrf_name: VRF to attach the SVI to. Defaults to "default" if not specified - :param vlan_port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_vlan_and_svi_v1(vlan_id, vlan_name, vlan_port_name, vlan_int_name, vlan_desc, ipv4, - vrf_name, vlan_port_desc, **kwargs) - else: # Updated else for when version is v10.04 - return _create_vlan_and_svi(vlan_id, vlan_name, vlan_port_name, vlan_int_name, vlan_desc, ipv4, - vrf_name, vlan_port_desc, **kwargs) - - -def _create_vlan_and_svi_v1(vlan_id, vlan_name, vlan_port_name, vlan_int_name, vlan_desc=None, ipv4=None, - vrf_name="default", vlan_port_desc=None, **kwargs): - """ - Perform POST and PUT calls to create a new VLAN and SVI. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_port_name: Alphanumeric Port name - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_desc: Optional description to add to VLAN - :param ipv4: Optional IPv4 address to assign to the interface.Defaults to nothing if not specified. - :param vrf_name: VRF to attach the SVI to. Defaults to "default" if not specified - :param vlan_port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - # Add a new VLAN to the VLAN table - success = create_vlan(vlan_id, vlan_name, vlan_desc, **kwargs) - - # Add a new entry to the Port table - success = success and port.add_vlan_port(vlan_port_name, vlan_id, ipv4, vrf_name, vlan_port_desc, **kwargs) - - # Add a new entry to the Interface table - return success and interface.add_vlan_interface(vlan_int_name, vlan_port_name, vlan_id, ipv4, vrf_name, vlan_port_desc, **kwargs) - - -def _create_vlan_and_svi(vlan_id, vlan_name, vlan_port_name, vlan_int_name, vlan_desc=None, ipv4=None, - vrf_name="default", vlan_port_desc=None, **kwargs): - """ - Perform POST and PUT calls to create a new VLAN and SVI. - - :param vlan_id: Numeric ID of VLAN - :param vlan_name: Alphanumeric name of VLAN - :param vlan_port_name: Alphanumeric Port name - :param vlan_int_name: Alphanumeric name for the VLAN interface - :param vlan_desc: Optional description to add to VLAN - :param ipv4: Optional IPv4 address to assign to the interface.Defaults to nothing if not specified. - :param vrf_name: VRF to attach the SVI to. Defaults to "default" if not specified - :param vlan_port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - # Add a new VLAN to the VLAN table - success = create_vlan(vlan_id, vlan_name, vlan_desc, **kwargs) - - # Add a new entry to the Interface table - return success and interface.add_vlan_interface(vlan_int_name, vlan_port_name, vlan_id, ipv4, vrf_name, vlan_port_desc, **kwargs) - - -def delete_vlan(vlan_id, **kwargs): - """ - Perform a DELETE call to delete VLAN. - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - if kwargs["url"].endswith("/v1/"): - return _delete_vlan_v1(vlan_id, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_vlan(vlan_id, **kwargs) - - -def _delete_vlan_v1(vlan_id, **kwargs): - """ - Perform a DELETE call to delete VLAN. - - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - vlans_list = get_all_vlans(**kwargs) - - if "/rest/v1/system/vlans/%s" % vlan_id in vlans_list: - - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id - - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VLAN ID: '%s' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VLAN ID: '%s' succeeded" % vlan_id) - return True - else: - logging.info("SUCCESS: No need to remove VLAN ID '%d' since it doesn't exist" % vlan_id) - return True + :return modified: Boolean, True if entry was created + ''' + vlan_data = {} -def _delete_vlan(vlan_id, **kwargs): - """ - Perform a DELETE call to delete VLAN. + # Get all VLAN data given by the user + vlan_data = utils.get_attrs(self, self.config_attrs) + if isinstance(self.id, str): + self.id = int(self.id) + vlan_data['id'] = self.id - :param vlan_id: Numeric ID of VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vlan.base_uri + ) - vlans_dict = get_all_vlans(**kwargs) + post_data = json.dumps(vlan_data, sort_keys=True, indent=4) - if str(vlan_id) in vlans_dict: + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id + except Exception as e: + raise ResponseError('POST', e) - response = kwargs["s"].delete(target_url, verify=False) + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VLAN ID: '%s' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting VLAN ID: '%s' succeeded" % vlan_id) - return True - else: - logging.info("SUCCESS: No need to remove VLAN ID '%d' since it doesn't exist" % vlan_id) - return True + logging.info("SUCCESS: Adding VLAN table entry '{}' \ + succeeded".format(self.id)) + # Get all objects data + self.get() -def delete_vlan_and_svi(vlan_id, vlan_port_name, **kwargs): - """ - Perform PUT and DELETE calls to delete SVI and VLAN. - - :param vlan_id: Numeric ID of VLAN - :param vlan_port_name: Name of SVI's entry in Port table - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - # Delete VLAN SVI - success = interface.delete_interface(vlan_port_name, **kwargs) - - # Delete VLAN - return success and delete_vlan(vlan_id, **kwargs) - - -def attach_vlan_acl(vlan_id, list_name, list_type, **kwargs): - """ - Perform GET and PUT calls to attach an ACL to a VLAN - - :param vlan_id: Numeric ID of VLAN - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _attach_vlan_acl_v1(vlan_id, list_name, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _attach_vlan_acl(vlan_id, list_name, list_type, **kwargs) - - -def _attach_vlan_acl_v1(vlan_id, list_name, list_type, **kwargs): - """ - Perform GET and PUT calls to attach an ACL to a VLAN - - :param vlan_id: Numeric ID of VLAN - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlan_data = get_vlan(vlan_id, depth=0, selector="configuration", **kwargs) - - if list_type == "ipv4": - vlan_data['aclv4_in_cfg'] = "/rest/v1/system/acls/%s/%s" % (list_name, list_type) - vlan_data['aclv4_in_cfg_version'] = random.randrange(9007199254740991) - - if list_type == "ipv6": - vlan_data['aclv6_in_cfg'] = "/rest/v1/system/acls/%s/%s" % (list_name, list_type) - vlan_data['aclv6_in_cfg_version'] = random.randrange(9007199254740991) - - if list_type == "mac": - vlan_data['aclmac_in_cfg'] = "/rest/v1/system/acls/%s/%s" % (list_name, list_type) - vlan_data['aclmac_in_cfg_version'] = random.randrange(9007199254740991) - - # must remove these fields from the data since they can't be modified - vlan_data.pop('id', None) - vlan_data.pop('type', None) - - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s ACL on VLAN %d to '%s' failed with status code %d: %s" - % (list_type, vlan_id, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s ACL on VLAN %d to '%s' succeeded" - % (list_type, vlan_id, list_name)) return True + @connected + def delete(self): + ''' + Perform DELETE call to delete VLAN table entry. -def _attach_vlan_acl(vlan_id, list_name, list_type, **kwargs): - """ - Perform GET and PUT calls to attach an ACL to a VLAN - - :param vlan_id: Numeric ID of VLAN - :param list_name: Alphanumeric name of the ACL - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlan_data = get_vlan(vlan_id, depth=2, selector="writable", **kwargs) - - if list_type == "ipv4": - vlan_data['aclv4_in_cfg'] = "/rest/v10.04/system/acls/%s,%s" % (list_name, list_type) - vlan_data['aclv4_in_cfg_version'] = random.randrange(9007199254740991) - - if list_type == "ipv6": - vlan_data['aclv6_in_cfg'] = "/rest/v10.04/system/acls/%s,%s" % (list_name, list_type) - vlan_data['aclv6_in_cfg_version'] = random.randrange(9007199254740991) - - if list_type == "mac": - vlan_data['aclmac_in_cfg'] = "/rest/v10.04/system/acls/%s,%s" % (list_name, list_type) - vlan_data['aclmac_in_cfg_version'] = random.randrange(9007199254740991) - - # must remove these fields from the data since they can't be modified - vlan_data.pop('id', None) - vlan_data.pop('type', None) - - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating %s ACL on VLAN %d to '%s' failed with status code %d: %s" - % (list_type, vlan_id, list_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating %s ACL on VLAN %d to '%s' succeeded" - % (list_type, vlan_id, list_name)) - return True + ''' + uri = "{base_url}{class_uri}/{id}".format( + base_url=self.session.base_url, + class_uri=Vlan.base_uri, + id=self.id + ) -def detach_vlan_acl(vlan_id, list_type, **kwargs): - """ - Perform GET and PUT calls to detach ACL from a VLAN - - :param vlan_id: Numeric ID of VLAN - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _detach_vlan_acl_v1(vlan_id, list_type, **kwargs) - else: # Updated else for when version is v10.04 - return _detach_vlan_acl(vlan_id, list_type, **kwargs) - - -def _detach_vlan_acl_v1(vlan_id, list_type, **kwargs): - """ - Perform GET and PUT calls to detach ACL from a VLAN - - :param vlan_id: Numeric ID of VLAN - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlan_data = get_vlan(vlan_id, depth=0, selector="configuration", **kwargs) - - if list_type == "ipv4": - vlan_data.pop('aclv4_in_cfg', None) - vlan_data.pop('aclv4_in_cfg_version', None) - - if list_type == "ipv6": - vlan_data.pop('aclv6_in_cfg', None) - vlan_data.pop('aclv6_in_cfg_version', None) - - if list_type == "mac": - vlan_data.pop('aclmac_in_cfg', None) - vlan_data.pop('aclmac_in_cfg_version', None) - - # must remove these fields from the data since they can't be modified - vlan_data.pop('id', None) - vlan_data.pop('type', None) - - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing %s ACL from VLAN %d failed with status code %d: %s" - % (list_type, vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing %s ACL from VLAN %d succeeded" - % (list_type, vlan_id)) - return True - + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) -def _detach_vlan_acl(vlan_id, list_type, **kwargs): - """ - Perform GET and PUT calls to detach ACL from a VLAN + except Exception as e: + raise ResponseError('DELETE', e) - :param vlan_id: Numeric ID of VLAN - :param list_type: Type should be one of "ipv4," "ipv6," or "mac" - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vlan_data = get_vlan(vlan_id, depth=1, selector="writable", **kwargs) + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError( + response.text, response.status_code, "DELETE VLAN") - if list_type == "ipv4": - vlan_data.pop('aclv4_in_cfg', None) - vlan_data.pop('aclv4_in_cfg_version', None) - - if list_type == "ipv6": - vlan_data.pop('aclv6_in_cfg', None) - vlan_data.pop('aclv6_in_cfg_version', None) - - if list_type == "mac": - vlan_data.pop('aclmac_in_cfg', None) - vlan_data.pop('aclmac_in_cfg_version', None) - - target_url = kwargs["url"] + "system/vlans/%s" % vlan_id - put_data = json.dumps(vlan_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Removing %s ACL from VLAN %d failed with status code %d: %s" - % (list_type, vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Removing %s ACL from VLAN %d succeeded" - % (list_type, vlan_id)) - return True + else: + logging.info("SUCCESS: Delete VLAN table entry '{}'\ + succeeded".format(self.id)) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, response_data): + ''' + Create a Vlan object given a response_data related to the Vlan object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + dictionary: { + 1: "/rest/v10.04/system/vlans/1" + } + or a + string: "/rest/v1/system/vlans/1" + :return: Vlan Object + ''' + vlan_id_arr = session.api_version.get_keys( + response_data, Vlan.resource_uri_name) + vlan_id = vlan_id_arr[0] + return Vlan(session, vlan_id) + + @classmethod + def from_uri(cls, session, uri): + ''' + Create a Vlan object given a VLAN URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return vlan_id, vlan: tuple containing both the Vlan object and the VLAN's + ID + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)vlans/(?P.+)') + index_str = index_pattern.match(uri).group('index') + vlan_id = int(index_str) + # Create Vlan object + vlan_obj = Vlan(session, vlan_id, uri=uri) + + return vlan_id, vlan_obj + + @classmethod + def get_facts(cls, session): + ''' + Modify this to Perform a GET call to retrieve all VLANs and their respective data + :param cls: Class reference. + :param session: pyaoscx.Session object used to represent a logical + connection to the device + + :return facts: Dictionary containing VLAN IDs as keys and Vlan objects as values + + ''' + # Log + logging.info("Retrieving switch VLANs facts") + + # Set VLAN facts depth + vlan_depth = session.api_version.default_facts_depth + + # Build URI + uri = '{base_url}{class_uri}?depth={depth}'.format( + base_url=session.base_url, + class_uri=Vlan.base_uri, + depth=vlan_depth + ) + + try: + # Try to get facts data via GET method + response = session.s.get( + uri, + verify=False, + proxies=session.proxy + ) + + except Exception as e: + raise ResponseError('GET', e) + if not utils._response_ok(response, "GET"): + raise GenericOperationError( + response.text, + response.status_code) + + # Load response text into json format + facts = json.loads(response.text) + + # Delete internal VLANs + internal_vlan_list = [] + for vlan in facts.keys(): + if 'type' in facts[vlan].keys(): + if facts[vlan]['type'] == 'internal': + internal_vlan_list.append(vlan) + + for vlan in internal_vlan_list: + facts.pop(vlan) + + return facts + + def __str__(self): + try: + return "Vlan, name: '{}' ID: '{}' and description: '{}'"\ + .format(self.name, self.id, self.description) + except Exception: + return "Vlan, ID: '{}'".format(self.id) + + def get_uri(self): + ''' + Method used to obtain the specific VLAN URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{id}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Vlan.base_uri, + id=self.id + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def modify(self, vlan_name=None, vlan_desc=None, admin_conf_state=None): + """ + Perform a PUT calls to modify an existing VLAN. + + :param vlan_name: Optional Alphanumeric name of VLAN. Won't be + modified if not specified. + :param vlan_desc: Optional description to add to VLAN. Won't be + modified if not specified. + :param admin_conf_state: Optional administratively-configured state of + VLAN. Won't be modified if not specified. + Only configurable for static VLANs. + :return: True if object was changed, False otherwise + """ + + if vlan_name is not None: + self.name = vlan_name + if vlan_desc is not None: + self.description = vlan_desc -def port_set_vlan_mode(l2_port_name, vlan_mode, **kwargs): - """ - Perform GET and PUT calls to set an L2 interface's VLAN mode (native-tagged, native-untagged, or access) - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_mode: A string, either 'native-tagged', 'native-untagged', or 'access', specifying the desired VLAN - mode - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._port_set_vlan_mode(l2_port_name, vlan_mode, **kwargs) - else: # Updated else for when version is v10.04 - return interface._port_set_vlan_mode(l2_port_name, vlan_mode, **kwargs) - - -def port_add_vlan_trunks(l2_port_name, vlan_trunk_ids=[], **kwargs): - """ - Perform GET and PUT/POST calls to add specified VLANs to a trunk port. By default, this will also set the port to - have 'no routing' and if there is not a native VLAN, will set the native VLAN to VLAN 1. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_trunk_ids: List of VLANs to specify as allowed on the trunk port. If empty, the interface will - allow all VLANs on the trunk. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._port_add_vlan_trunks(l2_port_name, vlan_trunk_ids, **kwargs) - else: # Updated else for when version is v10.04 - return interface._port_add_vlan_trunks(l2_port_name, vlan_trunk_ids, **kwargs) - - -def port_set_native_vlan(l2_port_name, vlan_id, tagged=True, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN to be the native VLAN on the trunk. Also gives the option to set - the VLAN as tagged. - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to add to trunk port - :param tagged: Boolean to determine if the native VLAN will be set as the tagged VLAN. If False, the VLAN - will be set as the native untagged VLAN - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._port_set_native_vlan(l2_port_name, vlan_id, tagged, **kwargs) - else: # Updated else for when version is v10.04 - return interface._port_set_native_vlan(l2_port_name, vlan_id, tagged, **kwargs) - - -def port_delete_vlan_port(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT calls to remove a VLAN from a trunk port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to remove from trunk port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._delete_vlan_port(l2_port_name, vlan_id, **kwargs) - else: # Updated else for when version is v10.04 - return interface._delete_vlan_port(l2_port_name, vlan_id, **kwargs) - - -def port_set_untagged_vlan(l2_port_name, vlan_id, **kwargs): - """ - Perform GET and PUT/POST calls to set a VLAN on an access port - - :param l2_port_name: L2 interface's Port table entry name - :param vlan_id: Numeric ID of VLAN to set on access port - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._port_set_untagged_vlan(l2_port_name, vlan_id, **kwargs) - else: # Updated else for when version is v10.04 - return interface._port_set_untagged_vlan(l2_port_name, vlan_id, **kwargs) \ No newline at end of file + if self.type == "static" and admin_conf_state is not None: + # admin-configured state can only be set on static VLANs + self.admin = admin_conf_state + + # Apply changes inside switch + return self.apply() + + def attach_acl_in(self, acl_name, list_type): + """ + Update ACL IN values inside a Vlan object + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of ipv4, ipv6, or mac to + specify the type of ACL + :return: True if object was changed, False otherwise + + """ + import random + + # Create Acl object + acl_obj = self.session.api_version.get_module( + self.session, 'ACL', index_id=acl_name, list_type=list_type) + + if list_type == "ipv6": + self.aclv6_in_cfg = acl_obj + if self.aclv6_in_cfg_version is None: + self.aclv6_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "ipv4": + self.aclv4_in_cfg = acl_obj + if self.aclv4_in_cfg_version is None: + self.aclv4_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "mac": + self.aclmac_in_cfg = acl_obj + if self.aclmac_in_cfg_version is None: + self.aclmac_in_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + + # Apply changes + return self.apply() + + def attach_acl_out(self, acl_name, list_type): + """ + Update ACL OUT values inside a Vlan object + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of ipv4, ipv6, or mac to + specify the type of ACL + :return: True if object was changed, False otherwise + + """ + import random + + # Create Acl object + acl_obj = self.session.api_version.get_module( + self.session, 'ACL', index_id=acl_name, list_type=list_type) + + if list_type == "ipv6": + self.aclv6_out_cfg = acl_obj + if self.aclv6_out_cfg_version is None: + self.aclv6_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "ipv4": + self.aclv4_out_cfg = acl_obj + if self.aclv4_out_cfg_version is None: + self.aclv4_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + if list_type == "mac": + self.aclmac_out_cfg = acl_obj + if self.aclmac_out_cfg_version is None: + self.aclmac_out_cfg_version = random.randint( + -9007199254740991, 9007199254740991) + + # Apply changes + return self.apply() + + def detach_acl_in(self, acl_name, list_type): + """ + Detach an ACL from a VLAN + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of ipv4, ipv6, or mac to + specify the type of ACL + :return: True if object was changed, False otherwise + + """ + + if list_type == "ipv6": + self.aclv6_in_cfg = None + self.aclv6_in_cfg_version = None + elif list_type == "ipv4": + self.aclv4_in_cfg = None + self.aclv4_in_cfg_version = None + elif list_type == "mac": + self.aclmac_in_cfg = None + self.aclmac_in_cfg_version = None + + # Apply changes + return self.apply() + + def detach_acl_out(self, acl_name, list_type): + """ + Detach an ACL from a VLAN + + :param acl_name: Alphanumeric String that is the name of the ACL + :param list_type: Alphanumeric String of ipv4, ipv6, or mac to + specify the type of ACL + :return: True if object was changed, False otherwise + + """ + + if list_type == "ipv6": + self.aclv6_out_cfg = None + self.aclv6_out_cfg_version = None + elif list_type == "ipv4": + self.aclv4_out_cfg = None + self.aclv4_out_cfg_version = None + elif list_type == "mac": + self.aclmac_out_cfg = None + self.aclmac_out_cfg_version = None + + # Apply changes + return self.apply() diff --git a/pyaoscx/vrf.py b/pyaoscx/vrf.py index 3763778..db4914b 100644 --- a/pyaoscx/vrf.py +++ b/pyaoscx/vrf.py @@ -1,348 +1,641 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import common_ops +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError + +from pyaoscx.bgp_router import BgpRouter +from pyaoscx.ospf_router import OspfRouter +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.static_route import StaticRoute +from pyaoscx.vrf_address_family import VrfAddressFamily +from pyaoscx.utils.connection import connected +from pyaoscx.exceptions.verification_error import VerificationError import json import logging +import re +import pyaoscx.utils.util as utils +from pyaoscx.utils.list_attributes import ListDescriptor + + +class Vrf(PyaoscxModule): + ''' + Provide configuration management for VRF on AOS-CX devices. + ''' + + base_uri = 'system/vrfs' + indices = ['name'] + resource_uri_name = 'vrfs' + + bgp_routers = ListDescriptor('bgp_routers') + address_families = ListDescriptor('address_families') + ospf_routers = ListDescriptor('ospf_routers') + static_routes = ListDescriptor('static_routes') + + def __init__(self, session, name, uri=None, **kwargs): + + self.session = session + self._uri = uri + self.name = name + # List used to determine attributes related to the VRF configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + + # Use to manage BGP Routers + self.bgp_routers = [] + # Use to manage Vrf Address Families + self.address_families = [] + # Use to manage OSPF Routers + self.ospf_routers = [] + # Use to manage Static Routes + self.static_routes = [] + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a VRF table entry and fill the + class with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information + to return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch VRF") + + depth = self.session.api_version.default_depth if depth is \ + None else depth + selector = self.session.api_version.default_selector if selector\ + is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Vrf.base_uri, + name=self.name + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + # Delete unwanted data + if 'ospf_routers' in data: + data.pop('ospf_routers') + data.pop('bgp_routers') + if 'static_routes' in data: + data.pop("static_routes") + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the VRF is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, data, 'config_attrs', [ + 'name', 'type', 'bgp_routers', + 'ospf_routers', 'vrf_address_families', + 'static_routes']) + + # Set original attributes + self.__original_attributes = data + # Remove ID + if 'name' in self.__original_attributes: + self.__original_attributes.pop('name') + # Remove type + if 'type' in self.__original_attributes: + self.__original_attributes.pop('type') + # Remove bgp_routers + if 'bgp_routers' in self.__original_attributes: + self.__original_attributes.pop('bgp_routers') + # Remove ospf_routers + if 'ospf_routers' in self.__original_attributes: + self.__original_attributes.pop('ospf_routers') + # Remove static_routes + if 'static_routes' in self.__original_attributes: + self.__original_attributes.pop('static_routes') + # Remove vrf_address_families + if 'vrf_address_families' in self.__original_attributes: + self.__original_attributes.pop('vrf_address_families') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + + # Clean BGP Router settings + if self.bgp_routers == []: + # Set BGP Routers if any + # Adds bgp_bouters to parent Vrf object + BgpRouter.get_all(self.session, self) + + # Clean Address Families settings + if self.address_families == []: + # Set Address Families if any + # Adds address_families to parent Vrf object + VrfAddressFamily.get_all(self.session, self) + + # Clean OSPF Routers settings + if self.ospf_routers == []: + # Set OSPF Routers if any + # Adds ospf_routers to parent Vrf object + OspfRouter.get_all(self.session, self) + + # Clean Static Routess settings + if self.static_routes == []: + # Set Static Route if any + # Adds static_routes to parent Vrf object + StaticRoute.get_all(self.session, self) + return True -def get_all_vrfs(**kwargs): - """ - Perform a GET call to get a list (or dictionary) of all entries in VRF table - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List/dict of all VRFs in the table - """ - target_url = kwargs["url"] + "system/vrfs" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dict of all VRF table entries failed with status code %d: %s" - % (response.status_code, response.text)) - vrfs = [] - else: - logging.info("SUCCESS: Getting list/dict of all VRF table entries succeeded") - vrfs = response.json() - - return vrfs - - -def add_vrf(vrf_name, route_distinguisher=None, vrf_type="user", **kwargs): - """ - Perform a POST call to create a new VRF, and add a route distinguisher if desired. - - :param vrf_name: Alphanumeric name of VRF - :param route_distinguisher: Optional route distinguisher to add. Defaults to nothing if not specified. - :param vrf_type: Optional VRF type. Defaults to "user" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return add_vrf_v1(vrf_name, route_distinguisher, vrf_type, **kwargs) - else: # Updated else for when version is v10.04 - return _add_vrf(vrf_name, route_distinguisher, vrf_type, **kwargs) - - -def add_vrf_v1(vrf_name, route_distinguisher=None, vrf_type="user", **kwargs): - """ - Perform a POST call to create a new VRF, and add a route distinguisher if desired. - - :param vrf_name: name of VRF - :param route_distinguisher: Optional route distinguisher to add. Defaults to nothing if not specified. - :param vrf_type: Optional VRF type. Defaults to "user" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrfs_list = get_all_vrfs(**kwargs) - - if "/rest/v1/system/vrfs/%s" % vrf_name not in vrfs_list: - vrf_data = {"name": vrf_name, "type": vrf_type} - - if route_distinguisher is not None: - vrf_data["rd"] = route_distinguisher - - target_url = kwargs["url"] + "system/vrfs" - post_data = json.dumps(vrf_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) + @classmethod + def get_all(cls, session): + ''' + Perform a GET call to retrieve all system VRFs and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :return: Dictionary containing VRF names as keys and a Vrf objects as + values + ''' + logging.info("Retrieving the VRFs inside the switch") + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=Vrf.base_uri + ) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + vrfs_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create Vrf object + name, vrf = Vrf.from_uri(session, uri) + # Set VRF in dictionary + vrfs_dict[name] = vrf + + return vrfs_dict + + @connected + def apply(self): + ''' + Main method used to either create or update an existing VRF table entry. + Checks whether the VRF exists in the switch + Calls self.update() if VRF is being updated + Calls self.create() if a new VRF is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing VRF table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + vrf_data = {} + + vrf_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Vrf.base_uri, + name=self.name + ) + + # Compare dictionaries + # if vrf_data == self.__original_attributes: + if json.dumps( + vrf_data, sort_keys=True, indent=4) == \ + json.dumps( + self.__original_attributes, sort_keys=True, indent=4): + # Object was not modified + modified = False - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating new VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Creating new VRF '%s' succeeded" % vrf_name) - return True - else: - logging.info("SUCCESS: No need to create VRF '%s' since it already exists" % vrf_name) - return True + post_data = json.dumps(vrf_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, + proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + else: + logging.info( + "SUCCESS: Adding VRF table entry '%s' succeeded" % + self.name) + # Set new original attributes + self.__original_attributes = vrf_data + modified = True + return modified -def _add_vrf(vrf_name, route_distinguisher=None, vrf_type="user", **kwargs): - """ - Perform a POST call to create a new VRF, and add a route distinguisher if desired. + @connected + def create(self): + ''' + Perform a POST call to create a new VRF using the object's attributes + as POST body + Only returns if an exception is not raise - :param vrf_name: name of VRF - :param route_distinguisher: Optional route distinguisher to add. Defaults to nothing if not specified. - :param vrf_type: Optional VRF type. Defaults to "user" if not specified. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrfs_dict = get_all_vrfs(**kwargs) + :return modified: Boolean, True if entry was created + ''' - if vrf_name not in vrfs_dict: - vrf_data = {"name": vrf_name, "type": vrf_type} + vrf_data = {} + vrf_data = utils.get_attrs(self, self.config_attrs) - if route_distinguisher is not None: - vrf_data["rd"] = route_distinguisher + vrf_data['name'] = self.name + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vrf.base_uri + ) - target_url = kwargs["url"] + "system/vrfs" post_data = json.dumps(vrf_data, sort_keys=True, indent=4) + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) - response = kwargs["s"].post(target_url, data=post_data, verify=False) + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating new VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Creating new VRF '%s' succeeded" % vrf_name) - return True - else: - logging.info("SUCCESS: No need to create VRF '%s' since it already exists" % vrf_name) - return True + logging.info( + "SUCCESS: Adding VRF table entry '%s' succeeded" % self.name) + # Get all objects data + self.get() -def get_vrf(vrf_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to get data for a VRF table entry - - :param vrf_name: Alphanumeric name of the VRF - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing the VRF data - """ - if kwargs["url"].endswith("/v1/"): - return _get_vrf_v1(vrf_name, depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_vrf(vrf_name, depth, selector, **kwargs) - - -def _get_vrf_v1(vrf_name, depth=0, selector=None, **kwargs): - """ - Perform a GET call to get data for a VRF table entry - - :param vrf_name: Alphanumeric name of the VRF - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing the VRF data - """ - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VRF table entry '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - vrf = [] - else: - logging.info("SUCCESS: Getting VRF table entry '%s' succeeded" % vrf_name) - vrf = response.json() - - return vrf - - -def _get_vrf(vrf_name, depth=1, selector=None, **kwargs): - """ - Perform a GET call to get data for a VRF table entry - - :param vrf_name: Alphanumeric name of the VRF - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing the VRF data - """ - - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VRF table entry '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - vrf = [] - else: - logging.info("SUCCESS: Getting VRF table entry '%s' succeeded" % vrf_name) - vrf = response.json() - - return vrf - - -def delete_vrf(vrf_name, **kwargs): - """ - Perform a DELETE call to delete a VRF. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of VRF - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrf_list = get_all_vrfs(**kwargs) - - if kwargs["url"].endswith("/v1/"): - vrf_check = "/rest/v1/system/vrfs/%s" % vrf_name - else: - # Else logic designed for v10.04 and later - vrf_check = vrf_name - - if vrf_check in vrf_list: - target_url = kwargs["url"] + "system/vrfs/%s" % vrf_name - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VRF '%s' failed with status code %d: %s" - % (vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VRF '%s' succeeded" % vrf_name) - return True - else: - logging.info("SUCCESS: No need to delete VRF '%s' since it doesn't exist" - % vrf_name) + # Object was modified return True + @connected + def delete(self): + ''' + Perform DELETE call to delete VRF table entry. -def add_vrf_address_family(vrf_name, family_type="ipv4_unicast", export_target=[], import_targets=[], **kwargs): - """ - Perform a POST call to create a new VRF, and add a route distinguisher if desired. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of VRF - :param family_type: Alphanumeric type of the Address Family. The options are 'ipv4_unicast' and 'ipv6_unicast'. - The default value is set to 'ipv4_unicast'. - :param export_target: Optional list of export route targets. - :param import_targets: Optional list of import route targets - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrf_list = get_all_vrfs(**kwargs) - - if family_type == "ipv4-unicast": - family_type = "ipv4_unicast" - elif family_type == "ipv6-unicast": - family_type = "ipv6_unicast" - - if family_type not in ['ipv4_unicast', 'ipv6_unicast']: - raise Exception("ERROR: family_type should be 'ipv4_unicast', or 'ipv6_unicast'") - - if kwargs["url"].endswith("/v1/"): - vrf_check = "/rest/v1/system/vrfs/%s" % vrf_name - else: - # Else logic designed for v10.04 and later - vrf_check = vrf_name - - if vrf_check in vrf_list: - address_family_data = { - "address_family": family_type, - "export_route_targets": export_target, - "import_route_targets": import_targets, - "route_map": {} - } + ''' - target_url = kwargs["url"] + "system/vrfs/%s/vrf_address_families" % vrf_name - post_data = json.dumps(address_family_data, sort_keys=True, indent=4) + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) - response = kwargs["s"].post(target_url, data=post_data, verify=False) + uri = "{base_url}{class_uri}/{name}".format( + base_url=self.session.base_url, + class_uri=Vrf.base_uri, + name=self.name + ) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating '%s' Address Family on VRF '%s' failed with status code %d: %s" - % (family_type, vrf_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating '%s' Address Family on VRF '%s' succeeded" % (family_type, vrf_name)) - return True - else: - logging.warning("FAIL: Cannot add Address Family to VRF '%s' since the VRF has not been created yet" % vrf_name) - return True + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) -def delete_vrf_address_family(vrf_name, family_type="ipv4_unicast", **kwargs): - """ - Perform a DELETE call to remove a VRF address family. - Note that this functions has logic that works for both v1 and v10.04 - - :param vrf_name: Alphanumeric name of VRF - :param family_type: Alphanumeric type of the Address Family. The options are 'ipv4_unicast' and 'ipv6_unicast'. - The default value is set to 'ipv4_unicast'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vrf_list = get_all_vrfs(**kwargs) - - if family_type == "ipv4-unicast": - family_type = "ipv4_unicast" - elif family_type == "ipv6-unicast": - family_type = "ipv6_unicast" - - if family_type not in ['ipv4_unicast', 'ipv6_unicast']: - raise Exception("ERROR: family_type should be 'ipv4_unicast', or 'ipv6_unicast'") - - if kwargs["url"].endswith("/v1/"): - vrf_check = "/rest/v1/system/vrfs/%s" % vrf_name - else: - # Else logic designed for v10.04 and later - vrf_check = vrf_name - - if vrf_check in vrf_list: - target_url = kwargs["url"] + "system/vrfs/%s/vrf_address_families/%s" % (vrf_name, family_type) - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting '%s' Address Family on VRF '%s' failed with status code %d: %s" - % (family_type, vrf_name, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting '%s' Address Family on VRF '%s' succeeded" % (family_type, vrf_name)) - return True - else: - logging.info("SUCCESS: No need to delete Address Family to VRF '%s' since it does not exist" % vrf_name) - return True + logging.info( + "SUCCESS: Delete VRF table entry '%s' succeeded" % self.name) + + @classmethod + def from_response(cls, session, response_data): + ''' + Create a Vrf object given a response_data related to the Vrf object + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param response_data: The response can be either a + dictionary: + { + "test_vrf": "/rest/v10.04/system/vrfs/test_vrf" + } + or a + string: "/rest/v1/system/vrfs/test_vrf" + :return: Vrf object + + ''' + vrf_name_arr = session.api_version.get_keys( + response_data, Vrf.resource_uri_name) + vrf_name = vrf_name_arr[0] + return Vrf(session, vrf_name) + + @classmethod + def from_uri(cls, session, uri): + ''' + Create a Vrf object given a VRF URI + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return name, vrf_obj: tuple containing both the VRF's name and a Vrf + object + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)vrfs/(?P.+)') + name = index_pattern.match(uri).group('index') + # Create vlan object + vrf_obj = Vrf(session, name, uri=uri) + + return name, vrf_obj + + @classmethod + def get_facts(cls, session): + ''' + Modify this to Perform a GET call to retrieve all VRFs and their respective data + :param cls: Class reference. + :param session: pyaoscx.Session object used to represent a logical + connection to the device. + + :return facts: Dictionary containing VRF IDs as keys and VRF objects as values + + ''' + # Log + logging.info("Retrieving switch VRF facts") + + # Set VRF facts depth + vrf_depth = session.api_version.default_facts_depth + + # Build URI + uri = "{base_url}{class_uri}?depth={depth}".format( + base_url=session.base_url, + class_uri=Vrf.base_uri, + depth=vrf_depth + ) + + try: + # Try to get facts data via GET method + response = session.s.get( + uri, + verify=False, + proxies=session.proxy + ) + + except Exception as e: + raise ResponseError('GET', e) + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + # Load response text into json format + facts = json.loads(response.text) + + return facts + + def get_uri(self): + ''' + Method used to obtain the specific VRF URI + return: Object's URI + ''' + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{name}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Vrf.base_uri, + name=self.name + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def __str__(self): + return "VRF name: '{}'".format(self.name) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified + + #################################################################### + # IMPERATIVES FUNCTIONS + #################################################################### + + def add_address_family(self, family_type="ipv4_unicast", export_target=[], + import_targets=[]): + """ + Add a VRF Address Family to the current Vrf object + + :param family_type: Alphanumeric type of the Address Family. + The options are 'ipv4_unicast' and 'ipv6_unicast'. + The default value is set to 'ipv4_unicast'. + :param export_target: Optional list of export route targets. + :param import_targets: Optional list of import route targets + + :return address_family: VrfAddressFamily Object + """ + + if not self.materialized: + raise VerificationError( + 'VRF {}'.format(self.name), + 'Object not materialized') + + # Verify if incoming address is a string + if isinstance(family_type, str): + # Create Vrf_Family_Address object -- add it to it's internal + # address_families + vrf_address_family = self.session.api_version.get_module( + self.session, 'VrfAddressFamily', family_type, + parent_vrf=self, + export_route_targets=export_target, + import_route_targets=import_targets, + route_map={}) + # Try to get data, if non existent create + try: + # Try to obtain vrf_address_family address data + vrf_address_family.get() + # If vrf_address_family object is non existent, create it + except GenericOperationError: + # Create vrf_address_family inside switch + vrf_address_family.apply() + + # Apply changes inside switch + self.apply() + + return vrf_address_family + + def delete_address_family(self, family_type="ipv4_unicast"): + """ + Given a address family type, delete that address from the current + Vrf object. + + :param family_type: Alphanumeric type of the Address Family. + The options are 'ipv4_unicast' and 'ipv6_unicast'. + A VrfAddressFamily object is accepted. + The default value is set to 'ipv4_unicast'. + + """ + + if not self.materialized: + raise VerificationError( + 'VRF {}'.format(self.name), + 'Object not materialized') + + # Verify if incoming address is a object + if isinstance(family_type, VrfAddressFamily): + # Obtain address + family_type = family_type.address_family + + # Iterate through every address inside interface + for add_family_obj in self.address_families: + if add_family_obj.address_family == family_type: + # Removing address does an internal delete + self.address_families.remove(add_family_obj) + + def setup_dns(self, domain_name=None, domain_list=None, + domain_servers=None, host_v4_address_mapping=None, + host_v6_address_mapping=None): + """ + Setup DNS client configuration within a VRF. + + :param domain_name: Domain name used for name resolution by + the DNS client, if 'dns_domain_list' is not configured + :param domain_list: dict of DNS Domain list names to be used for + address resolution, keyed by the resolution priority order + Example: + { + 0: "hpe.com" + 1: "arubanetworks.com" + } + :param domain_servers: dict of DNS Name servers to be used for address + resolution, keyed by the resolution priority order + Example: + { + 0: "4.4.4.10" + 1: "4.4.4.12" + } + :param host_v4_address_mapping: dict of static host + address configurations and the IPv4 address associated with them + Example: + { + "host1": "5.5.44.5" + "host2": "2.2.44.2" + } + :param host_v6_address_mapping: dict of static host + address configurations and the IPv6 address associated with them + Example: + { + "host1": "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + } + :return modified: Returns True if modified, False + otherwise + """ + # Update Values + + if domain_name is not None: + self.dns_domain_name = domain_name + + if domain_list is not None: + self.dns_domain_list = domain_list + + if domain_servers is not None: + self.dns_name_servers = domain_servers + + if host_v4_address_mapping is not None: + self.dns_host_v4_address_mapping = host_v4_address_mapping + + if host_v6_address_mapping is not None: + self.dns_host_v6_address_mapping = host_v6_address_mapping + + return self.apply() + + def delete_dns(self, domain_name=None, domain_list=None, + domain_servers=None, host_v4_address_mapping=None, + host_v6_address_mapping=None): + """ + Delete DNS client configuration within a Vrf object. + + :param domain_name: If value is not None, it is deleted + :param domain_list: If value is not None, it is deleted + :param domain_servers: If value is not None, it is deleted + :param host_v4_address_mapping: If value is not None, it is deleted + :param host_v6_address_mapping: If value is not None, it is deleted + + :return modified: Returns True if modified, False + otherwise + """ + # Update Values + + if domain_name is not None: + self.dns_domain_name = None + + if domain_list is not None: + self.dns_domain_list = None + + if domain_servers is not None: + self.dns_name_servers = None + + if host_v4_address_mapping is not None: + self.dns_host_v4_address_mapping = None + + if host_v6_address_mapping is not None: + self.dns_host_v6_address_mapping = None + + return self.apply() diff --git a/pyaoscx/vrf_address_family.py b/pyaoscx/vrf_address_family.py new file mode 100644 index 0000000..25f46e3 --- /dev/null +++ b/pyaoscx/vrf_address_family.py @@ -0,0 +1,419 @@ +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. +# Apache License 2.0 + +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + +from pyaoscx.pyaoscx_module import PyaoscxModule + +from pyaoscx.utils.connection import connected + +import json +import logging +import re +import pyaoscx.utils.util as utils + + +class VrfAddressFamily(PyaoscxModule): + ''' + Provide configuration management for Address Family settings on AOS-CX devices. + ''' + + indices = ['address_family'] + resource_uri_name = 'vrf_address_families' + + def __init__(self, session, address_family, parent_vrf, uri=None, + **kwargs): + + self.session = session + # Assign ID + self.address_family = address_family + # Assign parent Vrf object + self.__set_vrf(parent_vrf) + self._uri = uri + # List used to determine attributes related to the VRF Address Family + # configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + def __set_vrf(self, parent_vrf): + ''' + Set parent VRF as an attribute for the VrfAddressFamily object + :param parent_vrf: a Vrf object + ''' + + # Set parent_vrf + self.__parent_vrf = parent_vrf + + # Set URI + self.base_uri = \ + '{base_vrf_uri}/{vrf_name}/vrf_address_families'.format( + base_vrf_uri=self.__parent_vrf.base_uri, + vrf_name=self.__parent_vrf.name) + + # Verify VRF Address Family doesn't exist already inside VRF + for vrf_address_family in self.__parent_vrf.address_families: + if vrf_address_family.address_family == self.address_family: + # Make list element point to current object + vrf_address_family = self + else: + # Add self to vrf_address_families list in parent_vrf + self.__parent_vrf.address_families.append(self) + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a VRF Address Family table + entry and fill the object with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch VRF Address families") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of {}".format(selectors)) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}/{address_family}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address_family=self.address_family + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if the VrfAddressFamily object is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs( + self, data, 'config_attrs', ['address_family']) + + # Set original attributes + self.__original_attributes = data + + # Remove ID + if 'address_family' in self.__original_attributes: + self.__original_attributes.pop('address_family') + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + + @classmethod + def get_all(cls, session, parent_vrf): + ''' + Perform a GET call to retrieve all system VRF Address Families inside a + VRF, and create a dictionary containing them + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent Vrf object where VRF Address Family is stored + :return: Dictionary containing VRF Address Family IDs as keys and a + VrfAddressFamily objects as values + ''' + + logging.info("Retrieving the switch VRF Address Family") + + base_uri = '{base_vrf_uri}/{vrf_name}/vrf_address_families'.format( + base_vrf_uri=parent_vrf.base_uri, + vrf_name=parent_vrf.name) + + uri = '{base_url}{class_uri}'.format( + base_url=session.base_url, + class_uri=base_uri) + + try: + response = session.s.get(uri, verify=False, proxies=session.proxy) + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + vrf_address_family_dict = {} + # Get all URI elements in the form of a list + uri_list = session.api_version.get_uri_from_data(data) + + for uri in uri_list: + # Create a VrfAddressFamily object and adds it to parent + # VRF list + address_family, vrf_address_family = VrfAddressFamily.from_uri( + session, parent_vrf, uri) + # Load all VRF Address Families data from within the Switch + vrf_address_family.get() + vrf_address_family_dict[address_family] = vrf_address_family + + return vrf_address_family_dict + + @connected + def apply(self): + ''' + Main method used to either create or update + an existing VrfAddressFamily object + Checks whether the VRF Address Family exists in the switch + Calls self.update() if VRF Address Family being updated + Calls self.create() if a new VRF Address Family is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + if not self.__parent_vrf.materialized: + self.__parent_vrf.apply() + + modified = False + + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified + + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing VRF Address Family table entry + + :return modified: True if Object was modified and a PUT request was made. + False otherwise + + ''' + + vrf_address_family_data = {} + vrf_address_family_data = utils.get_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}/{address_family}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address_family=self.address_family + ) + + # Compare dictionaries + if vrf_address_family_data == self.__original_attributes: + # Object was not modified + modified = False + + else: + post_data = json.dumps( + vrf_address_family_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Update VRF Address Family table entry {} succeeded\ + ".format( + self.address_family)) + # Set new original attributes + self.__original_attributes = vrf_address_family_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new VRF Address Family table entry + Only returns if an exception is not raise + return: True if entry was created + ''' + + vrf_address_family_data = {} + + vrf_address_family_data = utils.get_attrs(self, self.config_attrs) + vrf_address_family_data['address_family'] = self.address_family + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=self.base_uri + ) + post_data = json.dumps( + vrf_address_family_data, sort_keys=True, indent=4) + + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info + ("SUCCESS: Adding VRF Address Family table entry {} succeeded\ + ".format(self.address_family)) + + # Get all object's data + self.get() + # Object was modified + return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete VRF Address Family table entry + + ''' + + uri = "{base_url}{class_uri}/{address_family}".format( + base_url=self.session.base_url, + class_uri=self.base_uri, + address_family=self.address_family + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info( + "SUCCESS: Delete VRF Address Family table entry {} succeeded\ + ".format(self.address_family)) + + # Delete back reference from VRF + for vrf_address_family in self.__parent_vrf.address_families: + if vrf_address_family.address_family == self.address_family: + self.__parent_vrf.address_families.remove(vrf_address_family) + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + @classmethod + def from_response(cls, session, parent_vrf, response_data): + ''' + Create a VrfAddressFamily object given a response_data related to + the VRF Address Family's address_family + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a + logical connection to the device + :param parent_vrf: parent Vrf object where VrfAddressFamlily object is stored + :param response_data: The response can be either a + dictionary: { + address_family: "/rest/v10.04/system/vrfs/vrf_address_families/ + address_family" + } + or a + string: "/rest/v10.04/system/vrfs/vrf_address_families/address_family" + :return: VrfAddressFamily object + ''' + vrf_address_family_arr = session.api_version.get_keys( + response_data, VrfAddressFamily.resource_uri_name) + address_family = vrf_address_family_arr[0] + return VrfAddressFamily(session, address_family, parent_vrf) + + @classmethod + def from_uri(cls, session, parent_vrf, uri): + ''' + Create a VrfAddressFamily object given a URI and a specified parent VRF + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param parent_vrf: parent vrf class where VRF Address Family is stored + :param uri: a String with a URI + + :return index, vrf_address_family_obj: tuple containing both the VRF + Address Family object and the VRF Address Family's address_family + ''' + # Obtain ID from URI + index_pattern = re.compile(r'(.*)vrf_address_families/(?P.+)') + index = index_pattern.match(uri).group('index') + # Create VrfAddressFamily object + vrf_address_family_obj = VrfAddressFamily( + session, index, parent_vrf, uri=uri) + + return index, vrf_address_family_obj + + def __str__(self): + return "VRF Address Family ID {}".format(self.address_family) + + def get_uri(self): + ''' + Method used to obtain the specific VRF Address Family URI + return: Object's URI + ''' + + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}/{address_family}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=self.base_uri, + address_family=self.address_family + ) + + return self._uri + + def get_info_format(self): + ''' + Method used to obtain correct object format for referencing inside + other objects + return: Object format depending on the API Version + ''' + return self.session.api_version.get_index(self) + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/vsx.py b/pyaoscx/vsx.py index af88502..7f57dae 100644 --- a/pyaoscx/vsx.py +++ b/pyaoscx/vsx.py @@ -1,517 +1,349 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. +# (C) Copyright 2019-2021 Hewlett Packard Enterprise Development LP. # Apache License 2.0 -from pyaoscx import common_ops, interface, port, system +from pyaoscx.exceptions.response_error import ResponseError +from pyaoscx.exceptions.generic_op_error import GenericOperationError +from pyaoscx.exceptions.verification_error import VerificationError + + +from pyaoscx.pyaoscx_module import PyaoscxModule +from pyaoscx.vrf import Vrf +from pyaoscx.utils.connection import connected import json import logging +import pyaoscx.utils.util as utils + + +class Vsx(PyaoscxModule): + ''' + Provide configuration management for VSX protocol on AOS-CX devices. + ''' + + base_uri = 'system/vsx' + + def __init__(self, session, uri=None, **kwargs): + + self.session = session + self._uri = uri + # List used to determine attributes related to the VSX configuration + self.config_attrs = [] + self.materialized = False + # Attribute dictionary used to manage the original data + # obtained from the GET + self.__original_attributes = {} + # Set arguments needed for correct creation + utils.set_creation_attrs(self, **kwargs) + # Attribute used to know if object was changed recently + self.__modified = False + + @connected + def get(self, depth=None, selector=None): + ''' + Perform a GET call to retrieve data for a VSX table entry and fill the + class with the incoming attributes + + :param depth: Integer deciding how many levels into the API JSON that + references will be returned. + :param selector: Alphanumeric option to select specific information to + return. + :return: Returns True if there is not an exception raised + ''' + logging.info("Retrieving the switch VSX configuration") + + depth = self.session.api_version.default_depth \ + if depth is None else depth + selector = self.session.api_version.default_selector \ + if selector is None else selector + + if not self.session.api_version.valid_depth(depth): + depths = self.session.api_version.valid_depths + raise Exception("ERROR: Depth should be {}".format(depths)) + + if selector not in self.session.api_version.valid_selectors: + selectors = ' '.join(self.session.api_version.valid_selectors) + raise Exception( + "ERROR: Selector should be one of %s" % selectors) + + payload = { + "depth": depth, + "selector": selector + } + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vsx.base_uri + ) + + try: + response = self.session.s.get( + uri, verify=False, params=payload, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('GET', e) + + if not utils._response_ok(response, "GET"): + raise GenericOperationError(response.text, response.status_code) + + data = json.loads(response.text) + + # Add dictionary as attributes for the object + utils.create_attrs(self, data) + + # Determines if VSX is configurable + if selector in self.session.api_version.configurable_selectors: + # Set self.config_attrs and delete ID from it + utils.set_config_attrs(self, data, 'config_attrs') + + # Set original attributes + self.__original_attributes = data + + # If the VSX has a isl_port inside the switch and a new one is not + # being added + if hasattr(self, 'isl_port') and self.isl_port is not None: + isl_port_response = self.isl_port + interface_cls = self.session.api_version.get_module( + self.session, 'Interface', '') + # Set port as a Interface Object + self.isl_port = interface_cls.from_response( + self.session, isl_port_response) + self.isl_port.get() + # If the VSX has a keepalive_vrf inside the switch and a new one is + # not being added + if hasattr(self, 'keepalive_vrf') and self.keepalive_vrf is not None: + + # Set keepalive VRF as a Vrf object + self.keepalive_vrf = Vrf.from_response( + self.session, self.keepalive_vrf) + self.keepalive_vrf.get() + + # Sets object as materialized + # Information is loaded from the Device + self.materialized = True + return True + @classmethod + def get_all(cls, session): + pass + + @classmethod + def from_uri(cls, session, uri): + ''' + Create a Vsx object given a VSX URI + :param cls: Object's class + :param session: pyaoscx.Session object used to represent a logical + connection to the device + :param uri: a String with a URI + + :return Vsx object + ''' + # Create Vsx object + vsx_obj = Vsx(session, uri=uri) + + return vsx_obj + + @connected + def apply(self): + ''' + Main method used to either create or update an existing VSX configuration. + Checks whether the VSX configuration exists in the switch + Calls self.update() if VSX configuration being updated + Calls self.create() if a new VSX configuration is being created + + :return modified: Boolean, True if object was created or modified + False otherwise + ''' + + # Verify ISL port is materialized inside switch and has NO-routing + # status + if not self.isl_port.materialized or self.isl_port.routing: + raise VerificationError( + 'Interface', 'Object not materialized--or--routing enabled') + # Verify that VRF is materialized inside switch + if not self.keepalive_vrf.materialized: + raise VerificationError('VRF', 'Object not materialized') + + modified = False + if self.materialized: + modified = self.update() + else: + modified = self.create() + # Set internal attribute + self.__modified = modified + return modified -def get_vsx(depth=0, selector=None, **kwargs): - """ - Perform a GET call to get get the current VSX information on a system. - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. If running v10.04 or later, an additional option 'writable' is included. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: JSON of VSX information - """ - if kwargs["url"].endswith("/v1/"): - return _get_vsx_v1(depth, selector, **kwargs) - else: # Updated else for when version is v10.04 - return _get_vsx(depth, selector, **kwargs) - - -def _get_vsx_v1(depth, selector, **kwargs): - """ - Perform a GET call to get get the current VSX information on a system. - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', or 'statistics'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: JSON of VSX information - """ - if selector not in ['configuration', 'status', 'statistics', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', or 'statistics'") - - target_url = kwargs["url"] + "system/vsx" - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - result = [] - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VSX failed with status code %d: %s" - % (response.status_code, response.text)) - if response.status_code == 400: - logging.warning("FAIL: Possibly no VSX currently configured") - else: - logging.info("SUCCESS: Getting VSX information succeeded") - result = response.json() - - return result - - -def _get_vsx(depth, selector, **kwargs): - """ - Perform a GET call to get get the current VSX information on a system. - - :param depth: Integer deciding how many levels into the API JSON that references will be returned. - :param selector: Alphanumeric option to select specific information to return. The options are 'configuration', - 'status', 'statistics' or 'writable'. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: JSON of VSX information - """ - if selector not in ['configuration', 'status', 'statistics', 'writable', None]: - raise Exception("ERROR: Selector should be 'configuration', 'status', 'statistics', or 'writable'") - - target_url = kwargs["url"] + "system/vsx" - payload = { - "depth": depth, - "selector": selector - } - response = kwargs["s"].get(target_url, verify=False, params=payload, timeout=2) - - result = [] - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting VSX failed with status code %d: %s" - % (response.status_code, response.text)) - if response.status_code == 400: - logging.warning("FAIL: Possibly no VSX currently configured") - else: - logging.info("SUCCESS: Getting VSX information succeeded") - result = response.json() - - return result - - -def create_vsx(role, isl_port, keepalive_peer, keepalive_src, keepalive_vrf, vsx_mac, **kwargs): - """ - Perform a POST call to create VSX commands. - - :param role: Alphanumeric role that the system will be in the VSX pair. The options are "primary" or "secondary" - :param isl_port: Alphanumeric name of the interface that will function as the inter-switch link - :param keepalive_peer: Alphanumeric IP address of the VSX Peer that will be reached as the keepalive connection. - :param keepalive_src: Alphanumeric IP address on the switch that will function as the keepalive connection source. - :param keepalive_vrf: Alphanumeric name of the VRF that the keepalive connection will reside on. - :param vsx_mac: Alphanumeric MAC address that will function as the VSX System MAC. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if role not in ['primary', 'secondary']: - raise Exception("ERROR: VSX role should be 'primary' or 'secondary'") - - if kwargs["url"].endswith("/v1/"): - return _create_vsx_v1(role, isl_port, keepalive_peer, keepalive_src, keepalive_vrf, vsx_mac, **kwargs) - else: # Updated else for when version is v10.04 - return _create_vsx(role, isl_port, keepalive_peer, keepalive_src, keepalive_vrf, vsx_mac, **kwargs) - - -def _create_vsx_v1(role, isl_port, keepalive_peer, keepalive_src, keepalive_vrf, vsx_mac, - keepalive_port=7678, **kwargs): - """ - Perform a POST call to create VSX commands. - - :param role: Alphanumeric role that the system will be in the VSX pair. The options are "primary" or "secondary" - :param isl_port: Alphanumeric name of the interface that will function as the inter-switch link - :param keepalive_peer: Alphanumeric IP address of the VSX Peer that will be reached as the keepalive connection. - :param keepalive_src: Alphanumeric IP address on the switch that will function as the keepalive connection source. - :param keepalive_vrf: Alphanumeric name of the VRF that the keepalive connection will reside on. - :param vsx_mac: Alphanumeric MAC address that will function as the VSX System MAC. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - current_vsx = system.get_system_info(**kwargs) - - if 'vsx' in current_vsx: - logging.warning("FAIL: Creating VSX Role '%s' on vrf %s. There is already an existing VSX setup." - % (role, keepalive_vrf)) - return False - else: - if role not in ['primary', 'secondary']: - raise Exception("ERROR: VSX role should be 'primary' or 'secondary'") - - ip_src_subnet = keepalive_src.find('/') - ip_peer_subnet = keepalive_peer.find('/') - if ip_src_subnet >= 0: - keepalive_src = keepalive_src[0:ip_src_subnet] - if ip_peer_subnet >= 0: - keepalive_peer = keepalive_peer[0:ip_peer_subnet] - - vsx_data = { - "config_sync_disable": False, - "config_sync_features": [], - "device_role": role, - "isl_port": "/rest/v1/system/ports/" + isl_port, - "isl_timers": { - "hello_interval": 1, - "hold_time": 0, - "peer_detect_interval": 300, - "timeout": 20 - }, - "keepalive_peer_ip": keepalive_peer, - "keepalive_src_ip": keepalive_src, - "keepalive_timers": { - "dead_interval": 3, - "hello_interval": 1 - }, - "keepalive_udp_port": keepalive_port, - "keepalive_vrf": "/rest/v1/system/vrfs/" + keepalive_vrf, - "linkup_delay_timer": 180, - "split_recovery_disable": False, - "system_mac": vsx_mac - } - - target_url = kwargs["url"] + "system/vsx" - post_data = json.dumps(vsx_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) + @connected + def update(self): + ''' + Perform a PUT call to apply changes to an existing VSX inside the switch + + :return modified: True if Object was modified and a PUT request + was made. False otherwise + + ''' + + vsx_data = {} + vsx_data = utils.get_attrs(self, self.config_attrs) + + # Get VRF uri + vsx_data["keepalive_vrf"] = self.keepalive_vrf.get_info_format() + # Get ISL port uri + vsx_data["isl_port"] = self.isl_port.get_info_format() + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vsx.base_uri + ) + + # Compare dictionaries + if vsx_data == self.__original_attributes: + # Object was not modified + modified = False - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating VSX Role '%s' on vrf %s failed with status code %d: %s" - % (role, keepalive_vrf, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Creating VSX Role '%s' succeeded on vrf %s" % (role, keepalive_vrf)) - return True - - -def _create_vsx(role, isl_port, keepalive_peer, keepalive_src, keepalive_vrf, vsx_mac, keepalive_port=7678, **kwargs): - """ - Perform a POST call to create VSX commands. - - :param role: Alphanumeric role that the system will be in the VSX pair. The options are "primary" or "secondary" - :param isl_port: Alphanumeric name of the interface that will function as the inter-switch link - :param keepalive_peer: Alphanumeric IP address of the VSX Peer that will be reached as the keepalive connection. - :param keepalive_src: Alphanumeric IP address on the switch that will function as the keepalive connection source. - :param keepalive_vrf: Alphanumeric name of the VRF that the keepalive connection will reside on. - :param vsx_mac: Alphanumeric MAC address that will function as the VSX System MAC. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - system_info_dict = system.get_system_info(**kwargs) - - if system_info_dict['vsx'] is not None: - logging.warning("FAIL: Creating VSX Role '%s' on vrf %s. There is already an existing VSX setup." - % (role, keepalive_vrf)) - return False - else: - if role not in ['primary', 'secondary']: - raise Exception("ERROR: VSX role should be 'primary' or 'secondary'") - - # Checks if the ISL Port is a physical interface or a Lag. If an interface, replace the slashes - if isl_port[0].isdigit(): - isl_port = common_ops._replace_special_characters(isl_port) - - isl_port_uri = "/rest/v10.04/system/interfaces/" + isl_port - - ip_src_subnet = keepalive_src.find('/') - ip_peer_subnet = keepalive_peer.find('/') - if ip_src_subnet >= 0: - keepalive_src = keepalive_src[0:ip_src_subnet] - if ip_peer_subnet >= 0: - keepalive_peer = keepalive_peer[0:ip_peer_subnet] - - vsx_data = { - "config_sync_disable": False, - "config_sync_features": [], - "device_role": role, - "isl_port": isl_port_uri, - "isl_timers": { - "hello_interval": 1, - "hold_time": 0, - "peer_detect_interval": 300, - "timeout": 20 - }, - "keepalive_peer_ip": keepalive_peer, - "keepalive_src_ip": keepalive_src, - "keepalive_timers": { - "dead_interval": 3, - "hello_interval": 1 - }, - "keepalive_udp_port": keepalive_port, - "keepalive_vrf": { - keepalive_vrf: "/rest/v10.04/system/vrfs/" + keepalive_vrf, - }, - "linkup_delay_timer": 180, - "split_recovery_disable": False, - "system_mac": vsx_mac - } - - target_url = kwargs["url"] + "system/vsx" + post_data = json.dumps(vsx_data, sort_keys=True, indent=4) + + try: + response = self.session.s.put( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('PUT', e) + + if not utils._response_ok(response, "PUT"): + raise GenericOperationError( + response.text, response.status_code) + + else: + logging.info("SUCCESS: Adding VSX configuration") + # Set new original attributes + self.__original_attributes = vsx_data + # Object was modified + modified = True + return modified + + @connected + def create(self): + ''' + Perform a POST call to create a new VSX + Only returns if an exception is not raise + + return: True if entry was created + ''' + + vsx_data = {} + vsx_data = utils.get_attrs(self, self.config_attrs) + + # Verify Keepalive is created + if hasattr(self, 'keepalive_vrf'): + if not self.keepalive_vrf.materialized: + raise VerificationError( + 'Keepalive Vrf', 'Object not materialized') + + # Get VRF uri + vsx_data["keepalive_vrf"] = self.keepalive_vrf.get_info_format() + + if hasattr(self, 'isl_port'): + if not self.isl_port.materialized: + raise VerificationError('Isl Port ', 'Object not materialized') + + # Get ISL port uri + vsx_data["isl_port"] = self.isl_port.get_info_format() + + if hasattr(self, 'keepalive_peer') and \ + hasattr(self, 'keepalive_src') and \ + self.keepalive_src is not None and \ + self.keepalive_src is not None: + + ip_src_subnet = self.keepalive_src.find('/') + ip_peer_subnet = self.keepalive_peer.find('/') + + if ip_src_subnet >= 0: + self.keepalive_src = self.keepalive_src[0:ip_src_subnet] + + if ip_peer_subnet >= 0: + self.keepalive_peer = self.keepalive_peer[0:ip_peer_subnet] + + vsx_data["keepalive_peer_ip"] = self.keepalive_peer + vsx_data["keepalive_src_ip"] = self.keepalive_src + + if hasattr(self, 'system_mac') and self.system_mac is not None: + vsx_data["system_mac"] = self.system_mac + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vsx.base_uri + ) + post_data = json.dumps(vsx_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) + try: + response = self.session.s.post( + uri, verify=False, data=post_data, proxies=self.session.proxy) + + except Exception as e: + raise ResponseError('POST', e) + + if not utils._response_ok(response, "POST"): + raise GenericOperationError(response.text, response.status_code) - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating VSX Role '%s' on vrf %s failed with status code %d: %s" - % (role, keepalive_vrf, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating VSX Role '%s' succeeded on vrf %s" % (role, keepalive_vrf)) - return True - - -def update_vsx_interface_vlan(vlan_id, active_forwarding, vsx_sync, act_gw_mac, act_gw_ip, **kwargs): - """ - Perform PUT calls on a VLAN interface to configure VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param active_forwarding: True or False Boolean to set VSX active forwarding - :param vsx_sync: Set of alphanumeric values to enable VSX configuration synchronization. The options are - any combination of 'active-gateways', 'irdp', and 'policies'. VSX Sync is mainly used in the Primary. - :param act_gw_mac: Alphanumeric value of the Virtual MAC address for the interface active gateway - :param act_gw_ip: Alphanumeric value of the Virtual IP address for the interface active gateway - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_vsx_interface_vlan_v1(vlan_id, active_forwarding, vsx_sync, act_gw_mac, act_gw_ip, **kwargs) - else: # Updated else for when version is v10.04 - return _update_vsx_interface_vlan(vlan_id, active_forwarding, vsx_sync, act_gw_mac, act_gw_ip, **kwargs) - - -def _update_vsx_interface_vlan_v1(vlan_id, active_forwarding, vsx_sync, act_gw_mac, act_gw_ip, **kwargs): - """ - Perform PUT calls on a VLAN interface to configure VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param active_forwarding: True or False Boolean to set VSX active forwarding - :param vsx_sync: Set of alphanumeric values to enable VSX configuration synchronization. The options are - any combination of 'active-gateways', 'irdp', and 'policies' - :param act_gw_mac: Alphanumeric value of the Virtual MAC address for the interface active gateway - :param act_gw_ip: Alphanumeric value of the Virtual IP address for the interface active gateway - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - - ports_list = port.get_all_ports(**kwargs) - vlan_name = "vlan" + str(vlan_id) - - if "/rest/v1/system/ports/%s" % vlan_name not in ports_list: - logging.warning("FAIL: Adding VSX information to VLAN Interface '%d' failed because VLAN " - "Interface doesn't exist" % vlan_id) - return False - else: - port_data = port.get_port(vlan_name, depth=0, selector="configuration", **kwargs) - - vsx_sync_set = [] - if vsx_sync == None: - vsx_sync = {} - if "active-gateways" in vsx_sync: - vsx_sync_set.append("^vsx_virtual.*") - if "irdp" in vsx_sync: - vsx_sync_set.append(".irdp.*") - if "policies" in vsx_sync: - vsx_sync_set.append("^policy.*") - - port_data["vsx_active_forwarding_enable"] = active_forwarding - port_data["vsx_sync"] = vsx_sync_set - port_data["vsx_virtual_gw_mac_v4"] = act_gw_mac - port_data["vsx_virtual_ip4"] = [act_gw_ip] - - port_data.pop('name', None) # must remove this item from the json since name can't be modified - port_data.pop('origin', None) # must remove this item from the json since origin can't be modified - - target_url = kwargs["url"] + "system/ports/%s" % vlan_name - put_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding VSX information to VLAN Interface '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VSX information to VLAN Interface '%d' succeeded" % vlan_id) - return True - - -def _update_vsx_interface_vlan(vlan_id, active_forwarding, vsx_sync, act_gw_mac, act_gw_ip, **kwargs): - """ - Perform PUT calls on a VLAN interface to configure VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param active_forwarding: True or False Boolean to set VSX active forwarding - :param vsx_sync: Set of alphanumeric values to enable VSX configuration synchronization. The options are - any combination of 'active-gateways', 'irdp', and 'policies' - :param act_gw_mac: Alphanumeric value of the Virtual MAC address for the interface active gateway - :param act_gw_ip: Alphanumeric value of the Virtual IP address for the interface active gateway - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_list = interface.get_all_interfaces(**kwargs) - vlan_name = "vlan" + str(vlan_id) - - if vlan_name not in ints_list: - logging.warning("FAIL: Adding VSX information to VLAN Interface '%d' failed because " - "VLAN Interface doesn't exist" % vlan_id) - return False - else: - interface_vsx_data = interface.get_interface(vlan_name, depth=1, selector="writable", **kwargs) - - vsx_sync_set = [] - if "active-gateways" in vsx_sync: - vsx_sync_set.append("^vsx_virtual.*") - if "irdp" in vsx_sync: - vsx_sync_set.append(".irdp.*") - if "policies" in vsx_sync: - vsx_sync_set.append("^policy.*") - - if interface_vsx_data['vrf']: - # Convert the dictionary to a URI string - interface_vsx_data['vrf'] = list(interface_vsx_data['vrf'].values())[0] - - interface_vsx_data["vsx_active_forwarding_enable"] = active_forwarding - interface_vsx_data["vsx_sync"] = vsx_sync_set - interface_vsx_data["vsx_virtual_gw_mac_v4"] = act_gw_mac - interface_vsx_data["vsx_virtual_ip4"] = [act_gw_ip] - - target_url = kwargs["url"] + "system/interfaces/" + vlan_name - put_data = json.dumps(interface_vsx_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Adding VSX information to VLAN Interface '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Adding VSX information to VLAN Interface '%d' succeeded" % vlan_id) - return True - - -def delete_vsx_interface_vlan(vlan_id, **kwargs): - """ - Perform PUT calls on a VLAN interface to remove VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_vsx_interface_vlan_v1(vlan_id, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_vsx_interface_vlan(vlan_id, **kwargs) - - -def _delete_vsx_interface_vlan_v1(vlan_id, **kwargs): - """ - Perform PUT calls on a VLAN interface to remove VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ports_list = port.get_all_ports(**kwargs) - vlan_name = "vlan" + str(vlan_id) - - if "/rest/v1/system/ports/%s" % vlan_name not in ports_list: - logging.warning("FAIL: Deleting VSX information from VLAN Interface '%d' failed " - "because VLAN Interface doesn't exist" % vlan_id) - return False - else: - - port_data = port.get_port(vlan_name, depth=0, selector="configuration", **kwargs) - - port_data["vsx_active_forwarding_enable"] = False - port_data["vsx_sync"] = [] - port_data["vsx_virtual_ip4"] = [] - port_data.pop('vsx_virtual_gw_mac_v4', None) - - port_data.pop('name', None) # must remove this item from the json since name can't be modified - port_data.pop('origin', None) # must remove this item from the json since origin can't be modified - - target_url = kwargs["url"] + "system/ports/%s" % vlan_name - put_data = json.dumps(port_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Deleting VSX information from VLAN Interface '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VSX information from VLAN Interface '%d' succeeded" % vlan_id) - return True - - -def _delete_vsx_interface_vlan(vlan_id, **kwargs): - """ - Perform PUT calls on a VLAN interface to remove VSX IPv4 settings. - - :param vlan_id: Numeric ID of VLAN to that will be configured - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - ints_list = interface.get_all_interfaces(**kwargs) - vlan_name = "vlan" + str(vlan_id) - - if vlan_name not in ints_list: - logging.warning("FAIL: Deleting VSX information to VLAN Interface '%d' failed because " - "VLAN Interface doesn't exist" % vlan_id) - return False - else: - interface_vsx_data = interface.get_interface(vlan_name, depth=2, selector="writable", **kwargs) - - interface_vsx_data["vsx_active_forwarding_enable"] = None - interface_vsx_data["vsx_sync"] = None - interface_vsx_data["vsx_virtual_gw_mac_v4"] = None - interface_vsx_data["vsx_virtual_ip4"] = [] - - target_url = kwargs["url"] + "system/interfaces/" + vlan_name - put_data = json.dumps(interface_vsx_data, sort_keys=True, indent=4) - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Deleting VSX information from VLAN Interface '%d' failed with status code %d: %s" - % (vlan_id, response.status_code, response.text)) - return False else: - logging.info("SUCCESS: Deleting VSX information from VLAN Interface '%d' succeeded" % vlan_id) - return True - - -def delete_vsx(**kwargs): - """ - Perform a DELETE call to get get the current VSX information on a system. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - target_url = kwargs["url"] + "system/vsx" - response = kwargs["s"].delete(target_url, verify=False, timeout=2) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VSX instance failed with status code %d: %s" - % (response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VSX succeeded") + logging.info("SUCCESS: Adding VSX table entry succeeded") + + # Get all objects data + self.get() + # Object was modified return True + + @connected + def delete(self): + ''' + Perform DELETE call to delete VSX configuration. + + ''' + + # Delete object attributes + utils.delete_attrs(self, self.config_attrs) + + uri = "{base_url}{class_uri}".format( + base_url=self.session.base_url, + class_uri=Vsx.base_uri + ) + + try: + response = self.session.s.delete( + uri, verify=False, proxies=self.session.proxy) + except Exception as e: + raise ResponseError('DELETE', e) + + if not utils._response_ok(response, "DELETE"): + raise GenericOperationError(response.text, response.status_code) + + else: + logging.info("SUCCESS: Delete VSX configuration succeeded") + + def get_uri(self): + ''' + Method used to obtain the specific VSX URI + return: Object's URI + ''' + if self._uri is None: + self._uri = '{resource_prefix}{class_uri}'.format( + resource_prefix=self.session.resource_prefix, + class_uri=Vsx.base_uri + ) + return self._uri + + def get_info_format(self): + ''' + Not applicable for VSX + ''' + pass + + def was_modified(self): + """ + Getter method for the __modified attribute + :return: Boolean True if the object was recently modified, False otherwise. + """ + + return self.__modified diff --git a/pyaoscx/vxlan.py b/pyaoscx/vxlan.py deleted file mode 100644 index 057f5dc..0000000 --- a/pyaoscx/vxlan.py +++ /dev/null @@ -1,220 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops, port, interface - -import json -import logging - - -def create_vxlan_interface(port_name, source_ipv4=None, port_desc=None, dest_udp_port=4789, **kwargs): - """ - Perform POST calls to create a VXLAN table entry for a logical L3 Interface. If the - VXLAN Interface already exists and an IPv4 address is given, the function will update the IPv4 address. - - :param port_name: Alphanumeric Interface name - :param source_ipv4: Source IPv4 address to assign to the VXLAN interface. Defaults to nothing if not specified. - :param port_desc: Optional description for the interface. Defaults to nothing if not specified. - :param dest_udp_port: Optional Destination UDP Port that the VXLAN will use. Default is set to 4789 - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return port._create_vxlan_port(port_name, source_ipv4, port_desc, dest_udp_port, **kwargs) - else: # Updated else for when version is v10.04 - return interface._create_vxlan_interface(port_name, source_ipv4, port_desc, dest_udp_port, **kwargs) - - -def get_vni_list(**kwargs): - """ - Perform a GET call to receive a list of Virtual Network IDs on the system. - - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List of Virtual Network IDs - """ - target_url = kwargs["url"] + "system/virtual_network_ids" - - response = kwargs["s"].get(target_url, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list of all Virtual Network IDs failed with status code %d: %s" - % (response.status_code, response.text)) - vni_list = [] - else: - logging.info("SUCCESS: Getting list of all Virtual Network IDs succeeded") - vni_list = response.json() - - return vni_list - - -def add_vni_mapping(vni, vxlan, vlan, **kwargs): - """ - Perform POST call to create a Virtual Network ID and Map it to VLANs for a supplied VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param vxlan: Alphanumeric of the VXLAN that the VNI will be associated with - :param vlan: VLAN that the VNI will be mapped to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _add_vni_mapping_v1(vni, vxlan, vlan, **kwargs) - else: # Updated else for when version is v10.04 - return _add_vni_mapping(vni, vxlan, vlan, **kwargs) - - -def _add_vni_mapping_v1(vni, vxlan, vlan, **kwargs): - """ - Perform POST call to create a Virtual Network ID and Map it to VLANs for a supplied VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param vxlan: Alphanumeric of the VXLAN that the VNI will be associated with - :param vlan: VLAN that the VNI will be mapped to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - current_vni = get_vni_list(**kwargs) - - if "/rest/v1/system/virtual_network_ids/vxlan_vni/%d" % vni not in current_vni: - vni_data = { - "id": vni, - "interface": "/rest/v1/system/interfaces/%s" % vxlan, - "type": "vxlan_vni", - "vlan": "/rest/v1/system/vlans/%d" % vlan - } - - target_url = kwargs["url"] + "system/virtual_network_ids" - - post_data = json.dumps(vni_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating VNI '%s' for VXLAN '%s' failed with status code %d: %s" - % (vni, vxlan, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating VNI '%s' for VXLAN '%s' succeeded" % (vni, vxlan)) - return True - else: - logging.info("SUCCESS: No need to create VNI '%s' for VXLAN '%s' as it already exists" % (vni, vxlan)) - return True - - -def _add_vni_mapping(vni, vxlan, vlan, **kwargs): - """ - Perform POST call to create a Virtual Network ID and Map it to VLANs for a supplied VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param vxlan: Alphanumeric of the VXLAN that the VNI will be associated with - :param vlan: VLAN that the VNI will be mapped to - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vni_list = get_vni_list(**kwargs) - - if "vxlan_vni,%d" % vni not in vni_list: - vni_data = { - "id": vni, - "interface": "/rest/v10.04/system/interfaces/%s" % vxlan, - "type": "vxlan_vni", - "vlan": "/rest/v10.04/system/vlans/%d" % vlan - } - - target_url = kwargs["url"] + "system/virtual_network_ids" - - post_data = json.dumps(vni_data, sort_keys=True, indent=4) - response = kwargs["s"].post(target_url, data=post_data, verify=False, timeout=2) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating VNI '%s' for VXLAN '%s' failed with status code %d: %s" - % (vni, vxlan, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating VNI '%s' for VXLAN '%s' succeeded" % (vni, vxlan)) - return True - else: - logging.info("SUCCESS: No need to create VNI '%s' for VXLAN '%s' as it already exists" % (vni, vxlan)) - return True - - -def delete_vni_mapping(vni, **kwargs): - """ - Perform DELETE call to remove a Virtual Network ID for a VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _delete_vni_mapping_v1(vni, **kwargs) - else: # Updated else for when version is v10.04 - return _delete_vni_mapping(vni, **kwargs) - - -def _delete_vni_mapping_v1(vni, **kwargs): - """ - Perform DELETE call to remove a Virtual Network ID for a VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vni_list = get_vni_list(**kwargs) - - if "/rest/v1/system/virtual_network_ids/vxlan_vni/%d" % vni in vni_list: - - target_url = kwargs["url"] + "system/virtual_network_ids/vxlan_vni/%d" % vni - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VNI '%s' failed with status code %d: %s" - % (vni, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VNI '%s' succeeded" % vni) - return True - else: - logging.info("SUCCESS: No need to delete VNI '%s' since it doesn't exist" % vni) - return True - - -def _delete_vni_mapping(vni, **kwargs): - """ - Perform DELETE call to remove a Virtual Network ID for a VXLAN. - - :param vni: Integer representing the Virtual Network ID - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: True if successful, False otherwise - """ - vni_list = get_vni_list(**kwargs) - - if "vxlan_vni,%d" % vni in vni_list: - target_url = kwargs["url"] + "system/virtual_network_ids/vxlan_vni,%d" % vni - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting VNI '%s' failed with status code %d: %s" - % (vni, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting VNI '%s' succeeded" % vni) - return True - else: - logging.info("SUCCESS: No need to delete VNI '%s' since it doesn't exist" % vni) - return True diff --git a/setup.py b/setup.py index fceae27..f402ddf 100644 --- a/setup.py +++ b/setup.py @@ -7,18 +7,18 @@ long_description = f.read() setup(name='pyaoscx', - version='0.2.2', + version='2.0.0', description='AOS-CX Python Modules', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/aruba/pyaoscx', - author='Aruba Switching Automation', - author_email='aruba-switching-automation@hpe.com', + author='Aruba Automation', + author_email='aruba-automation@hpe.com', license='Apache 2.0', python_requires='>=3', classifiers=[ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: System Administrators', 'Topic :: System :: Networking', diff --git a/workflows/cleanup_l2_l3_vlans.py b/workflows/cleanup_l2_l3_vlans.py deleted file mode 100644 index b0c788a..0000000 --- a/workflows/cleanup_l2_l3_vlans.py +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python3 - -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -""" -This workflow performs the following steps: -1. Delete DHCP helpers from SVI - Ex: - interface vlan999 - no ip helper-address 1.1.1.1 - no ip helper-address 2.2.2.2 - -2. Delete SVI - Ex: - no interface vlan 999 - -3. Delete VLAN - Ex: - no vlan 999 - -4. Initialize L2 interface - Ex: - interface 1/1/20 - no shutdown - no routing - vlan access 1 - -Preconditions: -Must have run the configure_l2_l3_vlans workflow or have the equivalent settings. -""" - -from requests.packages.urllib3.exceptions import InsecureRequestWarning -import requests -import os -import sys -import logging -import getpass - -logging.basicConfig(level=logging.INFO) -dirpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) -sys.path.append(dirpath) -sys.path.append(os.path.join(dirpath, "pyaoscx")) -sys.path.append(os.path.join(dirpath, "cx_utils")) - -from pyaoscx import session -from pyaoscx import dhcp -from pyaoscx import interface -from pyaoscx import vlan - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - - -def main(): - switchip = input("Switch IP Address: ") - - username = input("Switch login username: ") - - password = getpass.getpass("Switch login password: ") - - bypassproxy = False # Set to 'True' to bypass proxy and communicate directly with device. - - if bypassproxy: - os.environ['no_proxy'] = switchip - os.environ['NO_PROXY'] = switchip - - version = 'v1' # Set to 'v10.04' if running code v10.04 or later, otherwise set to 'v1' - - base_url = "https://{0}/rest/{1}/".format(switchip, version) - try: - session_dict = dict(s=session.login(base_url, username, password), url=base_url) - - # Delete all DHCP relays for interface - dhcp.delete_dhcp_relays('vlan999', "default", **session_dict) - - # Delete VLAN and SVI - vlan.delete_vlan_and_svi(999, 'vlan999', **session_dict) - - # Initialize L2 interface - interface.initialize_interface('1/1/20', **session_dict) - - except Exception as error: - print('Ran into exception: {}. Logging out..'.format(error)) - session.logout(**session_dict) - - -if __name__ == '__main__': - main() diff --git a/workflows/configure_l2_l3_vlans.py b/workflows/configure_l2_l3_vlans.py deleted file mode 100644 index 1f8227b..0000000 --- a/workflows/configure_l2_l3_vlans.py +++ /dev/null @@ -1,109 +0,0 @@ -#!/usr/bin/env python3 - -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -""" -This workflow performs the following steps: -1. Create VLAN - Ex: - vlan 999 - description For LAB 999 - -2. Create SVI - Ex: - interface vlan999 - description ### SVI for LAB999 ### - ip address 10.10.10.99/24 - -3. Add DHCP helpers for SVI - Ex: - interface vlan999 - description ### SVI for LAB999 ### - ip address 10.10.10.99/24 - ip helper-address 1.1.1.1 - ip helper-address 2.2.2.2 - -3. Create L2 interface - a. Create the interface - b. Enable the interface - c. Set VLAN mode to 'access' - d. Set VLAN as untagged VLAN - Ex: - interface 1/1/20 - no shutdown - no routing - vlan access 999 - - - -Preconditions: -None -""" - -from requests.packages.urllib3.exceptions import InsecureRequestWarning -import requests -import os -import sys -import logging -import getpass - -logging.basicConfig(level=logging.INFO) -dirpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) -sys.path.append(dirpath) -sys.path.append(os.path.join(dirpath, "pyaoscx")) -sys.path.append(os.path.join(dirpath, "cx_utils")) - -from pyaoscx import session -from pyaoscx import vlan -from pyaoscx import interface -from pyaoscx import dhcp - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - - -def main(): - switchip = input("Switch IP Address: ") - - username = input("Switch login username: ") - - password = getpass.getpass("Switch login password: ") - - bypassproxy = False # Set to 'True' to bypass proxy and communicate directly with device. - - if bypassproxy: - os.environ['no_proxy'] = switchip - os.environ['NO_PROXY'] = switchip - - version = 'v1' # Set to 'v10.04' if running code v10.04 or later, otherwise set to 'v1' - - base_url = "https://{0}/rest/{1}/".format(switchip, version) - try: - session_dict = dict(s=session.login(base_url, username, password), url=base_url) - - vlan.create_vlan_and_svi(999, 'VLAN999', 'vlan999', 'vlan999', - 'For LAB 999', '10.10.10.99/24', vlan_port_desc='### SVI for LAB999 ###', - **session_dict) - - # Add DHCP helper IPv4 addresses for SVI - dhcp.add_dhcp_relays('vlan999', "default", ['1.1.1.1', '2.2.2.2'], **session_dict) - - # Add a new entry to the Port table if it doesn't yet exist - interface.add_l2_interface('1/1/20', **session_dict) - - # Update the Interface table entry with "user-config": {"admin": "up"} - interface.enable_disable_interface('1/1/20', **session_dict) - - # Set the L2 port VLAN mode as 'access' - vlan.port_set_vlan_mode('1/1/20', "access", **session_dict) - - # Set the access VLAN on the port - vlan.port_set_untagged_vlan('1/1/20', 999, **session_dict) - - except Exception as error: - print('Ran into exception: {}. Logging out..'.format(error)) - session.logout(**session_dict) - - -if __name__ == '__main__': - main() diff --git a/workflows/print_system_info.py b/workflows/print_system_info.py deleted file mode 100644 index b2e8e6a..0000000 --- a/workflows/print_system_info.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -""" -This workflow performs the following steps: -1. Print the system information - -Preconditions: -None -""" - -from requests.packages.urllib3.exceptions import InsecureRequestWarning -import requests -import os -import sys -import logging -import getpass - -logging.basicConfig(level=logging.INFO) -dirpath = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)) -sys.path.append(dirpath) -sys.path.append(os.path.join(dirpath, "pyaoscx")) -sys.path.append(os.path.join(dirpath, "cx_utils")) - -from pyaoscx import session -from pyaoscx import system - -import pprint - -requests.packages.urllib3.disable_warnings(InsecureRequestWarning) - - -def main(): - switchip = input("Switch IP Address: ") - - username = input("Switch login username: ") - - password = getpass.getpass("Switch login password: ") - - bypassproxy = False # Set to 'True' to bypass proxy and communicate directly with device. - - if bypassproxy: - os.environ['no_proxy'] = switchip - os.environ['NO_PROXY'] = switchip - - version = 'v1' # Set to 'v10.04' if running code v10.04 or later, otherwise set to 'v1' - - base_url = "https://{0}/rest/{1}/".format(switchip, version) - try: - session_dict = dict(s=session.login(base_url, username, password), url=base_url) - - system_info_dict = system.get_system_info(params={"selector": "configuration"}, **session_dict) - - pprint.pprint(system_info_dict) - - except Exception as error: - print('Ran into exception: {}. Logging out..'.format(error)) - session.logout(**session_dict) - - -if __name__ == '__main__': - main() diff --git a/workflows/workflow.py b/workflows/workflow.py new file mode 100644 index 0000000..67e1792 --- /dev/null +++ b/workflows/workflow.py @@ -0,0 +1,116 @@ +from pyaoscx.session import Session +from pyaoscx.pyaoscx_factory import PyaoscxFactory +from pyaoscx.vlan import Vlan +from pyaoscx.interface import Interface + +# There are two approaches to workflows, both using the session. +version = '10.04' +switch_ip = '172.25.0.2' +s = Session(switch_ip, version) +s.open('admin', 'admin') + +# Try block is used so that session closes even on error. +try: + # APPROACH 1: OPEN GRANULATED APPROACH + # VLAN + # Create Vlan object -- Not yet materialized + vlan100 = Vlan(s, 100, name="VLAN 100", voice=True) + + # Since object is not materialized, performs + # a POST request -- This method internally + # makes a GET request right after the POST + # Obtaining all attributes VLAN related + vlan100.apply() + + # Now let's create another object, that we know already exists + # inside of the Switch + vlan1 = Vlan(s, 1) + # Perform a GET request to obtain all data and materialize object + vlan1.get() + # Now, we are able to modify the objects internal attributes + vlan1.voice = True + # Apply changes + changed = vlan1.apply() + # If changed is True, a PUT request was done and object was modified + + vlan100.description = "New description, changed via pyaoscx SDK" + vlan100.apply() + # Now vlan100 contains the description attribute + print("VLAN 100 description {}".format(vlan100.description)) + + # =========================================================== + # =========================================================== + # =========================================================== + + # APPROACH 2: IMPERATIVE FACTORY APPROACH + # VLAN + # Create Factory object, passing the Session Object + factory = PyaoscxFactory(s) + + # Create Vlan object + # If vlan is non-existent, Factory instantly creates it + # inside the switch device + vlan200 = factory.vlan(200, "NAME200") + + # Now the granulated approach could still be used. + # Or an imperative method too + + # =========================================================== + # =========================================================== + # =========================================================== + + # More complex example using the OPEN GRANULATED APPROACH + # Create an Interface object + + lag = Interface(s, 'lag1') + lag.apply() + + # Create a Vlan object + + vlan_1 = Vlan(s, 1) + # In this case, now that the VLAN exists within the Switch, + # a GET request is called to obtain the VLAN's information. + # The information is then added to the object as attributes. + vlan_1.get() + # Interfaces/Ports added to LAG + port_1_1_11 = Interface(s, '1/1/11') + port_1_1_11.get() + # Make changes to configure LAG as L2 + lag.admin = 'down' + lag.routing = False + lag.vlan_trunks = [vlan_1] + lag.lacp = "passive" + lag.other_config["mclag_enabled"] = False + lag.other_config["lacp-fallback"] = False + lag.vlan_mode = "native-untagged" + lag.vlan_tag = vlan_1 + # Add port as LAG member + lag.interfaces.append(port_1_1_11) + + # Apply changes + lag.apply() + + # =========================================================== + # =========================================================== + # =========================================================== + + # Same complex example using the IMPERATIVE FACTORY APPROACH + # PLUS USING IMPERATIVE METHODS + + # Create the Interface object + lag2 = factory.interface('lag2') + modified = lag2.configure_l2( + description="Created using imperative method", + admin='up', + vlan_mode="native-untagged", + vlan_tag=1, + trunk_allowed_all=True, + native_vlan_tag=True) + # If modified is True, a PUT request was done and object was modified + +except Exception as error: + print('Ran into exception: {}. Closing session.'.format(error)) + +finally: + # At the end, the session MUST be closed + s.close() From 990749035a1bd8d8c07576685689c7ab1d90889e Mon Sep 17 00:00:00 2001 From: Alvin Castro Date: Fri, 4 Jun 2021 08:52:28 -0700 Subject: [PATCH 2/2] Major update for v2. Restructure of libraries to use objects and factories. Several files migrated to a different naming schema or removed until supported in v2. Solved merge conflicts and removed nae.py until supported. See RELEASE-NOTES.md for more information. --- pyaoscx/nae.py | 616 ------------------------------------------------- 1 file changed, 616 deletions(-) delete mode 100644 pyaoscx/nae.py diff --git a/pyaoscx/nae.py b/pyaoscx/nae.py deleted file mode 100644 index a174987..0000000 --- a/pyaoscx/nae.py +++ /dev/null @@ -1,616 +0,0 @@ -# (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP. -# Apache License 2.0 - -from pyaoscx import common_ops - -import json -import logging -import base64 -import os - - -def get_all_nae_scripts(params={}, **kwargs): - """ - Perform a GET call to get a list or dictionary of all the Network Analytics Engine scripts on the device. - - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List (v1) or Dictionary (v10.04 or empty) containing all of the NAE scripts on the device - """ - target_url = kwargs["url"] + "system/nae_scripts" - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting dictionary of all NAE scripts on device failed with status code %d: %s" - % (response.status_code, response.text)) - all_nae_scripts = {} - else: - if kwargs["url"].endswith("/v1/"): - logging.info("SUCCESS: Getting list of all NAE scripts on device succeeded") - else: - # Else logic designed for v10.04 and later - logging.info("SUCCESS: Getting dictionary of all NAE scripts on device succeeded") - all_nae_scripts = response.json() - - return all_nae_scripts - - -def get_nae_script(nae_script, params={}, **kwargs): - """ - Perform a GET call to get the details of a specific Network Analytics Engine script on the device. - - :param nae_script: String of name of the script that the function will return - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing the details of the specified NAE script on the device - """ - target_url = kwargs["url"] + "system/nae_scripts/" + nae_script - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting details of NAE script %s on device failed with status code %d: %s" - % (nae_script, response.status_code, response.text)) - nae_script_details = {} - else: - logging.info("SUCCESS: Getting details of NAE script %s on device succeeded" % nae_script) - nae_script_details = response.json() - - return nae_script_details - - -def get_nae_script_code(nae_script, params={}, **kwargs): - """ - Perform a GET call to get the decoded Network Analytics Engine script from the device. - - :param nae_script: String of name of the script that the function will return - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: String of NAE script python code, decoded from base64 - """ - target_url = kwargs["url"] + "system/nae_scripts/" + nae_script - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting NAE script %s python code on device failed with status code %d: %s" - % (nae_script, response.status_code, response.text)) - nae_script_code = "" - else: - logging.info("SUCCESS: Getting NAE script %s python code on device succeeded" % nae_script) - nae_script_details = response.json() - nae_script_code = base64.b64decode(nae_script_details['script']) - - return nae_script_code - - -def load_nae_script(script_name, script, is_base64=True, **kwargs): - """ - Perform a POST call to upload a Network Analytics Engine script to the device. This function passes the script - code as a parameter. By default, is_base64 is True and the script expects the base64 encoded text for the script. - If is_base64 is set to false, the function will expect a block of python code for the NAE script. - - :param script_name: Alphanumeric String of the name of the script - :param script: the base64 text or python code of the NAE script - :param is_base64: Boolean to determine if the script parameter is already encoded in base64. If the set to True, - the function will take in the script as is. If set to False, the function will encode the script to base64. - By default, this is True. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _load_nae_script_v1(script_name, script, is_base64, **kwargs) - else: # Updated else for when version is v10.04 - return _load_nae_script(script_name, script, is_base64, **kwargs) - - -def _load_nae_script_v1(script_name, script, is_base64, **kwargs): - """ - Perform a POST call to upload a Network Analytics Engine script to the device. This function passes the script - code as a parameter. By default, is_base64 is True and the script expects the base64 encoded text for the script. - If is_base64 is set to false, the function will expect a block of python code for the NAE script. - - :param script_name: Alphanumeric String of the name of the script - :param script: the base64 text or python code of the NAE script - :param is_base64: Boolean to determine if the script parameter is already encoded in base64. If the set to True, - the function will take in the script as is. If set to False, the function will encode the script to base64. - By default, this is True. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - script_list = get_all_nae_scripts(**kwargs) - - if "/rest/v1/system/nae_scripts/%s" % script_name not in script_list: - script_data = { - "name": script_name - } - - if is_base64: - script_data["script"] = script - else: - script_data["script"] = base64.b64encode(script) - - target_url = kwargs["url"] + "system/nae_scripts" - post_data = json.dumps(script_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Uploading NAE script named '%s' failed with status code %d: %s" - % (script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Uploading NAE script named '%s' succeeded" % script_name) - return True - else: - logging.info("SUCCESS: Upload not needed; NAE Script named '%s' already exists on the system" % script_name) - return True - - -def _load_nae_script(script_name, script, is_base64, **kwargs): - """ - Perform a POST call to upload a Network Analytics Engine script to the device. This function passes the script - code as a parameter. By default, is_base64 is True and the script expects the base64 encoded text for the script. - If is_base64 is set to false, the function will expect a block of python code for the NAE script. - - :param script_name: Alphanumeric String of the name of the script - :param script: the base64 text or python code of the NAE script - :param is_base64: Boolean to determine if the script parameter is already encoded in base64. If the set to True, - the function will take in the script as is. If set to False, the function will encode the script to base64. - By default, this is True. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - script_dict = get_all_nae_scripts(**kwargs) - - if script_name not in script_dict: - script_data = { - "expert_only": False, - "name": script_name - } - - if is_base64: - script_data["script"] = script - else: - script_data["script"] = base64.b64encode(script) - - target_url = kwargs["url"] + "system/nae_scripts" - post_data = json.dumps(script_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Uploading NAE script named '%s' failed with status code %d: %s" - % (script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Uploading NAE script named '%s' succeeded" % script_name) - return True - else: - logging.info("SUCCESS: Upload not needed; NAE Script named '%s' already exists on the system" % script_name) - return True - - -def load_nae_script_from_file(script_name, path_and_filename, is_base64=False, **kwargs): - """ - Retrieves a specified NAE script and uploads it to the device. This function will read the specified python file - then perform a POST call to upload the script to the device. By default, is_base64 is False and the function expects - to encoded the text in base64 prior to uploading. - If is_base64 is set to True, the function will attempt to encode the python code prior to uploading. - - :param script_name: Alphanumeric String of the name of the script - :param path_and_filename: Alphanumeric String of the path and filename to the NAE python script - :param is_base64: Boolean to determine if the script parameter is already encoded in base64. If the set to True, - the function will take in the script as is. If set to False, the function will encode the script to base64. - By default, this is True. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - with open(path_and_filename, 'rb') as py_file: - py_file_data = py_file.read() - - if not is_base64: - output_script = base64.b64encode(py_file_data) - output_script = output_script.decode('utf-8') - - else: - output_script = py_file_data - - return load_nae_script(script_name, output_script, is_base64=True, **kwargs) - - -def delete_nae_script(script_name, **kwargs): - """ - Perform a DELETE call to remove a Network Analytics Engine script from the system. - - *Note that by removing a script, all associated agents of the script will be removed as well. - :param script_name: Alphanumeric String of the name of the script - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - script_check = "/rest/v1/system/nae_scripts/%s" % script_name - else: # Updated else for when version is v10.04 - script_check = script_name - - all_scripts = get_all_nae_scripts(**kwargs) - - if script_check not in all_scripts: - logging.info("SUCCESS: NAE Script deletion not needed; NAE Script named '%s' does NOT exist on the system" - % script_name) - return True - - else: - target_url = kwargs["url"] + "system/nae_scripts/%s" % script_name - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting NAE script named '%s' failed with status code %d: %s" - % (script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting NAE script named '%s' succeeded" % script_name) - return True - - -def get_all_nae_agents_of_script(script_name, params={}, **kwargs): - """ - Perform a GET call to get a list or dictionary of Network Analytics Engine agents for the specified NAE script. - - :param script_name: Alphanumeric String of the name of the script - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: List (v1) or Dictionary (v10.04 or empty) containing all of the existing agents for the specified script. - """ - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents" % script_name - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting list/dictionary of all NAE agents for NAE script named %s failed with " - "status code %d: %s" % (script_name, response.status_code, response.text)) - all_nae_agents = {} - else: - if kwargs["url"].endswith("/v1/"): - logging.info("SUCCESS: Getting list of all NAE agents for script named %s on device succeeded" - % script_name) - else: - # Else logic designed for v10.04 and later - logging.info("SUCCESS: Getting dictionary of all NAE agents for script named %s on device succeeded" - % script_name) - all_nae_agents = response.json() - - return all_nae_agents - - -def get_nae_agent_details(script_name, agent_name, params={}, **kwargs): - """ - Perform a GET call to get the details of a specific Network Analytics Engine agent on the device. - - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent that will have details returned - :param params: Dictionary of optional parameters for the GET request - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Dictionary containing the details of the specified NAE script on the device - """ - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) - response = kwargs["s"].get(target_url, params=params, verify=False) - - if not common_ops._response_ok(response, "GET"): - logging.warning("FAIL: Getting details of NAE agent %s for script %s on device failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - nae_script_details = {} - else: - logging.info("SUCCESS: Getting details of NAE agent %s for script %s on device succeeded" - % (agent_name, script_name)) - nae_script_details = response.json() - - return nae_script_details - - -def create_nae_agent(script_name, agent_name, agent_parameters={}, disabled=False, **kwargs): - """ - Perform a POST call to create a Network Analytics Engine agent for the specified script. This function will also - take in the agent name, as well as a dictionary of parameters that are specific to the agent. - - *Note that upon initial check in, creating an NAE agent in v10.04 is not working. - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon creation. By default, disabled is False, - implying that the agent will be enabled upon creation. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _create_nae_agent_v1(script_name, agent_name, agent_parameters, disabled, **kwargs) - else: # Updated else for when version is v10.04 - return _create_nae_agent(script_name, agent_name, agent_parameters, disabled, **kwargs) - - -def _create_nae_agent_v1(script_name, agent_name, agent_parameters, disabled, **kwargs): - """ - Perform a POST call to create a Network Analytics Engine agent for the specified script. This function will also - take in the agent name, as well as a dictionary of parameters that are specific to the agent. - - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon creation. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - agent_list = get_all_nae_agents_of_script(script_name, **kwargs) - - if "/rest/v1/system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) not in agent_list: - agent_data = { - "disabled": disabled, - "encrypted_parameters_values": {}, - "name": agent_name, - "parameters_values": agent_parameters - } - - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents" % script_name - post_data = json.dumps(agent_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating NAE agent named '%s' from script '%s' failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating NAE agent named '%s' from script '%s' succeeded" - % (agent_name, script_name)) - return True - else: - logging.info("SUCCESS: Agent creation not needed; NAE Agent named '%s' already exists on the system" - % agent_name) - return True - - -def _create_nae_agent(script_name, agent_name, agent_parameters, disabled, **kwargs): - """ - Perform a POST call to create a Network Analytics Engine agent for the specified script. This function will also - take in the agent name, as well as a dictionary of parameters that are specific to the agent. - - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon creation. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - all_agents = get_all_nae_agents_of_script(script_name, **kwargs) - - if agent_name not in all_agents: - agent_data = { - "disabled": disabled, - "encrypted_parameters_values": {}, - "name": agent_name, - "parameters_values": agent_parameters - } - - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents" % script_name - post_data = json.dumps(agent_data, sort_keys=True, indent=4) - - response = kwargs["s"].post(target_url, data=post_data, verify=False) - - if not common_ops._response_ok(response, "POST"): - logging.warning("FAIL: Creating NAE agent named '%s' from script '%s' failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Creating NAE agent named '%s' from script '%s' succeeded" - % (agent_name, script_name)) - return True - else: - logging.info("SUCCESS: Agent creation not needed; NAE Agent named '%s' already exists on the system" - % agent_name) - return True - - -def update_nae_agent(script_name, agent_name, agent_parameters, disabled=False, **kwargs): - """ - Perform a PUT call to update a specified Network Analytics Engine agent. This function will take in a dictionary of - parameters that are specific to the agent, as well as a boolean to update whether the agent is disabled or enabled. - - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon updating. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - return _update_nae_agent_v1(script_name, agent_name, agent_parameters, disabled, **kwargs) - else: # Updated else for when version is v10.04 - return _update_nae_agent(script_name, agent_name, agent_parameters, disabled, **kwargs) - - -def _update_nae_agent_v1(script_name, agent_name, agent_parameters, disabled, **kwargs): - """ - Perform a PUT call to update a specified Network Analytics Engine agent. This function will take in a dictionary of - parameters that are specific to the agent, as well as a boolean to update whether the agent is disabled or enabled. - - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon updating. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - all_agents = get_all_nae_agents_of_script(script_name, **kwargs) - all_agents_check = [] - # Create a new list with full uri stripped from agent list - for agent in all_agents: - all_agents_check.append(agent[(agent.rfind('/')+1):]) - - if agent_name not in all_agents_check: - logging.warning("FAIL: Updating NAE agent named '%s' failed. No agent with that name was found." % agent_name) - return False - else: - current_agent_details = get_nae_agent_details(script_name, agent_name, **kwargs) - new_parameters = {} - # Create a new dictionary with keys stripped of full uri - for parameter in current_agent_details['parameters_values']: - key = parameter[(parameter.rfind('/')+1):] - value = current_agent_details['parameters_values'][parameter] - new_parameters[key] = value - - # Update the stripped dictionary with the key/value pairs passed in for the function - new_parameters.update(agent_parameters) - - updated_agent_data = { - "disabled": disabled, - "encrypted_parameters_values": {}, - "parameters_values": new_parameters - } - - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) - put_data = json.dumps(updated_agent_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating NAE agent named '%s' from script '%s' failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating NAE agent named '%s' from script '%s' succeeded" - % (agent_name, script_name)) - return True - - -def _update_nae_agent(script_name, agent_name, agent_parameters, disabled, **kwargs): - """ - Perform a PUT call to update a specified Network Analytics Engine agent. This function will take in a dictionary of - parameters that are specific to the agent, as well as a boolean to update whether the agent is disabled or enabled. - - *Note that encrypted parameters are not currently supported in this function. - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param agent_parameters: Dictionary of optional parameters for the agent. The key:value pairing is based on the - agent parameter name and values to be passed in. Any parameters not mentioned will be set to the default value - for the given parameter, specified in the NAE script. - :param disabled: Boolean to determine if the agent is disabled upon updating. - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - all_agents = get_all_nae_agents_of_script(script_name, **kwargs) - - if agent_name not in all_agents: - logging.warning("FAIL: Updating NAE agent named '%s' failed. No agent with that name was found." % agent_name) - return False - else: - current_agent_details = get_nae_agent_details(script_name, agent_name, **kwargs) - new_parameters = {} - # Create a new dictionary with keys stripped of full uri - for parameter in current_agent_details['parameters_values']: - key = parameter[(parameter.rfind('/')+1):] - value = current_agent_details['parameters_values'][parameter] - new_parameters[key] = value - - # Update the stripped dictionary with the key/value pairs passed in for the function - new_parameters.update(agent_parameters) - - updated_agent_data = { - "disabled": disabled, - "encrypted_parameters_values": {}, - "parameters_values": new_parameters - } - - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) - put_data = json.dumps(updated_agent_data, sort_keys=True, indent=4) - - response = kwargs["s"].put(target_url, data=put_data, verify=False) - - if not common_ops._response_ok(response, "PUT"): - logging.warning("FAIL: Updating NAE agent named '%s' from script '%s' failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Updating NAE agent named '%s' from script '%s' succeeded" - % (agent_name, script_name)) - return True - - -def delete_nae_agent(script_name, agent_name, **kwargs): - """ - Perform a DELETE call to remove a Network Analytics Engine agent for the specified script. - - :param script_name: Alphanumeric String of the name of the script - :param agent_name: Alphanumeric String of the name of the agent - :param kwargs: - keyword s: requests.session object with loaded cookie jar - keyword url: URL in main() function - :return: Boolean True if successful, False otherwise - """ - if kwargs["url"].endswith("/v1/"): - agent_check = "/rest/v1/system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) - else: # Updated else for when version is v10.04 - agent_check = agent_name - - all_agents = get_all_nae_agents_of_script(script_name, **kwargs) - - if agent_check not in all_agents: - logging.info("SUCCESS: Agent deletion not needed; NAE Agent named '%s' does NOT exist on the system" - % agent_name) - return True - - else: - target_url = kwargs["url"] + "system/nae_scripts/%s/nae_agents/%s" % (script_name, agent_name) - response = kwargs["s"].delete(target_url, verify=False) - - if not common_ops._response_ok(response, "DELETE"): - logging.warning("FAIL: Deleting NAE agent named '%s' based on script '%s' failed with status code %d: %s" - % (agent_name, script_name, response.status_code, response.text)) - return False - else: - logging.info("SUCCESS: Deleting NAE agent named '%s' based on script '%s' succeeded" - % (agent_name, script_name)) - return True