├── .coveragerc ├── .git-commit-template.txt ├── .gitignore ├── .pylintrc ├── .travis.yml ├── LICENSE ├── README.md ├── build-tools ├── system-test-img.sh └── update-copyright.sh ├── f5_cccl ├── __init__.py ├── api.py ├── bigip.py ├── exceptions.py ├── resource │ ├── __init__.py │ ├── ltm │ │ ├── __init__.py │ │ ├── app_service.py │ │ ├── internal_data_group.py │ │ ├── irule.py │ │ ├── monitor │ │ │ ├── __init__.py │ │ │ ├── http_monitor.py │ │ │ ├── https_monitor.py │ │ │ ├── icmp_monitor.py │ │ │ ├── monitor.py │ │ │ ├── tcp_monitor.py │ │ │ ├── test │ │ │ │ ├── test_http_monitor.py │ │ │ │ ├── test_https_monitor.py │ │ │ │ ├── test_icmp_monitor.py │ │ │ │ ├── test_monitor.py │ │ │ │ ├── test_tcp_monitor.py │ │ │ │ └── test_udp_monitor.py │ │ │ └── udp_monitor.py │ │ ├── node.py │ │ ├── policy │ │ │ ├── __init__.py │ │ │ ├── action.py │ │ │ ├── condition.py │ │ │ ├── policy.py │ │ │ ├── rule.py │ │ │ └── test │ │ │ │ ├── bigip_policy.json │ │ │ │ ├── test_action.py │ │ │ │ ├── test_condition.py │ │ │ │ ├── test_policy.py │ │ │ │ └── test_rule.py │ │ ├── pool.py │ │ ├── pool_member.py │ │ ├── profile │ │ │ ├── __init__.py │ │ │ ├── profile.py │ │ │ └── test │ │ │ │ └── test_profile.py │ │ ├── test │ │ │ ├── bigip-members.json │ │ │ ├── conftest.py │ │ │ ├── test_api_pool_member.py │ │ │ ├── test_app_service.py │ │ │ ├── test_internal_data_group.py │ │ │ ├── test_irule.py │ │ │ ├── test_node.py │ │ │ ├── test_pool.py │ │ │ ├── test_pool_member.py │ │ │ ├── test_virtual.py │ │ │ └── test_virtual_address.py │ │ ├── virtual.py │ │ └── virtual_address.py │ ├── net │ │ ├── __init__.py │ │ ├── arp.py │ │ ├── fdb │ │ │ ├── __init__.py │ │ │ ├── record.py │ │ │ ├── test │ │ │ │ ├── test_record.py │ │ │ │ └── test_tunnel.py │ │ │ └── tunnel.py │ │ ├── route.py │ │ └── test │ │ │ └── test_arp.py │ ├── resource.py │ └── test │ │ ├── test_merge_resource.py │ │ └── test_resource.py ├── schemas │ ├── cccl-ltm-api-schema.yml │ ├── cccl-net-api-schema.yml │ └── tests │ │ ├── ltm_service.json │ │ ├── net_service.json │ │ └── test_policy_schema_01.json ├── service │ ├── __init__.py │ ├── config_reader.py │ ├── manager.py │ ├── test │ │ ├── bad_decode_schema.json │ │ ├── bad_schema.json │ │ ├── test_config_reader.py │ │ ├── test_schema_validator.py │ │ ├── test_service_manager.py │ │ └── test_validation.py │ └── validation.py ├── test │ ├── __init__.py │ ├── bigip_data.json │ ├── bigip_net_data.json │ ├── conftest.py │ ├── test_api.py │ ├── test_bigip.py │ └── test_exceptions.py └── utils │ ├── __init__.py │ ├── json_pos_patch.py │ ├── mgmt.py │ ├── profile.py │ ├── resource_merge.py │ ├── route_domain.py │ └── test │ ├── test_json_pos_patch.py │ ├── test_resource_merge.py │ └── test_route_domain.py ├── requirements.docs.txt ├── requirements.test.txt ├── setup.py ├── setup_requirements.txt ├── test └── f5_cccl │ ├── conftest.py │ ├── perf │ └── test_perf.py │ ├── resource │ └── ltm │ │ ├── monitor │ │ ├── monitor_schemas.py │ │ └── test_fn_monitor.py │ │ └── policy │ │ └── test_policy.py │ └── service │ └── test_manager.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | # unit tests 4 | */test/* 5 | */testcommon.py 6 | 7 | # setup etc. 8 | setup.py 9 | -------------------------------------------------------------------------------- /.git-commit-template.txt: -------------------------------------------------------------------------------- 1 | # Using the Fixes # will close the issue on commit to repo 2 | Fixes #: 3 | 4 | # Describe the issue that this change addresses 5 | Problem: 6 | 7 | # Describe the change itself and why you made the changes you did 8 | Analysis: 9 | 10 | # Describe the tests you ran and/or created to test this change 11 | Tests: 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # symbols file for pytest 92 | symbols.yaml 93 | 94 | # PyCharm directory 95 | .idea 96 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | sudo: true 6 | env: 7 | global: 8 | - REPO="f5-cccl" 9 | - PKG_VERSION=$(python -c "import f5_cccl; print f5_cccl.__version__") 10 | - MARATHON_BIGIP_CTLR_COMMIT_ISH=8a52ddb37191e9dd53c74ebfb2248dafe834e9e6 11 | - K8S_BIGIP_CTLR_COMMIT_ISH=e7550564dd61ac11df70799bc9648139d47f1edb 12 | services: 13 | - docker 14 | before_install: 15 | - git config --global user.email "OpenStack_TravisCI@f5.com" 16 | - git config --global user.name "Travis F5 Openstack" 17 | install: 18 | - pip install tox 19 | - pip install -r requirements.test.txt 20 | - pip install -r requirements.docs.txt 21 | - python ./setup.py install 22 | script: 23 | - tox -e style 24 | - tox -e unit 25 | # For security, travis CI not provide env vars on fork PRs. 26 | # So we only run coverage when the env var is present at merge. 27 | # https://docs.travis-ci.com/user/pull-requests/#Pull-Requests-and-Security-Restrictions 28 | - if [ "$COVERALLS_REPO_TOKEN" != "" ]; then tox -e coverage; fi 29 | - tox -e functional 30 | deploy: 31 | # push marathon dev-image for nightly regression tests 32 | - provider: script 33 | skip_cleanup: true 34 | script: ./build-tools/system-test-img.sh F5Networks/marathon-bigip-ctlr $MARATHON_BIGIP_CTLR_COMMIT_ISH ./build-tools/build-runtime-images.sh $DOCKER_NAMESPACE 35 | on: 36 | python: "2.7" 37 | all_branches: true 38 | # push k8s dev-image for nightly regression tests 39 | - provider: script 40 | skip_cleanup: true 41 | script: ./build-tools/system-test-img.sh F5Networks/k8s-bigip-ctlr $K8S_BIGIP_CTLR_COMMIT_ISH "make prod" $DOCKER_NAMESPACE 42 | on: 43 | python: "2.7" 44 | all_branches: true 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # f5-cccl 2 | 3 | [![Build Status](https://travis-ci.org/f5devcentral/f5-cccl.svg?branch=master)](https://travis-ci.org/f5devcentral/f5-cccl) [![Coverage Status](https://coveralls.io/repos/github/f5devcentral/f5-cccl/badge.svg?branch=HEAD)](https://coveralls.io/github/f5devcentral/f5-cccl?branch=HEAD) 4 | 5 | # Introduction 6 | 7 | This project implements a Common Controller Core Library for orchestration an F5 BIG-IP (r) for use within other libraries that need to read, diff and apply configurations to a BIG-IP (r). 8 | 9 | # Installation 10 | Add f5-cccl to the `requirements.txt` file for your project. Use [editable package format](https://pip.readthedocs.io/en/stable/reference/pip_install/#git): 11 | ``` 12 | [-e] git+https://git.myproject.org/MyProject#egg=MyProject 13 | ``` 14 | 15 | # Filling Issues 16 | 17 | Creating issues is good, creating good issues is even better. Please provide: 18 | 19 | * Clear steps on how to replicate the issue 20 | * Stack trace and error messages 21 | * SHA and branch information for f5-cccl 22 | * SHA and branch information for component using f5-cccl 23 | 24 | # Contributing 25 | 26 | This project is used internally by other F5 projects; we're not yet ready to accept contributions. 27 | Please check back later or see if another project, such as https://github.com/F5Networks/f5-common-python 28 | would be a good place for your contribution. 29 | 30 | # Copyright 31 | Copyright (c) 2017-2021 F5 Networks, Inc. 32 | 33 | # License 34 | 35 | ## Apache V2.0 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 38 | this file except in compliance with the License. You may obtain a copy of the 39 | License at 40 | 41 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 46 | See the License for the specific language governing permissions and limitations 47 | under the License. 48 | -------------------------------------------------------------------------------- /build-tools/system-test-img.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # builds docker image with top of tree CCCL using build-tools in repo using CCCL 3 | # for use in system tests within the repo using CCCL 4 | # local usage example: 5 | # `./build-tools/system-test-img.sh F5Networks/k8s-bigip-ctlr 3609340 "make prod" f5networksdevel f5devcentral/f5-cccl.git@83d7a311767ea8ca47e144233c4697fce0a3a2bd` 6 | 7 | set -ex 8 | 9 | USER_REPO=$1 10 | SHA=$2 11 | BUILD_CMD=$3 12 | DOCKER_NAMESPACE=$4 13 | EDITABLE_REQ=$5 14 | 15 | REPO=$(echo $USER_REPO | cut -d "/" -f 2) 16 | 17 | if [ \ 18 | "$USER_REPO" == "" -o \ 19 | "$SHA" == "" -o \ 20 | "$BUILD_CMD" == "" -o \ 21 | "$DOCKER_NAMESPACE" == "" \ 22 | ]; then 23 | echo "[ERROR:] repo, sha, build command & docker namespace required" 24 | false 25 | fi 26 | 27 | # in travis, fail f5devcentral commits that cannot push to docker 28 | # warn and skip docker if on fork 29 | 30 | if [ "$TRAVIS" ]; then 31 | if [ "$DOCKER_P" == "" -o "$DOCKER_U" == "" -o $DOCKER_NAMESPACE == "" ]; then 32 | echo "[INFO] DOCKER_U, DOCKER_P, or DOCKER_NAMESPACE vars absent from travis-ci." 33 | if [ "$TRAVIS_REPO_SLUG" == "f5devcentral/f5-cccl" ]; then 34 | echo "[ERROR] Docker push for f5devcentral will fail. Contact repo admin." 35 | false 36 | else 37 | echo "[INFO] Not an 'f5devcentral' commit, docker optional." 38 | echo "[INFO] Add DOCKER_U, DOCKER_P, and DOCKER_NAMESPACE to travis-ci to push to DockerHub." 39 | fi 40 | else 41 | docker login -u="$DOCKER_U" -p="$DOCKER_P" 42 | EDITABLE_REQ="$TRAVIS_REPO_SLUG.git@$TRAVIS_COMMIT" 43 | DOCKER_READY="true" 44 | fi 45 | else 46 | if [ "$EDITABLE_REQ" == "" ]; then 47 | echo "[ERROR] Specify an editable requirement for pip of the form /f5-cccl.git@" 48 | false 49 | fi 50 | TRAVIS_COMMIT=$(echo $EDITABLE_REQ | cut -d "@" -f 2) 51 | TRAVIS_BUILD_ID=$(date +%Y%m%d-%H%M) 52 | TRAVIS_BUILD_NUMBER="local" 53 | DOCKER_READY="true" 54 | fi 55 | 56 | if [ "$DOCKER_READY" ]; then 57 | git clone https://github.com/$USER_REPO.git 58 | cd $REPO 59 | git checkout -b cccl-systest $SHA 60 | find . -name "*requirements.txt" | xargs sed -i -e 's|f5devcentral/f5-cccl\.git@.*#|'"$EDITABLE_REQ"'#|' 61 | export IMG_TAG="${DOCKER_NAMESPACE}/cccl:$REPO-${TRAVIS_COMMIT}" 62 | $BUILD_CMD 63 | docker tag "$IMG_TAG" "$DOCKER_NAMESPACE/cccl:$REPO" 64 | docker tag "$IMG_TAG" "$DOCKER_NAMESPACE/cccl:$REPO-n-$TRAVIS_BUILD_NUMBER-id-$TRAVIS_BUILD_ID" 65 | docker push "$IMG_TAG" 66 | docker push "$DOCKER_NAMESPACE/cccl:$REPO" 67 | docker push "$DOCKER_NAMESPACE/cccl:$REPO-n-$TRAVIS_BUILD_NUMBER-id-$TRAVIS_BUILD_ID" 68 | fi 69 | -------------------------------------------------------------------------------- /build-tools/update-copyright.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Description: This script recursively searches the folder starting at FILEPATH 4 | # for all Copyright notices and both formats and updates them to extend from the 5 | # first year listed to the year provided by the user 6 | 7 | # Defines relative path from this file to top of directory where a recursive 8 | # copyright update will be performed 9 | FILEPATH="../" 10 | 11 | printf "\nAll instances of Copyright notices:\n": 12 | # Saves the grep command in a variable since it is called multiple times 13 | GCMD=$(grep -r -n --exclude-dir=".git" --exclude="update-copyright.sh" "Copyright (*[cC]*)* *20" $FILEPATH | grep "F5 Networks") 14 | # Displays all Copyright lines excluding those in the .git folder and saves these output lines to GREPOUT 15 | GREPOUT=$(echo "$GCMD") 16 | echo "$GREPOUT" 17 | # Displays the number of Copyright lines 18 | printf "Number of found Copyright lines: " 19 | echo "$GREPOUT" | wc -l 20 | 21 | # Allow user to stop after searching for Copyright notices 22 | read -n1 -p " 23 | Update copyright notices listed above? (y/n) 24 | " ANS 25 | case $ANS in 26 | y|Y) ;; 27 | *) exit ;; 28 | esac 29 | 30 | # Allows the user to enter the year to which they wish to have the Copyright notices extended 31 | printf "\nEnter the year you would like to add to the Copyright notices:" 32 | read YEAR 33 | 34 | printf "\nUpdating Copyrights...\n" 35 | 36 | # Search for all instances of "Copyright 20**" and "Copyright (c) 20**" 37 | echo "$GREPOUT" | while read -r line 38 | # Updates all Copyright notices and replaces for matching syntax 39 | do 40 | FIRSTYEAR=$(echo "$line" | grep -E -o -m 1 '20[0-9]{2}' | head -1) 41 | REPLACE=$(echo "$line" | cut -d: -f3 | sed 's/^.*Copyright/Copyright/' | sed 's/Inc\..*/Inc\./') 42 | #Already includes the current year and no other year 43 | if [[ $line = *$YEAR* && $YEAR == $FIRSTYEAR ]]; then 44 | echo "$line" | cut -d: -f1 | xargs sed -i '' "s/$REPLACE/Copyright\ (c)\ $YEAR,\ F5\ Networks,\ Inc./" 45 | #Already includes the current year and the previous year 46 | elif [[ $line == *$YEAR* && $YEAR -eq $((FIRSTYEAR + 1)) ]]; then 47 | echo "$line" | cut -d: -f1 | xargs sed -i '' "s/$REPLACE/Copyright\ (c)\ $FIRSTYEAR,$YEAR,\ F5\ Networks,\ Inc./" 48 | #Already includes the current year in a range 49 | elif [[ $line == *$YEAR* && $YEAR -gt $((FIRSTYEAR + 1)) ]]; then 50 | echo "$line" | cut -d: -f1 | xargs sed -i '' "s/$REPLACE/Copyright\ (c)\ $FIRSTYEAR-$YEAR,\ F5\ Networks,\ Inc./" 51 | #Only includes the previous year 52 | elif [[ $line != *$YEAR* && $YEAR -eq $((FIRSTYEAR + 1)) ]]; then 53 | echo "$line" | cut -d: -f1 | xargs sed -i '' "s/$REPLACE/Copyright\ (c)\ $FIRSTYEAR,$YEAR,\ F5\ Networks,\ Inc./" 54 | #Only includes a year earlier than the previous year 55 | elif [[ $line != *$YEAR* && $YEAR -gt $((FIRSTYEAR + 1)) ]]; then 56 | echo "$line" | cut -d: -f1 | xargs sed -i '' "s/$REPLACE/Copyright\ (c)\ $FIRSTYEAR-$YEAR,\ F5\ Networks,\ Inc./" 57 | fi 58 | done 59 | 60 | printf "\nAll instances of Copyright notices after update:\n": 61 | # Displays all updated Copyright lines excluding those in the .git folder 62 | GREPOUT=$(echo "$GCMD") 63 | echo "$GREPOUT" 64 | # Displays the number of Copyright lines 65 | printf "Number of found Copyright lines: " 66 | echo "$GREPOUT" | wc -l 67 | -------------------------------------------------------------------------------- /f5_cccl/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """F5 Common Controller Core Library. 17 | 18 | This module implements a Common Controller Core Library for use within other 19 | libraries that need to read, diff and apply configurations to a BIG-IP. 20 | """ 21 | 22 | __version__ = '0.1.0' 23 | -------------------------------------------------------------------------------- /f5_cccl/api.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """F5 Common Controller Core Library to read, diff and apply BIG-IP config.""" 18 | 19 | import logging 20 | import pkg_resources 21 | 22 | from f5_cccl.bigip import BigIPProxy 23 | from f5_cccl.service.manager import ServiceManager 24 | 25 | resource_package = __name__ 26 | ltm_api_schema = "schemas/cccl-ltm-api-schema.yml" 27 | net_api_schema = "schemas/cccl-net-api-schema.yml" 28 | 29 | LOGGER = logging.getLogger("f5_cccl") 30 | 31 | 32 | class F5CloudServiceManager(object): 33 | """F5 Common Controller Cloud Service Management. 34 | 35 | The F5 Common Controller Core Library (CCCL) is an orchestration package 36 | that provides a declarative API for defining BIG-IP LTM and NET services 37 | in diverse environments (e.g. Marathon, Kubernetes, OpenStack). The 38 | API will allow a user to create proxy services by specifying the: 39 | virtual servers, pools, L7 policy and rules, monitors, arps, or fdbTunnels 40 | as a service description object. Each instance of the CCCL is initialized 41 | with namespace qualifiers to allow it to uniquely identify the resources 42 | under its control. 43 | """ 44 | 45 | def __init__(self, bigip, partition, user_agent=None, prefix=None, 46 | schema_path=None): 47 | """Initialize an instance of the F5 CCCL service manager. 48 | 49 | :param bigip: BIG-IP management root. 50 | :param partition: Name of BIG-IP partition to manage. 51 | :param user_agent: String to append to the User-Agent header for 52 | iControl REST requests (default: None) 53 | :param prefix: The prefix assigned to resources that should be 54 | managed by this CCCL instance. This is prepended to the 55 | resource name (default: None) 56 | :param schema_path: User defined schema (default: from package) 57 | """ 58 | LOGGER.debug("F5CloudServiceManager initialize") 59 | 60 | # Set user-agent for ICR session 61 | if user_agent is not None: 62 | bigip.icrs.append_user_agent(user_agent) 63 | self._user_agent = user_agent 64 | 65 | self._bigip_proxy = BigIPProxy(bigip, 66 | partition, 67 | prefix=prefix) 68 | 69 | if schema_path is None: 70 | schema_path = pkg_resources.resource_filename(resource_package, 71 | ltm_api_schema) 72 | self._service_manager = ServiceManager(self._bigip_proxy, 73 | partition, 74 | schema_path) 75 | 76 | def get_proxy(self): 77 | """Return the BigIP proxy""" 78 | 79 | # This is only needed until delete_unused_ssl_profiles is properly 80 | # integrated into apply_ltm_config 81 | return self._bigip_proxy 82 | 83 | def apply_ltm_config(self, services): 84 | """Apply LTM service configurations to the BIG-IP partition. 85 | 86 | :param services: A serializable object that defines one or more 87 | services. Its schema is defined by cccl-ltm-api-schema.json. 88 | 89 | :return: True if successful, otherwise an exception is thrown. 90 | """ 91 | return self._service_manager.apply_ltm_config(services, 92 | self._user_agent) 93 | 94 | def apply_net_config(self, services): 95 | """Apply NET service configurations to the BIG-IP partition. 96 | 97 | :param services: A serializable object that defines one or more 98 | services. Its schema is defined by cccl-net-api-schema.json. 99 | 100 | :return: True if successful, otherwise an exception is thrown. 101 | """ 102 | return self._service_manager.apply_net_config(services) 103 | 104 | def get_partition(self): 105 | """Get the name of the managed partition. 106 | 107 | :return: The managed partition name. 108 | """ 109 | return self._service_manager.get_partition() 110 | 111 | def get_status(self): 112 | """Get status for each service in the managed partition. 113 | 114 | :return: A serializable object of the statuses of each managed 115 | resource. 116 | 117 | Its structure is defined by: 118 | cccl-status-schema.json 119 | """ 120 | status = {} 121 | 122 | return status 123 | 124 | def get_statistics(self): 125 | """Get statistics for each service in the managed partition. 126 | 127 | :return: A serializable object of the virtual server statistics 128 | for each service. 129 | 130 | Its structure is defined by: 131 | cccl-statistics-schema.json 132 | """ 133 | statistics = {} 134 | 135 | return statistics 136 | -------------------------------------------------------------------------------- /f5_cccl/exceptions.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | 19 | """This module defines the exceptions used in f5_cccl.""" 20 | 21 | 22 | class F5CcclError(Exception): 23 | """Base class for f5_cccl exceptions.""" 24 | 25 | def __init__(self, msg=None): 26 | """Initialize object members.""" 27 | super(F5CcclError, self).__init__() 28 | self.msg = msg 29 | 30 | def __str__(self): 31 | """Generate a string representation of the object.""" 32 | classname = self.__class__.__name__ 33 | if self.msg: 34 | return "%s - %s" % (classname, self.msg) 35 | return classname 36 | 37 | 38 | class F5CcclSchemaError(F5CcclError): 39 | """Error raised when base schema defining API is invalid.""" 40 | 41 | def __init__(self, msg): 42 | """Initialize with base schema invalid message.""" 43 | super(F5CcclSchemaError, self).__init__(msg) 44 | self.msg = 'Schema provided is invalid: ' + msg 45 | 46 | 47 | class F5CcclValidationError(F5CcclError): 48 | """Error raised when service config is invalid against the API schema.""" 49 | 50 | def __init__(self, msg): 51 | """Initialize with base config does not match schema message.""" 52 | super(F5CcclValidationError, self).__init__(msg) 53 | self.msg = 'Service configuration provided does not match schema: ' + \ 54 | msg 55 | 56 | 57 | class F5CcclResourceCreateError(F5CcclError): 58 | """General resource creation failure.""" 59 | 60 | 61 | class F5CcclResourceConflictError(F5CcclError): 62 | """Resource already exists on BIG-IP?.""" 63 | 64 | 65 | class F5CcclResourceNotFoundError(F5CcclError): 66 | """Resource not found on BIG-IP?.""" 67 | 68 | 69 | class F5CcclResourceRequestError(F5CcclError): 70 | """Resource request client error on BIG-IP?.""" 71 | 72 | 73 | class F5CcclResourceUpdateError(F5CcclError): 74 | """General resource update failure.""" 75 | 76 | 77 | class F5CcclResourceDeleteError(F5CcclError): 78 | """General resource delete failure.""" 79 | 80 | 81 | class F5CcclApplyConfigError(F5CcclError): 82 | """General config deployment failure.""" 83 | 84 | 85 | class F5CcclCacheRefreshError(F5CcclError): 86 | """Failed to update the BigIP configuration state.""" 87 | 88 | 89 | class F5CcclConfigurationReadError(F5CcclError): 90 | """Failed to create a Resource from the API configuration.""" 91 | -------------------------------------------------------------------------------- /f5_cccl/resource/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """This module implements the F5 CCCL Resource super class.""" 17 | 18 | 19 | from .resource import Resource # noqa: F401 20 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/resource/ltm/__init__.py -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/internal_data_group.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP iRule resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from copy import deepcopy 20 | import logging 21 | 22 | from f5_cccl.resource import Resource 23 | 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | def get_record_key(record): 29 | """Allows data groups to be sorted by the 'name' member.""" 30 | return record.get('name', '') 31 | 32 | 33 | class InternalDataGroup(Resource): 34 | """InternalDataGroup class.""" 35 | # The property names class attribute defines the names of the 36 | # properties that we wish to compare. 37 | properties = dict( 38 | name=None, 39 | partition=None, 40 | type=None, 41 | records=list() 42 | ) 43 | 44 | def __init__(self, name, partition, **data): 45 | """Create the InternalDataGroup""" 46 | super(InternalDataGroup, self).__init__(name, partition) 47 | 48 | self._data['type'] = data.get('type', '') 49 | records = data.get('records', list()) 50 | self._data['records'] = sorted(records, key=get_record_key) 51 | 52 | def __eq__(self, other_dg): 53 | """Check the equality of the two objects. 54 | 55 | Only compare the properties as defined in the 56 | properties class dictionany. 57 | """ 58 | if not isinstance(other_dg, InternalDataGroup): 59 | return False 60 | for key in self.properties: 61 | if self._data[key] != other_dg.data.get(key, None): 62 | return False 63 | return True 64 | 65 | def __hash__(self): # pylint: disable=useless-super-delegation 66 | return super(InternalDataGroup, self).__hash__() 67 | 68 | def _uri_path(self, bigip): 69 | return bigip.tm.ltm.data_group.internals.internal 70 | 71 | def __str__(self): 72 | return str(self._data) 73 | 74 | def update(self, bigip, data=None, modify=False): 75 | """Override of base class implemntation, required because data-groups 76 | are picky about what data can exist in the object when modifying. 77 | """ 78 | tmp_copy = deepcopy(self) 79 | tmp_copy.do_update(bigip, data, modify) 80 | 81 | def do_update(self, bigip, data, modify): 82 | """Remove 'type' before doing the update.""" 83 | del self._data['type'] 84 | super(InternalDataGroup, self).update( 85 | bigip, data=data, modify=modify) 86 | 87 | 88 | class IcrInternalDataGroup(InternalDataGroup): 89 | """InternalDataGroup object created from the iControl REST object""" 90 | pass 91 | 92 | 93 | class ApiInternalDataGroup(InternalDataGroup): 94 | """InternalDataGroup object created from the API configuration object""" 95 | pass 96 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/irule.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP iRule resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | class IRule(Resource): 28 | """iRule class.""" 29 | # The property names class attribute defines the names of the 30 | # properties that we wish to compare. 31 | properties = dict( 32 | name=None, 33 | partition=None, 34 | apiAnonymous=None 35 | ) 36 | 37 | def __init__(self, name, partition, **data): 38 | """Create the iRule""" 39 | super(IRule, self).__init__(name, partition, **data) 40 | 41 | self._data['metadata'] = data.get( 42 | 'metadata', 43 | self.properties.get('metadata') 44 | ) 45 | self._data['apiAnonymous'] = data.get( 46 | 'apiAnonymous', 47 | self.properties.get('apiAnonymous') 48 | ) 49 | # Strip any leading/trailing whitespace 50 | if self._data['apiAnonymous'] is not None: 51 | self._data['apiAnonymous'] = self._data['apiAnonymous'].strip() 52 | 53 | def __eq__(self, other): 54 | """Check the equality of the two objects. 55 | 56 | Only compare the properties as defined in the 57 | properties class dictionany. 58 | """ 59 | if not isinstance(other, IRule): 60 | return False 61 | 62 | for key in self.properties: 63 | if self._data[key] != other.data.get(key, None): 64 | return False 65 | return True 66 | 67 | def __hash__(self): # pylint: disable=useless-super-delegation 68 | return super(IRule, self).__hash__() 69 | 70 | def _uri_path(self, bigip): 71 | return bigip.tm.ltm.rules.rule 72 | 73 | def __str__(self): 74 | return str(self._data) 75 | 76 | 77 | class IcrIRule(IRule): 78 | """iRule object created from the iControl REST object""" 79 | pass 80 | 81 | 82 | class ApiIRule(IRule): 83 | """IRule object created from the API configuration object""" 84 | pass 85 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """This module implements the F5 CCCL Resource super class.""" 17 | 18 | 19 | from .monitor import Monitor # noqa: F401, F403 20 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/http_monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # 7 | # Copyright (c) 2017-2021 F5 Networks, Inc. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | 22 | import logging 23 | 24 | from f5_cccl.resource.ltm.monitor import Monitor 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | class HTTPMonitor(Monitor): 31 | """Creates a CCCL BIG-IP HTTP Monitor Object of sub-type of Resource 32 | 33 | This object hosts the ability to orchestrate basic CRUD actions against a 34 | BIG-IP HTTP Monitor via the F5-SDK. 35 | 36 | The major difference is the afforded schema for HTTP specifically. 37 | """ 38 | http_properties = dict(interval=5, 39 | timeout=16, 40 | send="GET /\\r\\n", 41 | recv="") 42 | 43 | def __init__(self, name, partition, **kwargs): 44 | super(HTTPMonitor, self).__init__(name, partition, **kwargs) 45 | for key in ['send', 'recv']: 46 | self._data[key] = kwargs.get(key, self.http_properties.get(key)) 47 | 48 | def _uri_path(self, bigip): 49 | """Get the URI resource path key for the F5-SDK for HTTP monitor 50 | 51 | This is the URI reference for an HTTP Monitor. 52 | """ 53 | return bigip.tm.ltm.monitor.https.http 54 | 55 | 56 | class ApiHTTPMonitor(HTTPMonitor): 57 | """Create the canonical HTTP monitor from API input.""" 58 | pass 59 | 60 | 61 | class IcrHTTPMonitor(HTTPMonitor): 62 | """Create the canonical HTTP monitor from iControl REST response.""" 63 | def __init__(self, name, partition, **kwargs): 64 | try: 65 | super(IcrHTTPMonitor, self).__init__(name, partition, **kwargs) 66 | except ValueError: 67 | # Need to allow for misconfigured legacy monitors from BIG-IP, 68 | # so let this through 69 | pass 70 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/https_monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # 7 | # Copyright (c) 2017-2021 F5 Networks, Inc. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | 22 | import logging 23 | 24 | from f5_cccl.resource.ltm.monitor import Monitor 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | class HTTPSMonitor(Monitor): 31 | """Creates a CCCL BIG-IP HTTPS Monitor Object of sub-type of Resource 32 | 33 | This object hosts the ability to orchestrate basic CRUD actions against a 34 | BIG-IP HTTPS Monitor via the F5-SDK. 35 | 36 | The major difference is the afforded schema for HTTPS specifically. 37 | """ 38 | properties = dict(interval=5, 39 | timeout=16, 40 | send="GET /\\r\\n", 41 | recv="", 42 | sslProfile="") 43 | 44 | def __init__(self, name, partition, **kwargs): 45 | super(HTTPSMonitor, self).__init__(name, partition, **kwargs) 46 | for key in ['send', 'recv', 'sslProfile']: 47 | self._data[key] = kwargs.get(key, self.properties.get(key)) 48 | # fix for bipip health monitor 49 | self._data["compatibility"] = "disabled" 50 | 51 | def _uri_path(self, bigip): 52 | """Get the URI resource path key for the F5-SDK for HTTPS monitor 53 | 54 | This is the URI reference for an HTTPS Monitor. 55 | """ 56 | return bigip.tm.ltm.monitor.https_s.https 57 | 58 | 59 | class ApiHTTPSMonitor(HTTPSMonitor): 60 | """Create the canonical HTTPS monitor from API input.""" 61 | pass 62 | 63 | 64 | class IcrHTTPSMonitor(HTTPSMonitor): 65 | """Create the canonical HTTPS monitor from iControl REST response.""" 66 | def __init__(self, name, partition, **kwargs): 67 | try: 68 | super(IcrHTTPSMonitor, self).__init__(name, partition, **kwargs) 69 | except ValueError: 70 | # Need to allow for misconfigured legacy monitors from BIG-IP, 71 | # so let this through 72 | pass 73 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/icmp_monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # 7 | # Copyright (c) 2017-2021 F5 Networks, Inc. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | 22 | import logging 23 | 24 | from f5_cccl.resource.ltm.monitor import Monitor 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | class ICMPMonitor(Monitor): 31 | """Creates a CCCL BIG-IP ICMP Monitor Object of sub-type of Resource 32 | 33 | This object hosts the ability to orchestrate basic CRUD actions against a 34 | BIG-IP ICMP Monitor via the F5-SDK. 35 | 36 | The major difference is the afforded schema for ICMP specifically. 37 | """ 38 | def _uri_path(self, bigip): 39 | """Get the URI resource path key for the F5-SDK for ICMP monitor 40 | 41 | This is the URI reference for an ICMP Monitor. 42 | """ 43 | return bigip.tm.ltm.monitor.gateway_icmps.gateway_icmp 44 | 45 | 46 | class ApiICMPMonitor(ICMPMonitor): 47 | """Create the canonical ICMP monitor from the CCCL API input.""" 48 | pass 49 | 50 | 51 | class IcrICMPMonitor(ICMPMonitor): 52 | """Create the canonical ICMP monitor from the iControl REST response.""" 53 | def __init__(self, name, partition, **kwargs): 54 | try: 55 | super(IcrICMPMonitor, self).__init__(name, partition, **kwargs) 56 | except ValueError: 57 | # Need to allow for misconfigured legacy monitors from BIG-IP, 58 | # so let this through 59 | pass 60 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # 7 | # Copyright (c) 2017-2021 F5 Networks, Inc. 8 | # 9 | # Licensed under the Apache License, Version 2.0 (the "License"); 10 | # you may not use this file except in compliance with the License. 11 | # You may obtain a copy of the License at 12 | # 13 | # http://www.apache.org/licenses/LICENSE-2.0 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | 22 | import logging 23 | 24 | from f5_cccl.resource import Resource 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | class Monitor(Resource): 31 | """Creates a CCCL BIG-IP Monitor Object of sub-type of Resource 32 | 33 | This object hosts the ability to orchestrate basic CRUD actions against a 34 | BIG-IP Monitor via the F5-SDK. 35 | """ 36 | properties = dict(timeout=16, interval=5) 37 | 38 | def __eq__(self, compare): 39 | myself = self._data 40 | 41 | if isinstance(compare, Monitor): 42 | compare = compare.data 43 | 44 | return myself == compare 45 | 46 | def __init__(self, name, partition, **kwargs): 47 | super(Monitor, self).__init__(name, partition) 48 | 49 | for key, value in list(self.properties.items()): 50 | self._data[key] = kwargs.get(key, value) 51 | 52 | # Check for invalid interval/timeout values 53 | if self._data['interval'] >= self._data['timeout']: 54 | raise ValueError( 55 | "Health Monitor interval ({}) must be less than " 56 | "timeout ({})".format(self._data['interval'], 57 | self._data['timeout'])) 58 | 59 | def __str__(self): 60 | return("Monitor(partition: {}, name: {}, type: {})".format( 61 | self._data['partition'], self._data['name'], type(self))) 62 | 63 | def _uri_path(self, bigip): 64 | """Returns the bigip object instance's reference to the monitor object 65 | 66 | This method takes in a bigip and returns the uri reference for managing 67 | the monitor object via the F5-SDK on the BIG-IP 68 | """ 69 | raise NotImplementedError("No default monitor implemented") 70 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/tcp_monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # coding=utf-8 7 | # 8 | # Copyright (c) 2017-2021 F5 Networks, Inc. 9 | # 10 | # Licensed under the Apache License, Version 2.0 (the "License"); 11 | # you may not use this file except in compliance with the License. 12 | # You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software 17 | # distributed under the License is distributed on an "AS IS" BASIS, 18 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | # See the License for the specific language governing permissions and 20 | # limitations under the License. 21 | # 22 | 23 | import logging 24 | 25 | from f5_cccl.resource.ltm.monitor import Monitor 26 | 27 | 28 | LOGGER = logging.getLogger(__name__) 29 | 30 | 31 | class TCPMonitor(Monitor): 32 | """Creates a CCCL BIG-IP TCP Monitor Object of sub-type of Resource 33 | 34 | This object hosts the ability to orchestrate basic CRUD actions against a 35 | BIG-IP TCP Monitor via the F5-SDK. 36 | 37 | The major difference is the afforded schema for TCP specifically. 38 | """ 39 | properties = dict(interval=5, recv="", send="", timeout=16) 40 | 41 | def __init__(self, name, partition, **kwargs): 42 | super(TCPMonitor, self).__init__(name, partition, **kwargs) 43 | for key in ['send', 'recv']: 44 | self._data[key] = kwargs.get(key, self.properties.get(key)) 45 | 46 | def _uri_path(self, bigip): 47 | """Get the URI resource path key for the F5-SDK for TCP monitor 48 | 49 | This is the URI reference for an TCP Monitor. 50 | """ 51 | return bigip.tm.ltm.monitor.tcps.tcp 52 | 53 | 54 | class ApiTCPMonitor(TCPMonitor): 55 | """Create the canonical TCP monitor from API input.""" 56 | pass 57 | 58 | 59 | class IcrTCPMonitor(TCPMonitor): 60 | """Create the canonical TCP monitor from API input.""" 61 | def __init__(self, name, partition, **kwargs): 62 | try: 63 | super(IcrTCPMonitor, self).__init__(name, partition, **kwargs) 64 | except ValueError: 65 | # Need to allow for misconfigured legacy monitors from BIG-IP, 66 | # so let this through 67 | pass 68 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_http_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from mock import MagicMock 18 | import pytest 19 | 20 | import f5_cccl.resource.ltm.monitor.http_monitor as target 21 | 22 | 23 | @pytest.fixture 24 | def http_config(): 25 | return {"name": "test_monitor", 26 | "partition": "Test", 27 | "interval": 1, 28 | "timeout": 10, 29 | "send": "GET /\r\n", 30 | "recv": "SERVER"} 31 | 32 | 33 | @pytest.fixture 34 | def bigip(): 35 | bigip = MagicMock() 36 | return bigip 37 | 38 | 39 | def test_create_w_defaults(http_config): 40 | monitor = target.HTTPMonitor( 41 | name=http_config['name'], 42 | partition=http_config['partition']) 43 | 44 | assert monitor 45 | assert monitor.name == "test_monitor" 46 | assert monitor.partition == "Test" 47 | data = monitor.data 48 | assert data.get('interval') == 5 49 | assert data.get('timeout') == 16 50 | assert data.get('send') == "GET /\\r\\n" 51 | assert data.get('recv') == "" 52 | 53 | def test_create_w_config(http_config): 54 | monitor = target.HTTPMonitor( 55 | **http_config 56 | ) 57 | 58 | assert monitor 59 | assert monitor.name == "test_monitor" 60 | assert monitor.partition == "Test" 61 | data = monitor.data 62 | assert data.get('interval') == 1 63 | assert data.get('timeout') == 10 64 | assert data.get('send') == "GET /\r\n" 65 | assert data.get('recv') == "SERVER" 66 | 67 | def test_get_uri_path(bigip, http_config): 68 | monitor = target.HTTPMonitor(**http_config) 69 | 70 | assert (monitor._uri_path(bigip) == 71 | bigip.tm.ltm.monitor.https.http) 72 | 73 | 74 | def test_create_icr_monitor(http_config): 75 | monitor = target.IcrHTTPMonitor(**http_config) 76 | 77 | assert isinstance(monitor, target.HTTPMonitor) 78 | 79 | 80 | def test_create_api_monitor(http_config): 81 | monitor = target.ApiHTTPMonitor(**http_config) 82 | 83 | assert isinstance(monitor, target.HTTPMonitor) 84 | 85 | 86 | def test_create_monitors_invalid(http_config): 87 | # Set interval to be larger than timeout, 88 | # ICR Monitor will be created, API Monitor will not 89 | http_config['interval'] = 30 90 | monitor = target.IcrHTTPMonitor(**http_config) 91 | 92 | assert isinstance(monitor, target.IcrHTTPMonitor) 93 | 94 | with pytest.raises(ValueError): 95 | monitor = target.ApiHTTPMonitor(**http_config) 96 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_https_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from mock import MagicMock 18 | import pytest 19 | 20 | import f5_cccl.resource.ltm.monitor.https_monitor as target 21 | 22 | 23 | @pytest.fixture 24 | def https_config(): 25 | return {"name": "test_monitor", 26 | "partition": "Test", 27 | "interval": 1, 28 | "timeout": 10, 29 | "send": "GET /\r\n", 30 | "recv": "SERVER"} 31 | 32 | 33 | @pytest.fixture 34 | def bigip(): 35 | bigip = MagicMock() 36 | return bigip 37 | 38 | 39 | def test_create_w_defaults(https_config): 40 | monitor = target.HTTPSMonitor( 41 | name=https_config['name'], 42 | partition=https_config['partition']) 43 | 44 | assert monitor 45 | assert monitor.name == "test_monitor" 46 | assert monitor.partition == "Test" 47 | data = monitor.data 48 | assert data.get('interval') == 5 49 | assert data.get('timeout') == 16 50 | assert data.get('send') == "GET /\\r\\n" 51 | assert data.get('recv') == "" 52 | 53 | 54 | def test_create_w_config(https_config): 55 | monitor = target.HTTPSMonitor( 56 | **https_config 57 | ) 58 | 59 | assert monitor 60 | assert monitor.name == "test_monitor" 61 | assert monitor.partition == "Test" 62 | data = monitor.data 63 | assert data.get('interval') == 1 64 | assert data.get('timeout') == 10 65 | assert data.get('send') == "GET /\r\n" 66 | assert data.get('recv') == "SERVER" 67 | 68 | 69 | def test_get_uri_path(bigip, https_config): 70 | monitor = target.HTTPSMonitor(**https_config) 71 | 72 | assert (monitor._uri_path(bigip) == 73 | bigip.tm.ltm.monitor.https_s.https) 74 | 75 | 76 | def test_create_icr_monitor(https_config): 77 | monitor = target.IcrHTTPSMonitor(**https_config) 78 | 79 | assert isinstance(monitor, target.HTTPSMonitor) 80 | 81 | 82 | def test_create_api_monitor(https_config): 83 | monitor = target.ApiHTTPSMonitor(**https_config) 84 | 85 | assert isinstance(monitor, target.HTTPSMonitor) 86 | 87 | 88 | def test_create_monitors_invalid(https_config): 89 | # Set interval to be larger than timeout, 90 | # ICR Monitor will be created, API Monitor will not 91 | https_config['interval'] = 30 92 | monitor = target.IcrHTTPSMonitor(**https_config) 93 | 94 | assert isinstance(monitor, target.IcrHTTPSMonitor) 95 | 96 | with pytest.raises(ValueError): 97 | monitor = target.ApiHTTPSMonitor(**https_config) 98 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_icmp_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from mock import MagicMock 18 | import pytest 19 | 20 | import f5_cccl.resource.ltm.monitor.icmp_monitor as target 21 | 22 | 23 | @pytest.fixture 24 | def icmp_config(): 25 | return {"name": "test_monitor", 26 | "partition": "Test", 27 | "interval": 1, 28 | "timeout": 10} 29 | 30 | 31 | @pytest.fixture 32 | def bigip(): 33 | bigip = MagicMock() 34 | return bigip 35 | 36 | 37 | def test_create_w_defaults(icmp_config): 38 | monitor = target.ICMPMonitor( 39 | name=icmp_config['name'], 40 | partition=icmp_config['partition']) 41 | 42 | assert monitor 43 | assert monitor.name == "test_monitor" 44 | assert monitor.partition == "Test" 45 | data = monitor.data 46 | assert data.get('interval') == 5 47 | assert data.get('timeout') == 16 48 | 49 | 50 | def test_create_w_config(icmp_config): 51 | monitor = target.ICMPMonitor( 52 | **icmp_config 53 | ) 54 | 55 | assert monitor 56 | assert monitor.name == "test_monitor" 57 | assert monitor.partition == "Test" 58 | data = monitor.data 59 | assert data.get('interval') == 1 60 | assert data.get('timeout') == 10 61 | 62 | 63 | def test_get_uri_path(bigip, icmp_config): 64 | monitor = target.ICMPMonitor(**icmp_config) 65 | 66 | assert (monitor._uri_path(bigip) == 67 | bigip.tm.ltm.monitor.gateway_icmps.gateway_icmp) 68 | 69 | 70 | def test_create_icr_monitor(icmp_config): 71 | monitor = target.IcrICMPMonitor(**icmp_config) 72 | 73 | assert isinstance(monitor, target.ICMPMonitor) 74 | 75 | 76 | def test_create_api_monitor(icmp_config): 77 | monitor = target.ApiICMPMonitor(**icmp_config) 78 | 79 | assert isinstance(monitor, target.ICMPMonitor) 80 | 81 | 82 | def test_create_monitors_invalid(icmp_config): 83 | # Set interval to be larger than timeout, 84 | # ICR Monitor will be created, API Monitor will not 85 | icmp_config['interval'] = 30 86 | monitor = target.IcrICMPMonitor(**icmp_config) 87 | 88 | assert isinstance(monitor, target.IcrICMPMonitor) 89 | 90 | with pytest.raises(ValueError): 91 | monitor = target.ApiICMPMonitor(**icmp_config) 92 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import pytest 18 | 19 | from mock import MagicMock 20 | 21 | import f5_cccl.exceptions as exceptions 22 | import f5_cccl.resource.ltm.monitor.monitor as target 23 | 24 | 25 | api_monitors_cfg = [ 26 | { "name": "myhttp", 27 | "type": "http", 28 | "send": "GET /\r\n", 29 | "recv": "SERVER" }, 30 | { "name": "my_ping", 31 | "type": "icmp" }, 32 | { "name": "my_tcp", 33 | "type": "tcp" }, 34 | { "name": "myhttp", 35 | "type": "https", 36 | "send": "GET /\r\n", 37 | "recv": "HTTPS-SERVER" } 38 | ] 39 | 40 | 41 | name = "test_monitor" 42 | partition = "Test" 43 | 44 | 45 | @pytest.fixture 46 | def http_monitor(): 47 | return api_monitors_cfg[0] 48 | 49 | 50 | @pytest.fixture 51 | def icmp_monitor(): 52 | return api_monitors_cfg[1] 53 | 54 | 55 | @pytest.fixture 56 | def tcp_monitor(): 57 | return api_monitors_cfg[2] 58 | 59 | 60 | @pytest.fixture 61 | def https_monitor(): 62 | return api_monitors_cfg[3] 63 | 64 | 65 | def test__eq__(): 66 | monitor1 = target.Monitor(name=name, partition=partition) 67 | monitor2 = target.Monitor(name=name, partition=partition) 68 | 69 | assert monitor1 == monitor2 70 | 71 | 72 | def test__init__(): 73 | 74 | monitor = target.Monitor(name=name, partition=partition) 75 | assert monitor 76 | 77 | monitor_data = monitor.data 78 | assert monitor_data 79 | assert monitor_data['interval'] == 5 80 | assert monitor_data['timeout'] == 16 81 | assert not monitor_data.get('send', None) 82 | assert not monitor_data.get('recv', None) 83 | 84 | 85 | def test__init__xtra_params(): 86 | properties = {'foo': 'xtra1', 'send': "GET /\r\n"} 87 | 88 | monitor = target.Monitor(name=name, 89 | partition=partition, 90 | **properties) 91 | assert monitor 92 | 93 | monitor_data = monitor.data 94 | assert monitor_data 95 | assert monitor_data['interval'] == 5 96 | assert monitor_data['timeout'] == 16 97 | assert monitor_data.get('send',"GET /\r\n") 98 | assert not monitor_data.get('foo', None) 99 | 100 | 101 | def test__str__(): 102 | monitor = target.Monitor(name=name, 103 | partition=partition) 104 | class_str = "" 105 | assert str(monitor) == ( 106 | "Monitor(partition: Test, name: test_monitor, type: {})".format( 107 | class_str)) 108 | 109 | 110 | def test_uri_path(): 111 | monitor = target.Monitor(name=name, 112 | partition=partition) 113 | with pytest.raises(NotImplementedError): 114 | monitor._uri_path(MagicMock()) 115 | 116 | 117 | def test_invalid_interval_and_timeout(): 118 | monitors = list(api_monitors_cfg) 119 | for mon in monitors: 120 | mon['interval'] = 10 121 | mon['timeout'] = 5 122 | with pytest.raises(ValueError): 123 | monitor = target.Monitor(partition=partition, **mon) 124 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_tcp_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from mock import MagicMock 18 | import pytest 19 | 20 | import f5_cccl.resource.ltm.monitor.tcp_monitor as target 21 | 22 | 23 | @pytest.fixture 24 | def tcp_config(): 25 | return {"name": "test_monitor", 26 | "partition": "Test", 27 | "interval": 1, 28 | "timeout": 10, 29 | "send": "GET /\r\n", 30 | "recv": "SERVER"} 31 | 32 | 33 | @pytest.fixture 34 | def bigip(): 35 | bigip = MagicMock() 36 | return bigip 37 | 38 | 39 | def test_create_w_defaults(tcp_config): 40 | monitor = target.TCPMonitor( 41 | name=tcp_config['name'], 42 | partition=tcp_config['partition']) 43 | 44 | assert monitor 45 | assert monitor.name == "test_monitor" 46 | assert monitor.partition == "Test" 47 | data = monitor.data 48 | assert data.get('interval') == 5 49 | assert data.get('timeout') == 16 50 | assert data.get('send') == "" 51 | assert data.get('recv') == "" 52 | 53 | def test_create_w_config(tcp_config): 54 | monitor = target.TCPMonitor( 55 | **tcp_config 56 | ) 57 | 58 | assert monitor 59 | assert monitor.name == "test_monitor" 60 | assert monitor.partition == "Test" 61 | data = monitor.data 62 | assert data.get('interval') == 1 63 | assert data.get('timeout') == 10 64 | assert data.get('send') == "GET /\r\n" 65 | assert data.get('recv') == "SERVER" 66 | 67 | 68 | def test_get_uri_path(bigip, tcp_config): 69 | monitor = target.TCPMonitor(**tcp_config) 70 | 71 | assert (monitor._uri_path(bigip) == 72 | bigip.tm.ltm.monitor.tcps.tcp) 73 | 74 | 75 | def test_create_icr_monitor(tcp_config): 76 | monitor = target.IcrTCPMonitor(**tcp_config) 77 | 78 | assert isinstance(monitor, target.TCPMonitor) 79 | 80 | 81 | def test_create_api_monitor(tcp_config): 82 | monitor = target.ApiTCPMonitor(**tcp_config) 83 | 84 | assert isinstance(monitor, target.TCPMonitor) 85 | 86 | 87 | def test_create_monitors_invalid(tcp_config): 88 | # Set interval to be larger than timeout, 89 | # ICR Monitor will be created, API Monitor will not 90 | tcp_config['interval'] = 30 91 | monitor = target.IcrTCPMonitor(**tcp_config) 92 | 93 | assert isinstance(monitor, target.IcrTCPMonitor) 94 | 95 | with pytest.raises(ValueError): 96 | monitor = target.ApiTCPMonitor(**tcp_config) 97 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/test/test_udp_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from mock import MagicMock 18 | import pytest 19 | 20 | import f5_cccl.resource.ltm.monitor.udp_monitor as target 21 | 22 | 23 | @pytest.fixture 24 | def udp_config(): 25 | return {"name": "test_monitor", 26 | "partition": "Test", 27 | "interval": 1, 28 | "timeout": 10, 29 | "send": "GET /\r\n", 30 | "recv": "SERVER"} 31 | 32 | 33 | @pytest.fixture 34 | def bigip(): 35 | bigip = MagicMock() 36 | return bigip 37 | 38 | 39 | def test_create_w_defaults(udp_config): 40 | monitor = target.UDPMonitor( 41 | name=udp_config['name'], 42 | partition=udp_config['partition']) 43 | 44 | assert monitor 45 | assert monitor.name == "test_monitor" 46 | assert monitor.partition == "Test" 47 | data = monitor.data 48 | assert data.get('interval') == 5 49 | assert data.get('timeout') == 16 50 | assert data.get('send') == "" 51 | assert data.get('recv') == "" 52 | 53 | def test_create_w_config(udp_config): 54 | monitor = target.UDPMonitor( 55 | **udp_config 56 | ) 57 | 58 | assert monitor 59 | assert monitor.name == "test_monitor" 60 | assert monitor.partition == "Test" 61 | data = monitor.data 62 | assert data.get('interval') == 1 63 | assert data.get('timeout') == 10 64 | assert data.get('send') == "GET /\r\n" 65 | assert data.get('recv') == "SERVER" 66 | 67 | 68 | def test_get_uri_path(bigip, udp_config): 69 | monitor = target.UDPMonitor(**udp_config) 70 | 71 | assert (monitor._uri_path(bigip) == 72 | bigip.tm.ltm.monitor.udps.udp) 73 | 74 | 75 | def test_create_icr_monitor(udp_config): 76 | monitor = target.IcrUDPMonitor(**udp_config) 77 | 78 | assert isinstance(monitor, target.UDPMonitor) 79 | 80 | 81 | def test_create_api_monitor(udp_config): 82 | monitor = target.ApiUDPMonitor(**udp_config) 83 | 84 | assert isinstance(monitor, target.UDPMonitor) 85 | 86 | 87 | def test_create_monitors_invalid(udp_config): 88 | # Set interval to be larger than timeout, 89 | # ICR Monitor will be created, API Monitor will not 90 | udp_config['interval'] = 30 91 | monitor = target.IcrUDPMonitor(**udp_config) 92 | 93 | assert isinstance(monitor, target.IcrUDPMonitor) 94 | 95 | with pytest.raises(ValueError): 96 | monitor = target.ApiUDPMonitor(**udp_config) 97 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/monitor/udp_monitor.py: -------------------------------------------------------------------------------- 1 | """Hosts an interface for the BIG-IP Monitor Resource. 2 | 3 | This module references and holds items relevant to the orchestration of the F5 4 | BIG-IP for purposes of abstracting the F5-SDK library. 5 | """ 6 | # coding=utf-8 7 | # 8 | # Copyright (c) 2017-2021 F5 Networks, Inc. 9 | # 10 | # Licensed under the Apache License, Version 2.0 (the "License"); 11 | # you may not use this file except in compliance with the License. 12 | # You may obtain a copy of the License at 13 | # 14 | # http://www.apache.org/licenses/LICENSE-2.0 15 | # 16 | # Unless required by applicable law or agreed to in writing, software 17 | # distributed under the License is distributed on an "AS IS" BASIS, 18 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | # See the License for the specific language governing permissions and 20 | # limitations under the License. 21 | # 22 | 23 | import logging 24 | 25 | from f5_cccl.resource.ltm.monitor import Monitor 26 | 27 | 28 | LOGGER = logging.getLogger(__name__) 29 | 30 | 31 | class UDPMonitor(Monitor): 32 | """Creates a CCCL BIG-IP UDP Monitor Object of sub-type of Resource 33 | 34 | This object hosts the ability to orchestrate basic CRUD actions against a 35 | BIG-IP UDP Monitor via the F5-SDK. 36 | 37 | The major difference is the afforded schema for UDP specifically. 38 | """ 39 | properties = dict(interval=5, recv="", send="", timeout=16) 40 | 41 | def __init__(self, name, partition, **kwargs): 42 | super(UDPMonitor, self).__init__(name, partition, **kwargs) 43 | for key in ['send', 'recv']: 44 | self._data[key] = kwargs.get(key, self.properties.get(key)) 45 | 46 | def _uri_path(self, bigip): 47 | """Get the URI resource path key for the F5-SDK for UDP monitor 48 | 49 | This is the URI reference for an UDP Monitor. 50 | """ 51 | return bigip.tm.ltm.monitor.udps.udp 52 | 53 | 54 | class ApiUDPMonitor(UDPMonitor): 55 | """Create the canonical UDP monitor from API input.""" 56 | pass 57 | 58 | 59 | class IcrUDPMonitor(UDPMonitor): 60 | """Create the canonical UDP monitor from API input.""" 61 | def __init__(self, name, partition, **kwargs): 62 | try: 63 | super(IcrUDPMonitor, self).__init__(name, partition, **kwargs) 64 | except ValueError: 65 | # Need to allow for misconfigured legacy monitors from BIG-IP, 66 | # so let this through 67 | pass 68 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/node.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP Node resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from copy import deepcopy 20 | import logging 21 | 22 | from f5_cccl.resource import Resource 23 | from f5_cccl.utils.route_domain import normalize_address_with_route_domain 24 | 25 | 26 | LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | class Node(Resource): 30 | """Node class for managing configuration on BIG-IP.""" 31 | 32 | properties = dict(name=None, 33 | partition=None, 34 | address=None, 35 | state=None, 36 | session=None) 37 | 38 | def __init__(self, name, partition, **properties): 39 | """Create a Node instance.""" 40 | super(Node, self).__init__(name, partition, **properties) 41 | 42 | for key, value in list(self.properties.items()): 43 | if key in ["name", "partition"]: 44 | continue 45 | 46 | self._data[key] = properties.get(key, value) 47 | 48 | def __eq__(self, other): 49 | if not isinstance(other, Node): 50 | LOGGER.warning( 51 | "Invalid comparison of Node object with object " 52 | "of type %s", type(other)) 53 | return False 54 | 55 | if self.name != other.name: 56 | return False 57 | if self.partition != other.partition: 58 | return False 59 | if self._data['address'] != other.data['address']: 60 | return False 61 | 62 | # Check equivalence of states 63 | if other.data['state'] == 'up' or other.data['state'] == 'unchecked': 64 | if 'enabled' in other.data['session']: 65 | return True 66 | return False 67 | 68 | def __hash__(self): # pylint: disable=useless-super-delegation 69 | return super(Node, self).__hash__() 70 | 71 | def _uri_path(self, bigip): 72 | return bigip.tm.ltm.nodes.node 73 | 74 | def update(self, bigip, data=None, modify=False): 75 | # 'address' is immutable, don't pass it in an update operation 76 | tmp_data = deepcopy(data) if data is not None else deepcopy(self.data) 77 | tmp_data.pop('address', None) 78 | super(Node, self).update(bigip, data=tmp_data, modify=modify) 79 | 80 | 81 | class ApiNode(Node): 82 | """Synthesize the CCCL input to create the canonical Node.""" 83 | def __init__(self, name, partition, default_route_domain, **properties): 84 | # The expected node should have route domain as part of name 85 | name = normalize_address_with_route_domain( 86 | properties.get('address'), default_route_domain)[0] 87 | super(ApiNode, self).__init__(name, partition, **properties) 88 | 89 | 90 | class IcrNode(Node): 91 | """Node instantiated from iControl REST pool member object.""" 92 | def __init__(self, name, partition, default_route_domain, **properties): 93 | # The address from the BigIP needs the route domain added if it 94 | # happens to match the default for the partition 95 | properties['address'] = normalize_address_with_route_domain( 96 | properties.get('address'), default_route_domain)[0] 97 | super(IcrNode, self).__init__(name, partition, **properties) 98 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2021 F5 Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | """This module implements the F5 CCCL Resource super class.""" 16 | 17 | # Ignore import but unused flake error 18 | # flake8: noqa 19 | 20 | from .action import Action 21 | from .condition import Condition 22 | 23 | from .policy import Policy 24 | from .policy import IcrPolicy 25 | from .policy import ApiPolicy 26 | 27 | from .rule import Rule 28 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/action.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP L7 Rule Action resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | 20 | 21 | import logging 22 | 23 | from f5_cccl.resource import Resource 24 | 25 | 26 | LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | class Action(Resource): 30 | """L7 Rule Action class.""" 31 | # The property names class attribute defines the names of the 32 | # properties that we wish to compare. 33 | properties = dict( 34 | expression=None, 35 | forward=False, 36 | location=None, 37 | pool=None, 38 | redirect=False, 39 | request=True, 40 | reset=False, 41 | setVariable=False, 42 | tcl=False, 43 | tmName=None, 44 | httpHost=False, 45 | httpUri=False, 46 | path=None, 47 | replace=False, 48 | value=None, 49 | shutdown=True, 50 | select=True, 51 | ) 52 | 53 | def __init__(self, name, data): 54 | """Initialize the Action object. 55 | 56 | Actions do not have explicit partition attributes, the are 57 | implied by the partition of the rule to which they belong. 58 | """ 59 | super(Action, self).__init__(name, partition=None) 60 | 61 | # Actions are Only supported on requests. 62 | self._data['request'] = True 63 | 64 | # Is this a forwarding action? 65 | if data.get('forward', False): 66 | 67 | self._data['forward'] = True 68 | 69 | # Yes, there are two supported forwarding actions: 70 | # forward to pool and reset, these are mutually 71 | # exclusive options. 72 | pool = data.get('pool', None) 73 | reset = data.get('reset', False) 74 | 75 | # This allows you to specify an empty node. This is 76 | # what Container Connector does. 77 | select = data.get('select', False) 78 | 79 | # This was added in 13.1.0 80 | shutdown = data.get('shutdown', False) 81 | if pool: 82 | self._data['pool'] = pool 83 | elif reset: 84 | self._data['reset'] = reset 85 | elif select: 86 | self._data['select'] = select 87 | elif shutdown: 88 | self._data['shutdown'] = shutdown 89 | else: 90 | raise ValueError( 91 | "Unsupported forward action, must be one of reset, " 92 | "forward to pool, select, or shutdown.") 93 | # Is this a redirect action? 94 | elif data.get('redirect', False): 95 | self._data['redirect'] = True 96 | 97 | # Yes, set the location and httpReply attribute 98 | self._data['location'] = data.get('location', None) 99 | self._data['httpReply'] = data.get('httpReply', True) 100 | # Is this a setVariable action? 101 | elif data.get('setVariable', False): 102 | self._data['setVariable'] = True 103 | 104 | # Set the variable name and the value 105 | self._data['tmName'] = data.get('tmName', None) 106 | self._data['expression'] = data.get('expression', None) 107 | self._data['tcl'] = True 108 | # Is this a replace URI host action? 109 | elif data.get('replace', False) and data.get('httpHost', False): 110 | self._data['replace'] = True 111 | self._data['httpHost'] = True 112 | self._data['value'] = data.get('value', None) 113 | # Is this a replace URI path action? 114 | elif data.get('replace', False) and data.get('httpUri', False) and \ 115 | data.get('path', False): 116 | self._data['replace'] = True 117 | self._data['httpUri'] = True 118 | self._data['path'] = data.get('path', None) 119 | self._data['value'] = data.get('value', None) 120 | # Is this a replace URI action? 121 | elif data.get('replace', False) and data.get('httpUri', False): 122 | self._data['replace'] = True 123 | self._data['httpUri'] = True 124 | self._data['value'] = data.get('value', None) 125 | else: 126 | # Only forward, redirect and setVariable are supported. 127 | raise ValueError("Unsupported action, must be one of forward, " 128 | "redirect, setVariable, replace, or reset.") 129 | 130 | def __eq__(self, other): 131 | """Check the equality of the two objects. 132 | 133 | Do a straight data to data comparison. 134 | """ 135 | if not isinstance(other, Action): 136 | return False 137 | 138 | return super(Action, self).__eq__(other) 139 | 140 | def __str__(self): 141 | return str(self._data) 142 | 143 | def _uri_path(self, bigip): 144 | """Return the URI path of an action object. 145 | 146 | Not implemented because the current implementation does 147 | not manage Actions individually.""" 148 | raise NotImplementedError 149 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/condition.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP L7 Rule Action resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | 20 | 21 | import logging 22 | 23 | from f5_cccl.resource import Resource 24 | 25 | 26 | LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | class Condition(Resource): 30 | """L7 Rule Condition class.""" 31 | # The property names class attribute defines the names of the 32 | # properties that we wish to compare. 33 | properties = { 34 | "name": None, 35 | "request": True, 36 | 37 | "equals": None, 38 | "endsWith": None, 39 | "startsWith": None, 40 | "contains": None, 41 | "matches": None, 42 | 43 | "not": None, 44 | "missing": None, 45 | "caseSensitive": None, 46 | 47 | "httpHost": False, 48 | "host": False, 49 | 50 | "httpUri": False, 51 | "pathSegment": False, 52 | "path": False, 53 | "extension": False, 54 | "index": None, 55 | 56 | "httpHeader": False, 57 | "httpCookie": False, 58 | 59 | "tcp": True, 60 | "address": False, 61 | 62 | "tmName": None, 63 | "values": list() 64 | } 65 | 66 | def __init__(self, name, data): 67 | super(Condition, self).__init__(name, partition=None) 68 | 69 | self._data['request'] = True 70 | 71 | values = sorted(data.get('values', list())) 72 | tm_name = data.get('tmName', None) 73 | 74 | # Does this rule match the HTTP hostname? 75 | if data.get('httpHost', False): 76 | condition_map = {'httpHost': True, 'host': True, 'values': values} 77 | 78 | # Does this rule match a part of the HTTP URI? 79 | elif data.get('httpUri', False): 80 | condition_map = {'httpUri': True, 'values': values} 81 | if data.get('path', False): 82 | condition_map['path'] = True 83 | elif data.get('pathSegment', False): 84 | condition_map['pathSegment'] = True 85 | condition_map['index'] = data.get('index', 1) 86 | elif data.get('extension', False): 87 | condition_map['extension'] = True 88 | elif data.get('host', False): 89 | condition_map['host'] = True 90 | else: 91 | raise ValueError("must specify one of host path, pathSegment, " 92 | "or extension for HTTP URI matching " 93 | "condition") 94 | 95 | # Does this rule match an HTTP header? 96 | elif data.get('httpHeader', False): 97 | condition_map = { 98 | 'httpHeader': True, 'tmName': tm_name, 'values': values} 99 | 100 | # Does this rule match an HTTP cookie? 101 | elif data.get('httpCookie', False): 102 | condition_map = { 103 | 'httpCookie': True, 'tmName': tm_name, 'values': values} 104 | 105 | # Does this rule match a TCP related setting? 106 | elif data.get('tcp', False): 107 | condition_map = {'tcp': True, 'values': values} 108 | 109 | if data.get('external', False): 110 | condition_map['external'] = True 111 | elif data.get('internal', False): 112 | condition_map['internal'] = True 113 | 114 | if data.get('matches', False): 115 | condition_map['matches'] = True 116 | 117 | if data.get('address', False): 118 | condition_map['address'] = True 119 | else: 120 | raise ValueError("must specify address for TCP matching " 121 | "condition") 122 | else: 123 | # This class does not support the condition type; however, 124 | # we want to create in order to manage the policy. 125 | raise ValueError("Invalid match type must be one of: httpHost, " 126 | "httpUri, httpHeader, or httpCookie") 127 | 128 | self._data.update(condition_map) 129 | 130 | # This condition attributes should not be set if they are not defined. 131 | # For example, having a comparison option set to 'None' will conflict 132 | # with the one that is set to 'True' 133 | match_options = ['not', 'missing', 'caseSensitive'] 134 | comparisons = [ 135 | 'contains', 'equals', 'startsWith', 'endsWith', 'matches' 136 | ] 137 | for key in match_options + comparisons: 138 | value = data.get(key, None) 139 | if value: 140 | self._data[key] = value 141 | 142 | def __eq__(self, other): 143 | """Check the equality of the two objects. 144 | 145 | Do a data to data comparison as implemented in Resource. 146 | """ 147 | if not isinstance(other, Condition): 148 | return False 149 | 150 | return super(Condition, self).__eq__(other) 151 | 152 | def __str__(self): 153 | return str(self._data) 154 | 155 | def _uri_path(self, bigip): 156 | """Return the URI path of an rule object. 157 | 158 | Not implemented because the current implementation does 159 | not manage Rules individually.""" 160 | raise NotImplementedError 161 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/rule.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP L7 Rule resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from functools import total_ordering 20 | 21 | 22 | import logging 23 | 24 | from f5_cccl.resource import Resource 25 | from f5_cccl.resource.ltm.policy.action import Action 26 | from f5_cccl.resource.ltm.policy.condition import Condition 27 | 28 | 29 | LOGGER = logging.getLogger(__name__) 30 | 31 | 32 | @total_ordering 33 | class Rule(Resource): 34 | """L7 Rule class""" 35 | # The property names class attribute defines the names of the 36 | # properties that we wish to compare. 37 | properties = dict( 38 | name=None, 39 | ordinal=None, 40 | actions=None, 41 | conditions=None, 42 | description=None 43 | ) 44 | 45 | def __init__(self, name, **data): 46 | """Create a Rule object. 47 | 48 | actions and conditions attributes are guaranteed to 49 | be initialized, if non exist, they will be empty 50 | lists. 51 | """ 52 | super(Rule, self).__init__(name, '') 53 | self._data['description'] = data.get('description', None) 54 | self._data['ordinal'] = data.get('ordinal', 0) 55 | self._data['actions'] = self._create_actions( 56 | data.get('actions', list())) 57 | self._data['conditions'] = self._create_conditions( 58 | data.get('conditions', list())) 59 | 60 | def __eq__(self, other): 61 | """Check the equality of the two objects. 62 | 63 | Only compare the properties as defined in the 64 | properties class dictionary. 65 | """ 66 | if not isinstance(other, Rule): 67 | return False 68 | 69 | for key in self.properties: 70 | if key in ['actions', 'conditions']: 71 | if len(self._data[key]) != len(other.data[key]): 72 | return False 73 | for index, obj in enumerate(self._data[key]): 74 | if obj != other.data[key][index]: 75 | return False 76 | continue 77 | if self._data[key] != other.data.get(key, None): 78 | return False 79 | 80 | return True 81 | 82 | def __str__(self): 83 | return str(self._data) 84 | 85 | def __lt__(self, other): 86 | """Rich comparison function for sorting Rules.""" 87 | return self._data['ordinal'] < other.data['ordinal'] 88 | 89 | def _create_actions(self, actions): 90 | """Return a new list of Actions data in sorted order. 91 | 92 | The order of the list of actions is interpretted as 93 | the order in which they should be applied. 94 | """ 95 | new_actions = list() 96 | 97 | unsupported_actions = 0 98 | for index, action in enumerate(actions): 99 | name = "{}".format(index - unsupported_actions) 100 | try: 101 | new_actions.append(Action(name, action)) 102 | except ValueError as e: 103 | LOGGER.warning( 104 | "Create actions: Caught ValueError: %s", str(e)) 105 | unsupported_actions += 1 106 | 107 | return [action.data for action in sorted(new_actions)] 108 | 109 | def _create_conditions(self, conditions): 110 | """Return a new list of Conditions data in sorted order. 111 | 112 | The order of the list of actions is interpretted as 113 | the order in which they should be evaluated. 114 | """ 115 | new_conditions = list() 116 | 117 | unsupported_conditions = 0 118 | for index, condition in enumerate(conditions): 119 | name = "{}".format(index - unsupported_conditions) 120 | try: 121 | new_conditions.append(Condition(name, condition)) 122 | except ValueError as e: 123 | LOGGER.warning( 124 | "Create conditions: Caught ValueError: %s", str(e)) 125 | unsupported_conditions += 1 126 | 127 | return [condition.data for condition in sorted(new_conditions)] 128 | 129 | def _uri_path(self, bigip): 130 | raise NotImplementedError 131 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/test/bigip_policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": "legacy", 3 | "generation": 2600, 4 | "metadata": [], 5 | "rulesReference": { 6 | "isSubcollection": true, 7 | "link": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules?ver=12.1.1", 8 | "items": [ 9 | { 10 | "actionsReference": { 11 | "isSubcollection": true, 12 | "link": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule1/actions?ver=12.1.1", 13 | "items": [ 14 | { 15 | "status": 0, 16 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule1/actions/0?ver=12.1.1", 17 | "kind": "tm:ltm:policy:rules:actions:actionsstate", 18 | "code": 0, 19 | "expirySecs": 0, 20 | "generation": 2600, 21 | "request": true, 22 | "vlanId": 0, 23 | "length": 0, 24 | "poolReference": { 25 | "link": "https://localhost/mgmt/tm/ltm/pool/~Test~pool1?ver=12.1.1" 26 | }, 27 | "select": true, 28 | "timeout": 0, 29 | "offset": 0, 30 | "forward": true, 31 | "fullPath": "0", 32 | "port": 0, 33 | "pool": "/Test/pool1", 34 | "name": "0" 35 | } 36 | ] 37 | }, 38 | "ordinal": 0, 39 | "kind": "tm:ltm:policy:rules:rulesstate", 40 | "name": "my_rule1", 41 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule1?ver=12.1.1", 42 | "generation": 2600, 43 | "fullPath": "my_rule1", 44 | "conditionsReference": { 45 | "isSubcollection": true, 46 | "link": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule1/conditions?ver=12.1.1", 47 | "items": [ 48 | { 49 | "index": 0, 50 | "all": true, 51 | "caseInsensitive": true, 52 | "name": "0", 53 | "generation": 2600, 54 | "contains": true, 55 | "request": true, 56 | "kind": "tm:ltm:policy:rules:conditions:conditionsstate", 57 | "tmName": "X-Header", 58 | "values": [ 59 | "openstack", 60 | "velcro" 61 | ], 62 | "external": true, 63 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule1/conditions/0?ver=12.1.1", 64 | "remote": true, 65 | "fullPath": "0", 66 | "httpHeader": true, 67 | "present": true 68 | } 69 | ] 70 | } 71 | }, 72 | { 73 | "actionsReference": { 74 | "isSubcollection": true, 75 | "link": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule2/actions?ver=12.1.1", 76 | "items": [ 77 | { 78 | "reset": true, 79 | "status": 0, 80 | "kind": "tm:ltm:policy:rules:actions:actionsstate", 81 | "code": 0, 82 | "expirySecs": 0, 83 | "generation": 2600, 84 | "request": true, 85 | "vlanId": 0, 86 | "length": 0, 87 | "timeout": 0, 88 | "offset": 0, 89 | "forward": true, 90 | "fullPath": "0", 91 | "port": 0, 92 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule2/actions/0?ver=12.1.1", 93 | "name": "0" 94 | } 95 | ] 96 | }, 97 | "ordinal": 1, 98 | "kind": "tm:ltm:policy:rules:rulesstate", 99 | "name": "my_rule2", 100 | "generation": 2600, 101 | "fullPath": "my_rule2", 102 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy/rules/~Test~my_rule2?ver=12.1.1" 103 | } 104 | ] 105 | }, 106 | "references": {}, 107 | "fullPath": "/Test/wrapper_policy", 108 | "kind": "tm:ltm:policy:policystate", 109 | "name": "wrapper_policy", 110 | "lastModified": "2017-06-14T13:53:28Z", 111 | "partition": "Test", 112 | "controls": [ 113 | "forwarding" 114 | ], 115 | "strategy": "/Common/first-match", 116 | "_meta_data": { 117 | "attribute_registry": {}, 118 | "reduction_forcing_pairs": [ 119 | [ 120 | "enabled", 121 | "disabled" 122 | ], 123 | [ 124 | "online", 125 | "offline" 126 | ], 127 | [ 128 | "vlansEnabled", 129 | "vlansDisabled" 130 | ] 131 | ], 132 | "container": null, 133 | "exclusive_attributes": [], 134 | "allowed_commands": [], 135 | "read_only_attributes": [], 136 | "allowed_lazy_attributes": [], 137 | "uri": "https://10.1.0.170:443/mgmt/tm/ltm/policy/~Test~wrapper_policy/", 138 | "required_json_kind": "tm:ltm:policy:policystate", 139 | "bigip": null, 140 | "icontrol_version": "", 141 | "required_command_parameters": null, 142 | "icr_session": null, 143 | "required_load_parameters": null, 144 | "required_creation_parameters": null, 145 | "creation_uri_frag": "", 146 | "object_has_stats": true, 147 | "creation_uri_qargs": { 148 | "ver": [ 149 | "12.1.1" 150 | ] 151 | }, 152 | "minimum_version": "11.5.0" 153 | }, 154 | "requires": [ 155 | "http" 156 | ], 157 | "selfLink": "https://localhost/mgmt/tm/ltm/policy/~Test~wrapper_policy?ver=12.1.1", 158 | "strategyReference": { 159 | "link": "https://localhost/mgmt/tm/ltm/policy-strategy/~Common~first-match?ver=12.1.1" 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/test/test_policy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import deepcopy 18 | import os 19 | 20 | import json 21 | from mock import Mock 22 | from pprint import pprint as pp 23 | import pytest 24 | 25 | from f5_cccl.resource.ltm.policy import IcrPolicy 26 | from f5_cccl.resource.ltm.policy import Policy 27 | 28 | 29 | @pytest.fixture 30 | def bigip(): 31 | bigip = Mock() 32 | return bigip 33 | 34 | 35 | @pytest.fixture 36 | def icr_policy_dict(): 37 | current_dir = os.path.dirname(os.path.abspath(__file__)) 38 | policy_file = os.path.join(current_dir, "bigip_policy.json") 39 | with open(policy_file, "r") as fp: 40 | json_data = fp.read() 41 | 42 | return json.loads(json_data) 43 | 44 | 45 | @pytest.fixture 46 | def api_policy(): 47 | test_policy = { 48 | 'name': "wrapper_policy", 49 | 'strategy': "/Common/first-match", 50 | 'rules': [{ 51 | 'name': "my_rule1", 52 | 'actions': [{ 53 | 'pool': "/Test/pool1", 54 | 'forward': True, 55 | 'request': True 56 | }], 57 | 'conditions': [{ 58 | "httpHeader": True, 59 | "contains": True, 60 | "tmName": "X-Header", 61 | "values": ["openstack", "velcro"] 62 | }] 63 | }, 64 | { 65 | 'name': "my_rule2", 66 | 'actions': [{'reset': True, 'forward': True}], 67 | 'conditions': [] 68 | }] 69 | } 70 | 71 | return Policy(partition="Test", **test_policy) 72 | 73 | 74 | @pytest.fixture 75 | def policy_0(): 76 | data = { 77 | 'name': "my_policy", 78 | 'partition': "Test", 79 | 'strategy': "/Common/first-match", 80 | 'rules': [], 81 | 'metadata': [] 82 | } 83 | return Policy(**data) 84 | 85 | 86 | def test_create_policy(): 87 | data = { 88 | 'name': "my_policy", 89 | 'partition': "Test", 90 | 'strategy': "/Common/first-match", 91 | 'rules': [], 92 | 'metadata': [] 93 | } 94 | policy = Policy(**data) 95 | 96 | assert policy.name == "my_policy" 97 | assert policy.partition == "Test" 98 | 99 | assert policy.data.get('strategy') == "/Common/first-match" 100 | assert len(policy.data.get('rules')) == 0 101 | assert policy.data.get('legacy') 102 | assert policy.data.get('controls') == ["forwarding"] 103 | assert policy.data.get('requires') == ["http"] 104 | 105 | rules = {'name': "test_rule", 106 | 'actions': [], 107 | 'conditions': []} 108 | data['rules'].append(rules) 109 | 110 | policy = Policy(**data) 111 | assert policy.name == "my_policy" 112 | assert policy.partition == "Test" 113 | 114 | assert policy.data.get('strategy') == "/Common/first-match" 115 | assert len(policy.data.get('rules')) == 1 116 | assert policy.data.get('legacy') 117 | assert policy.data.get('controls') == ["forwarding"] 118 | assert policy.data.get('requires') == ["http"] 119 | 120 | 121 | def test_uri_path(bigip, policy_0): 122 | assert (policy_0._uri_path(bigip) == 123 | bigip.tm.ltm.policys.policy) 124 | 125 | 126 | def test_compare_policy(policy_0): 127 | 128 | policy_1 = deepcopy(policy_0) 129 | 130 | assert policy_0 == policy_1 131 | 132 | rules = {'name': "test_rule", 133 | 'actions': [], 134 | 'conditions': []} 135 | policy_0.data['rules'].append(rules) 136 | 137 | assert policy_0 != policy_1 138 | 139 | rules = {'name': "prod_rule", 140 | 'actions': [], 141 | 'conditions': []} 142 | policy_1.data['rules'].append(rules) 143 | 144 | assert policy_0 != policy_1 145 | 146 | policy_2 = deepcopy(policy_0) 147 | assert policy_0 == policy_2 148 | 149 | policy_2.data['name'] = "your_policy" 150 | assert not policy_0 == policy_2 151 | 152 | def test_compare_policy_w_dict(policy_0): 153 | data = { 154 | 'name': "my_policy", 155 | 'partition': "Test", 156 | 'strategy': "/Common/first-match", 157 | 'rules': [] 158 | } 159 | assert policy_0 != data 160 | 161 | 162 | def test_tostring(policy_0): 163 | assert str(policy_0) != "" 164 | 165 | 166 | def test_create_policy_from_bigip(icr_policy_dict): 167 | policy = IcrPolicy(**icr_policy_dict) 168 | 169 | assert policy.name == "wrapper_policy" 170 | assert policy.partition == "Test" 171 | 172 | data = policy.data 173 | assert data.get('strategy') == "/Common/first-match" 174 | assert len(data.get('rules')) == 2 175 | assert data.get('legacy') 176 | assert data.get('controls') == ["forwarding"] 177 | assert data.get('requires') == ["http"] 178 | 179 | 180 | def test_compare_icr_to_api_policy(icr_policy_dict, api_policy): 181 | icr_policy = IcrPolicy(**icr_policy_dict) 182 | assert icr_policy == api_policy 183 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/policy/test/test_rule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from f5_cccl.resource.ltm.policy import Rule 18 | from mock import Mock 19 | import pytest 20 | 21 | 22 | @pytest.fixture 23 | def bigip(): 24 | bigip = Mock() 25 | return bigip 26 | 27 | 28 | action_0 = { 29 | "request": True, 30 | "redirect": True, 31 | "location": "http://boulder-dev.f5.com", 32 | "httpReply": True 33 | } 34 | 35 | action_1 = { 36 | "request": True, 37 | "redirect": True, 38 | "location": "http://seattle-dev.f5.com", 39 | "httpReply": True 40 | } 41 | 42 | action_2 = { 43 | "request": True, 44 | "forward": True, 45 | "virtual": "/Test/my_virtual" 46 | } 47 | 48 | condition_0 = { 49 | 'httpUri': True, 50 | 'pathSegment': True, 51 | 'contains': True, 52 | 'values': ["colorado"], 53 | } 54 | 55 | condition_1 = { 56 | 'httpUri': True, 57 | 'pathSegment': True, 58 | 'contains': True, 59 | 'values': ["washington"], 60 | } 61 | 62 | condition_2 = { 63 | 'httpUri': True, 64 | 'queryString': True, 65 | 'contains': True, 66 | 'values': ["washington"], 67 | } 68 | 69 | @pytest.fixture 70 | def rule_0(): 71 | data = {'ordinal': "0", 72 | 'actions': [], 73 | 'conditions': []} 74 | data['conditions'].append(condition_0) 75 | data['actions'].append(action_0) 76 | return Rule(name="rule_0", **data) 77 | 78 | 79 | @pytest.fixture 80 | def rule_0_clone(): 81 | data = {'ordinal': "0", 82 | 'actions': [], 83 | 'conditions': []} 84 | data['conditions'].append(condition_0) 85 | data['actions'].append(action_0) 86 | return Rule(name="rule_0", **data) 87 | 88 | 89 | @pytest.fixture 90 | def rule_1(): 91 | data = {'ordinal': "1", 92 | 'actions': [], 93 | 'conditions': []} 94 | data['conditions'].append(condition_1) 95 | data['actions'].append(action_1) 96 | return Rule(name="rule_1", **data) 97 | 98 | 99 | @pytest.fixture 100 | def rule_no_actions(): 101 | data = {'ordinal': "0", 102 | 'actions': [], 103 | 'conditions': []} 104 | data['conditions'].append(condition_0) 105 | return Rule(name="rule_0", **data) 106 | 107 | 108 | @pytest.fixture 109 | def rule_no_conditions(): 110 | data = {'ordinal': "0", 111 | 'actions': [], 112 | 'conditions': []} 113 | data['actions'].append(action_1) 114 | return Rule(name="rule_1", **data) 115 | 116 | 117 | def test_create_rule(): 118 | data = {'ordinal': "0", 119 | 'actions': [], 120 | 'conditions': [], 121 | 'description': 'This is a rule description'} 122 | 123 | rule = Rule(name="rule_0", **data) 124 | 125 | assert rule.name == "rule_0" 126 | assert len(rule.data['conditions']) == 0 127 | assert len(rule.data['actions']) == 0 128 | assert rule.data['description'] == 'This is a rule description' 129 | 130 | data['conditions'].append(condition_0) 131 | data['actions'].append(action_0) 132 | 133 | rule = Rule(name="rule_1", **data) 134 | assert len(rule.data['conditions']) == 1 135 | assert len(rule.data['actions']) == 1 136 | 137 | data['conditions'] = [condition_2] 138 | data['actions'] = [action_0] 139 | 140 | rule = Rule(name="rule_1", **data) 141 | assert len(rule.data['conditions']) == 0 142 | assert len(rule.data['actions']) == 1 143 | 144 | data['conditions'].append(condition_0) 145 | rule = Rule(name="rule_1", **data) 146 | assert len(rule.data['conditions']) == 1 147 | assert len(rule.data['actions']) == 1 148 | 149 | data['conditions'] = [condition_0] 150 | data['actions'] = [action_2] 151 | 152 | rule = Rule(name="rule_1", **data) 153 | assert len(rule.data['conditions']) == 1 154 | assert len(rule.data['actions']) == 0 155 | 156 | 157 | def test_uri_path(bigip, rule_0): 158 | with pytest.raises(NotImplementedError): 159 | rule_0._uri_path(bigip) 160 | 161 | 162 | def test_less_than(rule_0, rule_1): 163 | assert rule_0 < rule_1 164 | 165 | 166 | def test_tostring(rule_0): 167 | assert str(rule_0) != "" 168 | 169 | 170 | def test_compare_rules(rule_0, rule_0_clone, rule_1, 171 | rule_no_actions, rule_no_conditions): 172 | 173 | assert rule_0 == rule_0_clone 174 | assert rule_0 != rule_1 175 | 176 | assert rule_0 != rule_no_actions 177 | assert rule_0 != rule_no_conditions 178 | 179 | fake_rule = {'ordinal': "0", 180 | 'actions': [], 181 | 'conditions': []} 182 | 183 | assert rule_0 != fake_rule 184 | 185 | rule_0_clone.data['actions'][0]['location'] = \ 186 | "http://seattle-dev.f5.com" 187 | 188 | assert rule_0 != rule_0_clone 189 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/pool.py: -------------------------------------------------------------------------------- 1 | """This module provides class for managing resource configuration.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | 20 | 21 | import logging 22 | 23 | from f5_cccl.resource.ltm.pool_member import ApiPoolMember 24 | from f5_cccl.resource.ltm.pool_member import IcrPoolMember 25 | from f5_cccl.resource import Resource 26 | 27 | 28 | LOGGER = logging.getLogger(__name__) 29 | 30 | 31 | class Pool(Resource): 32 | """Pool class for deploying configuration on BIG-IP""" 33 | properties = dict(name=None, 34 | partition=None, 35 | loadBalancingMode="round-robin", 36 | description=None, 37 | monitor="default", 38 | membersReference={}) 39 | 40 | def __init__(self, name, partition, members=None, **properties): 41 | """Create a Pool instance from CCCL poolType.""" 42 | super(Pool, self).__init__(name, partition, **properties) 43 | 44 | for key, value in list(self.properties.items()): 45 | if key in ["name", "partition"]: 46 | continue 47 | self._data[key] = properties.get(key, value) 48 | 49 | self._data['membersReference'] = { 50 | 'isSubcollection': True, 'items': []} 51 | 52 | if members: 53 | self.members = members 54 | self._data['membersReference']['items'] = [ 55 | m.data for m in members] 56 | else: 57 | self.members = list() 58 | 59 | # pylint: disable=too-many-return-statements 60 | def __eq__(self, other): 61 | if not isinstance(other, Pool): 62 | LOGGER.warning( 63 | "Invalid comparison of Pool object with object " 64 | "of type %s", type(other)) 65 | return False 66 | 67 | for key in self.properties: 68 | if key in ['membersReference', 'monitor']: 69 | continue 70 | 71 | if isinstance(self._data[key], list): 72 | if sorted(self._data[key]) != \ 73 | sorted(other.data.get(key, list())): 74 | return False 75 | continue 76 | 77 | if self._data[key] != other.data.get(key, None): 78 | return False 79 | 80 | if len(self) != len(other): 81 | return False 82 | if set(self.members) - set(other.members): 83 | return False 84 | if not self._monitors_equal(other): 85 | return False 86 | 87 | return True 88 | 89 | def _monitors_equal(self, other): 90 | return self.monitors() == other.monitors() 91 | 92 | def __hash__(self): # pylint: disable=useless-super-delegation 93 | return super(Pool, self).__hash__() 94 | 95 | def __len__(self): 96 | return len(self.members) 97 | 98 | def _uri_path(self, bigip): 99 | return bigip.tm.ltm.pools.pool 100 | 101 | def monitors(self): 102 | """Return list of configured monitors""" 103 | self_monitor_list = sorted( 104 | [m.rstrip() for m in self._data['monitor'].split(" and ")] 105 | ) 106 | return self_monitor_list 107 | 108 | 109 | class ApiPool(Pool): 110 | """Parse the CCCL input to create the canonical Pool.""" 111 | def __init__(self, name, partition, default_route_domain, **properties): 112 | """Parse the CCCL schema input.""" 113 | pool_config = dict() 114 | for k, v in list(properties.items()): 115 | if k in ["members", "monitors"]: 116 | continue 117 | pool_config[k] = v 118 | 119 | members_config = properties.get('members', None) 120 | members = self._get_members(partition, default_route_domain, 121 | members_config) 122 | 123 | monitors_config = properties.pop('monitors', None) 124 | pool_config['monitor'] = self._get_monitors(monitors_config) 125 | 126 | super(ApiPool, self).__init__(name, partition, 127 | members, 128 | **pool_config) 129 | 130 | def _get_members(self, partition, default_route_domain, members): 131 | """Get a list of members from the pool definition""" 132 | members_list = list() 133 | if members: 134 | for member in members: 135 | m = ApiPoolMember( 136 | partition=partition, 137 | default_route_domain=default_route_domain, 138 | pool=self, 139 | **member) 140 | members_list.append(m) 141 | 142 | return members_list 143 | 144 | def _get_monitors(self, monitors): 145 | if not monitors: 146 | return "default" 147 | 148 | return " and ".join(sorted(monitors)) 149 | 150 | 151 | class IcrPool(Pool): 152 | """Filter the iControl REST input to create the canonical Pool.""" 153 | def __init__(self, name, partition, **properties): 154 | """Parse the iControl REST representation of the Pool""" 155 | members = self._get_members(**properties) 156 | super(IcrPool, self).__init__(name, partition, 157 | members, 158 | **properties) 159 | 160 | def _get_members(self, **properties): 161 | """Get a list of members from the pool definition""" 162 | try: 163 | members = ( 164 | properties['membersReference'].get('items', []) 165 | ) 166 | except KeyError: 167 | return list() 168 | 169 | return [ 170 | IcrPoolMember(pool=self, 171 | **member) 172 | for member in members] 173 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/profile/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | """This module implements the F5 CCCL Profile class.""" 17 | 18 | 19 | from .profile import Profile # noqa: F401 20 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/profile/profile.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP Profile resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | class Profile(Resource): 28 | """Profile class for managing configuration on BIG-IP.""" 29 | 30 | properties = dict(name=None, 31 | partition=None, 32 | context="all") 33 | 34 | def __init__(self, name, partition, **properties): 35 | """Create a Virtual server instance.""" 36 | super(Profile, self).__init__(name, partition) 37 | self._data['context'] = properties.get('context', "all") 38 | 39 | def __eq__(self, other): 40 | if not isinstance(other, Profile): 41 | return False 42 | 43 | return super(Profile, self).__eq__(other) 44 | 45 | def _uri_path(self, bigip): 46 | """""" 47 | raise NotImplementedError 48 | 49 | def __repr__(self): 50 | return 'Profile(%r, %r, context=%r)' % (self._data['name'], 51 | self._data['partition'], 52 | self._data['context']) 53 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/profile/test/test_profile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from f5_cccl.resource.ltm.profile import Profile 18 | from mock import Mock 19 | import pytest 20 | 21 | 22 | cfg_test = { 23 | 'name': 'tcp', 24 | 'partition': 'Common', 25 | 'context': 'all' 26 | } 27 | 28 | 29 | @pytest.fixture 30 | def bigip(): 31 | bigip = Mock() 32 | return bigip 33 | 34 | 35 | def test_create_profile(): 36 | """Test Profile creation.""" 37 | profile = Profile( 38 | **cfg_test 39 | ) 40 | assert profile 41 | 42 | # verify all cfg items 43 | for k,v in list(cfg_test.items()): 44 | assert profile.data[k] == v 45 | 46 | 47 | def test_eq(): 48 | """Test Profile equality.""" 49 | partition = 'Common' 50 | name = 'tcp' 51 | 52 | profile1 = Profile( 53 | **cfg_test 54 | ) 55 | profile2 = Profile( 56 | **cfg_test 57 | ) 58 | assert profile1 59 | assert profile2 60 | assert id(profile1) != id(profile2) 61 | assert profile1 == profile2 62 | 63 | # not equal 64 | profile2.data['context'] = 'serverside' 65 | assert profile1 != profile2 66 | 67 | # different objects 68 | assert profile1 != "profile1" 69 | 70 | 71 | def test_uri_path(bigip): 72 | """Test Profile URI.""" 73 | profile = Profile( 74 | **cfg_test 75 | ) 76 | assert profile 77 | 78 | with pytest.raises(NotImplementedError): 79 | profile._uri_path(bigip) 80 | 81 | def test_repr(): 82 | """Test get repr.""" 83 | profile = Profile( 84 | **cfg_test 85 | ) 86 | assert profile 87 | 88 | assert ( 89 | repr(profile) == "Profile('tcp', 'Common', context='all')") 90 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/bigip-members.json: -------------------------------------------------------------------------------- 1 | { 2 | "members": [ 3 | { 4 | "state": "user-down", 5 | "kind": "tm:ltm:pool:members:membersstate", 6 | "inheritProfile": "enabled", 7 | "name": "192.168.200.2:80", 8 | "priorityGroup": 0, 9 | "generation": 137066, 10 | "partition": "Common", 11 | "ephemeral": "false", 12 | "fqdn": { 13 | "autopopulate": "disabled" 14 | }, 15 | "rateLimit": "disabled", 16 | "session": "user-enabled", 17 | "dynamicRatio": 1, 18 | "nameReference": { 19 | "link": "https://localhost/mgmt/tm/ltm/node/~Common~192.168.200.2:80?ver=12.1.1" 20 | }, 21 | "connectionLimit": 0, 22 | "address": "192.168.200.2", 23 | "logging": "disabled", 24 | "fullPath": "/Common/192.168.200.2:80", 25 | "ratio": 1, 26 | "selfLink": "https://localhost/mgmt/tm/ltm/pool/~Common~pool1/members/~Common~192.168.200.2:80?ver=12.1.1", 27 | "monitor": "default" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import pdb 18 | import pytest 19 | 20 | from mock import Mock 21 | from mock import patch 22 | 23 | 24 | class TestLtmResource(object): 25 | """Creates a TestLtmResource Object 26 | This object is useful in inheriting it within other, branching 27 | Resource's sub-objects' testing. This object uses built-in 28 | features that can be used by any number of Resource objects for 29 | their testing. 30 | """ 31 | @pytest.fixture 32 | def create_ltm_resource(self): 33 | """Useful for mocking f5_cccl.resource.Resource.__init__() 34 | This test-class method is useful for mocking out the Resource 35 | parent object. 36 | """ 37 | Resource = Mock() 38 | pdb.set_trace() 39 | # future proofing: 40 | with patch('f5_cccl.resource.Resource.__init__', Resource, 41 | create=True): 42 | _data = dict() 43 | self.create_child() 44 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/test_internal_data_group.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import deepcopy 18 | from f5_cccl.resource.ltm.internal_data_group import InternalDataGroup 19 | from mock import Mock 20 | import pytest 21 | 22 | 23 | cfg_test = { 24 | 'name': 'test_dg', 25 | 'partition': 'my_partition', 26 | 'type': 'string', 27 | 'records': [ 28 | { 29 | "name": "test_record_name", 30 | "data": "test record data" 31 | } 32 | ] 33 | } 34 | 35 | class FakeObj: pass 36 | 37 | 38 | @pytest.fixture 39 | def bigip(): 40 | bigip = Mock() 41 | return bigip 42 | 43 | 44 | def test_create_internal_data_group(): 45 | """Test InternalDataGroup creation.""" 46 | idg = InternalDataGroup( 47 | **cfg_test 48 | ) 49 | assert idg 50 | 51 | # verify all cfg items 52 | for k,v in list(cfg_test.items()): 53 | assert idg.data[k] == v 54 | 55 | 56 | def test_hash(): 57 | """Test InternalDataGroup hash.""" 58 | idg1 = InternalDataGroup( 59 | **cfg_test 60 | ) 61 | idg2 = InternalDataGroup( 62 | **cfg_test 63 | ) 64 | cfg_changed = deepcopy(cfg_test) 65 | cfg_changed['name'] = 'test' 66 | idg3 = InternalDataGroup( 67 | **cfg_changed 68 | ) 69 | cfg_changed = deepcopy(cfg_test) 70 | cfg_changed['partition'] = 'other' 71 | idg4 = InternalDataGroup( 72 | **cfg_changed 73 | ) 74 | 75 | assert idg1 76 | assert idg2 77 | assert idg3 78 | assert idg4 79 | 80 | assert hash(idg1) == hash(idg2) 81 | assert hash(idg1) != hash(idg3) 82 | assert hash(idg1) != hash(idg4) 83 | 84 | 85 | def test_eq(): 86 | """Test InternalDataGroup equality.""" 87 | partition = 'Common' 88 | name = 'idg_1' 89 | 90 | idg1 = InternalDataGroup( 91 | **cfg_test 92 | ) 93 | idg2 = InternalDataGroup( 94 | **cfg_test 95 | ) 96 | assert idg1 97 | assert idg2 98 | assert idg1 == idg2 99 | 100 | # name not equal 101 | cfg_changed = deepcopy(cfg_test) 102 | cfg_changed['name'] = 'idg_2' 103 | idg2 = InternalDataGroup(**cfg_changed) 104 | assert idg1 != idg2 105 | 106 | # partition not equal 107 | cfg_changed = deepcopy(cfg_test) 108 | cfg_changed['partition'] = 'test' 109 | idg2 = InternalDataGroup(**cfg_changed) 110 | assert idg1 != idg2 111 | 112 | # the records in the group not equal 113 | cfg_changed = deepcopy(cfg_test) 114 | cfg_changed['records'][0]['data'] = 'changed data' 115 | idg2 = InternalDataGroup(**cfg_changed) 116 | assert idg1 != idg2 117 | 118 | # different objects 119 | fake = FakeObj 120 | assert idg1 != fake 121 | 122 | # should be equal after assignment 123 | idg2 = idg1 124 | assert idg1 == idg2 125 | 126 | 127 | def test_uri_path(bigip): 128 | """Test InternalDataGroup URI.""" 129 | idg = InternalDataGroup( 130 | **cfg_test 131 | ) 132 | assert idg 133 | assert idg._uri_path(bigip) == bigip.tm.ltm.data_group.internals.internal 134 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/test_irule.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import copy 18 | from f5_cccl.resource.ltm.irule import IRule 19 | from mock import Mock 20 | import pytest 21 | 22 | 23 | ssl_redirect_irule_1 = """ 24 | when HTTP_REQUEST { 25 | HTTP::redirect https://[getfield [HTTP::host] \":\" 1][HTTP::uri] 26 | } 27 | """ 28 | 29 | cfg_test = { 30 | 'name': 'ssl_redirect', 31 | 'partition': 'my_partition', 32 | 'apiAnonymous': ssl_redirect_irule_1, 33 | 'metadata': [{ 34 | 'name': 'user_agent', 35 | 'persist': 'true', 36 | 'value': 'some-controller-v.1.4.0' 37 | }] 38 | } 39 | 40 | class FakeObj: pass 41 | 42 | 43 | @pytest.fixture 44 | def bigip(): 45 | bigip = Mock() 46 | return bigip 47 | 48 | 49 | def test_create_irule(): 50 | """Test iRule creation.""" 51 | irule = IRule( 52 | **cfg_test 53 | ) 54 | assert irule 55 | 56 | # verify all cfg items 57 | for k,v in list(cfg_test.items()): 58 | if type(v) is not list: 59 | assert irule.data[k] == v.strip() 60 | else: 61 | assert irule.data[k] == v 62 | 63 | 64 | def test_hash(): 65 | """Test Node Server hash.""" 66 | irule1 = IRule( 67 | **cfg_test 68 | ) 69 | irule2 = IRule( 70 | **cfg_test 71 | ) 72 | cfg_changed = copy(cfg_test) 73 | cfg_changed['name'] = 'test' 74 | irule3 = IRule( 75 | **cfg_changed 76 | ) 77 | cfg_changed = copy(cfg_test) 78 | cfg_changed['partition'] = 'other' 79 | irule4 = IRule( 80 | **cfg_changed 81 | ) 82 | assert irule1 83 | assert irule2 84 | assert irule3 85 | assert irule4 86 | 87 | assert hash(irule1) == hash(irule2) 88 | assert hash(irule1) != hash(irule3) 89 | assert hash(irule1) != hash(irule4) 90 | 91 | 92 | def test_eq(): 93 | """Test iRule equality.""" 94 | partition = 'Common' 95 | name = 'irule_1' 96 | 97 | irule1 = IRule( 98 | **cfg_test 99 | ) 100 | irule2 = IRule( 101 | **cfg_test 102 | ) 103 | assert irule1 104 | assert irule2 105 | assert irule1 == irule2 106 | 107 | # name not equal 108 | cfg_changed = copy(cfg_test) 109 | cfg_changed['name'] = 'ssl_redirect_2' 110 | irule2 = IRule(**cfg_changed) 111 | assert irule1 != irule2 112 | 113 | # partition not equal 114 | cfg_changed = copy(cfg_test) 115 | cfg_changed['partition'] = 'test' 116 | irule2 = IRule(**cfg_changed) 117 | assert irule1 != irule2 118 | 119 | # the actual rule code not equal 120 | cfg_changed = copy(cfg_test) 121 | cfg_changed['apiAnonymous'] = None 122 | irule2 = IRule(**cfg_changed) 123 | assert irule1 != irule2 124 | 125 | # different objects 126 | fake = FakeObj 127 | assert irule1 != fake 128 | 129 | # should be equal after assignment 130 | irule2 = irule1 131 | assert irule1 == irule2 132 | 133 | 134 | def test_uri_path(bigip): 135 | """Test iRule URI.""" 136 | irule = IRule( 137 | **cfg_test 138 | ) 139 | assert irule 140 | 141 | assert irule._uri_path(bigip) == bigip.tm.ltm.rules.rule 142 | 143 | 144 | def test_whitespace(): 145 | """Verify that leading/trailing whitespace is removed from iRule.""" 146 | whitespace = '\n\t ' 147 | ssl_redirect_irule_ws = whitespace + ssl_redirect_irule_1 + whitespace 148 | 149 | cfg_ws = { 150 | 'name': 'ssl_redirect', 151 | 'partition': 'my_partition', 152 | 'apiAnonymous': ssl_redirect_irule_ws 153 | } 154 | 155 | irule = IRule( 156 | **cfg_ws 157 | ) 158 | 159 | assert irule 160 | assert irule.data['apiAnonymous'] == ssl_redirect_irule_1.strip() 161 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/test_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import copy 18 | from f5_cccl.resource import Resource 19 | from f5_cccl.resource.ltm.node import Node 20 | from f5_cccl.resource.ltm.pool import Pool 21 | from mock import Mock, patch 22 | import pytest 23 | 24 | 25 | cfg_test = { 26 | 'name': '1.2.3.4%2', 27 | 'partition': 'my_partition', 28 | 'address': '1.2.3.4%2' 29 | } 30 | 31 | 32 | @pytest.fixture 33 | def bigip(): 34 | bigip = Mock() 35 | return bigip 36 | 37 | 38 | def test_create_node(): 39 | """Test Node creation.""" 40 | node = Node( 41 | default_route_domain=2, 42 | **cfg_test 43 | ) 44 | assert node 45 | 46 | # verify all cfg items 47 | for k,v in list(cfg_test.items()): 48 | assert node._data[k] == v 49 | 50 | 51 | def test_update_node(): 52 | node = Node( 53 | default_route_domain=2, 54 | **cfg_test 55 | ) 56 | 57 | assert 'address' in node.data 58 | 59 | # Verify that immutable 'address' is not passed to parent method 60 | with patch.object(Resource, 'update') as mock_method: 61 | node.update(bigip) 62 | assert 1 == mock_method.call_count 63 | assert 'address' not in mock_method.call_args[1]['data'] 64 | 65 | 66 | def test_hash(): 67 | """Test Node Server hash.""" 68 | node = Node( 69 | default_route_domain=2, 70 | **cfg_test 71 | ) 72 | node1 = Node( 73 | default_route_domain=2, 74 | **cfg_test 75 | ) 76 | cfg_changed = copy(cfg_test) 77 | cfg_changed['name'] = 'test' 78 | node2 = Node( 79 | default_route_domain=2, 80 | **cfg_changed 81 | ) 82 | cfg_changed = copy(cfg_test) 83 | cfg_changed['partition'] = 'other' 84 | node3 = Node( 85 | default_route_domain=2, 86 | **cfg_changed 87 | ) 88 | assert node 89 | assert node1 90 | assert node2 91 | assert node3 92 | 93 | assert hash(node) == hash(node1) 94 | assert hash(node) != hash(node2) 95 | assert hash(node) != hash(node3) 96 | 97 | 98 | def test_eq(): 99 | """Test Node equality.""" 100 | partition = 'Common' 101 | name = 'node_1' 102 | 103 | node = Node( 104 | default_route_domain=2, 105 | **cfg_test 106 | ) 107 | node2 = Node( 108 | default_route_domain=2, 109 | **cfg_test 110 | ) 111 | pool = Pool( 112 | name=name, 113 | partition=partition 114 | ) 115 | assert node 116 | assert node2 117 | assert node != node2 118 | 119 | node2.data['state'] = 'up' 120 | node2.data['session'] = 'user-enabled' 121 | assert node == node2 122 | 123 | node2.data['state'] = 'unchecked' 124 | node2.data['session'] = 'monitor-enabled' 125 | assert node == node2 126 | 127 | # not equal 128 | node2.data['state'] = 'user-down' 129 | node2.data['session'] = 'user-enabled' 130 | assert node != node2 131 | 132 | node2.data['state'] = 'up' 133 | node2.data['session'] = 'user-disabled' 134 | assert node != node2 135 | 136 | node2.data['state'] = 'up' 137 | node2.data['session'] = 'user-enabled' 138 | node2.data['address'] = '10.10.0.10' 139 | assert node != node2 140 | 141 | # different objects 142 | assert node != pool 143 | 144 | 145 | def test_uri_path(bigip): 146 | """Test Node URI.""" 147 | node = Node( 148 | default_route_domain=2, 149 | **cfg_test 150 | ) 151 | assert node 152 | 153 | assert node._uri_path(bigip) == bigip.tm.ltm.nodes.node 154 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/test_pool_member.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import json 18 | import os 19 | from pprint import pprint as pp 20 | 21 | from f5_cccl.resource.ltm.pool_member import IcrPoolMember 22 | from f5_cccl.resource.ltm.pool_member import PoolMember 23 | 24 | 25 | from mock import MagicMock 26 | # import pdb 27 | import pytest 28 | 29 | 30 | @pytest.fixture 31 | def pool_member_ipv6(): 32 | pass 33 | 34 | 35 | @pytest.fixture 36 | def pool_member_with_rd(): 37 | member = {"name": "192.168.100.101%0:80"} 38 | return member 39 | 40 | 41 | @pytest.fixture 42 | def pool_member_with_rd_ipv6(): 43 | member = {"name": "2001:0db8:3c4d:0015:0000:0000:abcd:ef12%0.80"} 44 | return member 45 | 46 | 47 | @pytest.fixture 48 | def bigip(): 49 | bigip = MagicMock() 50 | return bigip 51 | 52 | 53 | @pytest.fixture 54 | def pool(): 55 | return MagicMock() 56 | 57 | 58 | @pytest.fixture 59 | def bigip_members(): 60 | members_filename = ( 61 | os.path.join(os.path.dirname(os.path.abspath(__file__)), 62 | './bigip-members.json')) 63 | with open(members_filename) as fp: 64 | json_data = fp.read() 65 | json_data = json.loads(json_data) 66 | members = [m for m in json_data['members']] 67 | pp(json_data) 68 | 69 | return members 70 | 71 | 72 | def test_create_bigip_member(pool, bigip_members): 73 | """Test the creation of PoolMember from BIG-IP data.""" 74 | member_cfg = bigip_members[0] 75 | 76 | pp(bigip_members) 77 | pp(member_cfg) 78 | # pdb.set_trace() 79 | member = IcrPoolMember( 80 | pool=pool, 81 | **member_cfg 82 | ) 83 | 84 | assert member 85 | 86 | # Test data 87 | assert member.data 88 | assert member.data['name'] == "192.168.200.2:80" 89 | assert member.data['ratio'] == 1 90 | assert member.data['connectionLimit'] == 0 91 | assert member.data['priorityGroup'] == 0 92 | assert member.data['session'] == "user-enabled" 93 | assert not member.data['description'] 94 | 95 | 96 | def test_create_pool_member(pool, bigip_members): 97 | """Test the creation of PoolMember from BIG-IP data.""" 98 | member_cfg = bigip_members[0] 99 | 100 | member = PoolMember( 101 | pool=pool, 102 | **member_cfg 103 | ) 104 | 105 | assert member 106 | assert member._pool 107 | 108 | # Test data 109 | assert member.data 110 | assert member.data['name'] == "192.168.200.2:80" 111 | assert member.data['ratio'] == 1 112 | assert member.data['connectionLimit'] == 0 113 | assert member.data['priorityGroup'] == 0 114 | assert member.data['session'] == "user-enabled" 115 | assert not member.data['description'] 116 | 117 | 118 | def test_create_pool_member_with_rd(pool, pool_member_with_rd): 119 | """Test the creation of PoolMember from BIG-IP data.""" 120 | member = PoolMember( 121 | partition="Common", 122 | pool=pool, 123 | **pool_member_with_rd 124 | ) 125 | 126 | assert member 127 | assert member._pool 128 | 129 | # Test data 130 | assert member.data 131 | assert member.data['name'] == "192.168.100.101%0:80" 132 | 133 | 134 | def test_create_pool_member_with_rd_ipv6(pool, pool_member_with_rd_ipv6): 135 | """Test the creation of PoolMember from BIG-IP data.""" 136 | member = PoolMember( 137 | partition="Common", 138 | pool=pool, 139 | **pool_member_with_rd_ipv6 140 | ) 141 | 142 | assert member 143 | assert member._pool 144 | 145 | # Test data 146 | assert member.data 147 | assert member.data['name'] == "2001:0db8:3c4d:0015:0000:0000:abcd:ef12%0.80" 148 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/test/test_virtual_address.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import deepcopy 18 | from mock import Mock, patch 19 | import pytest 20 | 21 | from f5_cccl.resource import Resource 22 | from f5_cccl.resource.ltm.virtual_address import VirtualAddress 23 | 24 | va_cfg = { 25 | "name": "192.168.100.100", 26 | "partition": "Test", 27 | "default_route_domain": 0, 28 | "address": "192.168.100.100", 29 | "autoDelete": "true", 30 | "enabled": "yes", 31 | "description": "Test virutal address resource", 32 | "trafficGroup": "/Common/traffic-group-local-only", 33 | 'metadata': [{ 34 | 'name': 'user_agent', 35 | 'persist': 'true', 36 | 'value': 'some-controller-v.1.4.0' 37 | }] 38 | } 39 | 40 | @pytest.fixture 41 | def bigip(): 42 | bigip = Mock() 43 | return bigip 44 | 45 | 46 | def test_create_virtual_address(): 47 | va = VirtualAddress(**va_cfg) 48 | 49 | assert va 50 | 51 | assert va.name == "192.168.100.100" 52 | assert va.partition == "Test" 53 | 54 | data = va.data 55 | assert data['address'] == "192.168.100.100%0" 56 | assert data['autoDelete'] == "true" 57 | assert data['enabled'] == "yes" 58 | assert data['description'] == "Test virutal address resource" 59 | assert data['trafficGroup'] == "/Common/traffic-group-local-only" 60 | 61 | 62 | def test_create_virtual_address_defaults(): 63 | va = VirtualAddress(name="test_va", partition="Test", default_route_domain=1) 64 | 65 | assert va 66 | 67 | assert va.name == "test_va" 68 | assert va.partition == "Test" 69 | 70 | data = va.data 71 | assert not data['address'] 72 | assert data['autoDelete'] == "false" 73 | assert not data['enabled'] 74 | assert not data['description'] 75 | assert data['trafficGroup'] == "/Common/traffic-group-1" 76 | 77 | 78 | def test_update_virtual_address(): 79 | va = VirtualAddress(**va_cfg) 80 | 81 | assert 'address' in va.data 82 | assert 'metadata' in va.data 83 | 84 | # Verify that immutable 'address' is not passed to parent method 85 | with patch.object(Resource, 'update') as mock_method: 86 | va.update(bigip) 87 | assert 1 == mock_method.call_count 88 | assert 'address' not in mock_method.call_args[1]['data'] 89 | 90 | 91 | def test_equals_virtual_address(): 92 | va1 = VirtualAddress(**va_cfg) 93 | va2 = VirtualAddress(**va_cfg) 94 | va3 = deepcopy(va1) 95 | 96 | assert id(va1) != id(va2) 97 | assert va1 == va2 98 | 99 | assert id(va1) != id(va3) 100 | assert va1 == va3 101 | 102 | va3._data['address'] = "192.168.200.100" 103 | assert va1 != va3 104 | 105 | assert va1 != va_cfg 106 | 107 | 108 | def test_get_uri_path(bigip): 109 | va = VirtualAddress(**va_cfg) 110 | 111 | assert (va._uri_path(bigip) == 112 | bigip.tm.ltm.virtual_address_s.virtual_address) 113 | -------------------------------------------------------------------------------- /f5_cccl/resource/ltm/virtual_address.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP Virtual Address resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | from copy import deepcopy 20 | import logging 21 | 22 | from f5_cccl.resource import Resource 23 | from f5_cccl.utils.route_domain import normalize_address_with_route_domain 24 | 25 | 26 | LOGGER = logging.getLogger(__name__) 27 | 28 | 29 | class VirtualAddress(Resource): 30 | """VirtualAddress class for managing configuration on BIG-IP.""" 31 | 32 | properties = dict(address=None, 33 | autoDelete="false", 34 | enabled=None, 35 | description=None, 36 | trafficGroup="/Common/traffic-group-1") 37 | 38 | def __init__(self, name, partition, default_route_domain, **properties): 39 | """Create a VirtualAddress instance.""" 40 | super(VirtualAddress, self).__init__(name, partition, **properties) 41 | 42 | for key, value in list(self.properties.items()): 43 | self._data[key] = properties.get(key, value) 44 | if self._data['address'] is not None: 45 | self._data['address'] = normalize_address_with_route_domain( 46 | self._data['address'], default_route_domain)[0] 47 | 48 | def __eq__(self, other): 49 | if not isinstance(other, VirtualAddress): 50 | return False 51 | 52 | for key in self._data: 53 | if isinstance(self._data[key], list): 54 | if sorted(self._data[key]) != \ 55 | sorted(other.data.get(key, list())): 56 | return False 57 | continue 58 | 59 | if self._data[key] != other.data.get(key): 60 | return False 61 | 62 | return True 63 | 64 | def _uri_path(self, bigip): 65 | return bigip.tm.ltm.virtual_address_s.virtual_address 66 | 67 | def update(self, bigip, data=None, modify=False): 68 | # 'address' is immutable, don't pass it in an update operation 69 | tmp_data = deepcopy(data) if data is not None else deepcopy(self.data) 70 | tmp_data.pop('address', None) 71 | super(VirtualAddress, self).update(bigip, data=tmp_data, modify=modify) 72 | 73 | 74 | class IcrVirtualAddress(VirtualAddress): 75 | """Filter the iControl REST input to create the canonical representation""" 76 | pass 77 | 78 | 79 | class ApiVirtualAddress(VirtualAddress): 80 | """Filter the CCCL API input to create the canonical representation""" 81 | pass 82 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/resource/net/__init__.py -------------------------------------------------------------------------------- /f5_cccl/resource/net/arp.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP ARP resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | class Arp(Resource): 28 | """ARP class for managing network configuration on BIG-IP.""" 29 | properties = dict(name=None, 30 | partition=None, 31 | ipAddress=None, 32 | macAddress=None) 33 | 34 | def __init__(self, name, partition, **data): 35 | """Create an ARP entry from CCCL arpType.""" 36 | super(Arp, self).__init__(name, partition) 37 | 38 | for key, value in list(self.properties.items()): 39 | if key in ["name", "partition"]: 40 | continue 41 | self._data[key] = data.get(key, value) 42 | 43 | def __eq__(self, other): 44 | if not isinstance(other, Arp): 45 | LOGGER.warning( 46 | "Invalid comparison of Arp object with object " 47 | "of type %s", type(other)) 48 | return False 49 | 50 | for key in self.properties: 51 | if self._data[key] != other.data.get(key, None): 52 | return False 53 | 54 | return True 55 | 56 | def __hash__(self): # pylint: disable=useless-super-delegation 57 | return super(Arp, self).__hash__() 58 | 59 | def _uri_path(self, bigip): 60 | return bigip.tm.net.arps.arp 61 | 62 | 63 | class IcrArp(Arp): 64 | """Arp object created from the iControl REST object.""" 65 | pass 66 | 67 | 68 | class ApiArp(Arp): 69 | """Arp object created from the API configuration object.""" 70 | pass 71 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/fdb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/resource/net/fdb/__init__.py -------------------------------------------------------------------------------- /f5_cccl/resource/net/fdb/record.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP FDB tunnel record resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | from f5_cccl.utils.route_domain import normalize_address_with_route_domain 23 | 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | class Record(Resource): 29 | """Record class for managing network configuration on BIG-IP.""" 30 | properties = dict(name=None, endpoint=None) 31 | 32 | def __init__(self, name, default_route_domain, **data): 33 | """Create a record from CCCL recordType.""" 34 | super(Record, self).__init__(name, partition=None) 35 | endpoint = data.get('endpoint', None) 36 | self._data['endpoint'] = normalize_address_with_route_domain( 37 | endpoint, default_route_domain)[0] 38 | 39 | def __eq__(self, other): 40 | if not isinstance(other, Record): 41 | return False 42 | 43 | return super(Record, self).__eq__(other) 44 | 45 | def _uri_path(self, bigip): 46 | raise NotImplementedError 47 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/fdb/test/test_record.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import copy 18 | from f5_cccl.resource.net.fdb.record import Record 19 | from mock import Mock 20 | import pytest 21 | 22 | 23 | cfg_test = { 24 | 'name':'12:ab:34:cd:56:ef', 25 | 'default_route_domain': 2, 26 | 'endpoint':'1.2.3.4' 27 | } 28 | 29 | @pytest.fixture 30 | def bigip(): 31 | bigip = Mock() 32 | return bigip 33 | 34 | 35 | def test_create_record(): 36 | """Test Record creation.""" 37 | record = Record(**cfg_test) 38 | assert Record 39 | assert record.name == '12:ab:34:cd:56:ef' 40 | assert record.data['endpoint'] == '1.2.3.4%2' 41 | 42 | 43 | def test_eq(): 44 | """Test Record equality.""" 45 | record1 = Record(**cfg_test) 46 | record2 = Record(**cfg_test) 47 | assert record1 == record2 48 | 49 | # name not equal 50 | cfg_changed = copy(cfg_test) 51 | cfg_changed['name'] = '98:ab:76:cd:54:ef' 52 | record2 = Record(**cfg_changed) 53 | assert record1 != record2 54 | 55 | # endpoint not equal 56 | cfg_changed = copy(cfg_test) 57 | cfg_changed['endpoint'] = '4.3.2.1' 58 | record2 = Record(**cfg_changed) 59 | assert record1 != record2 60 | 61 | 62 | def test_uri_path(bigip): 63 | """Test Record URI.""" 64 | record = Record(**cfg_test) 65 | 66 | with pytest.raises(NotImplementedError): 67 | record._uri_path(bigip) 68 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/fdb/test/test_tunnel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import copy 18 | from f5_cccl.resource.net.fdb.tunnel import FDBTunnel 19 | from mock import Mock 20 | import pytest 21 | 22 | 23 | cfg_test = { 24 | 'name': 'test_tunnel', 25 | 'partition': 'test_partition', 26 | 'default_route_domain': 3, 27 | 'records': [ 28 | { 29 | 'name': '12:ab:34:cd:56:ef', 30 | 'endpoint': '1.2.3.4' 31 | }, 32 | { 33 | 'name': '98:ab:76:cd:54:ef', 34 | 'endpoint': '4.3.2.1' 35 | } 36 | ] 37 | } 38 | 39 | 40 | @pytest.fixture 41 | def bigip(): 42 | bigip = Mock() 43 | return bigip 44 | 45 | 46 | def test_create_tunnel(): 47 | """Test FDBTunnel creation.""" 48 | tunnel = FDBTunnel(**cfg_test) 49 | data = tunnel.data 50 | assert tunnel.name == 'test_tunnel' 51 | assert tunnel.partition == 'test_partition' 52 | assert data['records'][0]['name'] == '12:ab:34:cd:56:ef' 53 | assert data['records'][0]['endpoint'] == '1.2.3.4%3' 54 | assert data['records'][1]['name'] == '98:ab:76:cd:54:ef' 55 | assert data['records'][1]['endpoint'] == '4.3.2.1%3' 56 | 57 | 58 | def test_eq(): 59 | """Test FDBTunnel equality.""" 60 | tunnel1 = FDBTunnel(**cfg_test) 61 | tunnel2 = FDBTunnel(**cfg_test) 62 | assert tunnel1 63 | assert tunnel2 64 | assert tunnel1 == tunnel2 65 | 66 | # name not equal 67 | cfg_changed = copy(cfg_test) 68 | cfg_changed['name'] = '4.3.2.1' 69 | tunnel2 = FDBTunnel(**cfg_changed) 70 | assert tunnel1 != tunnel2 71 | 72 | # partition not equal 73 | cfg_changed = copy(cfg_test) 74 | cfg_changed['partition'] = 'other' 75 | tunnel2 = FDBTunnel(**cfg_changed) 76 | assert tunnel1 != tunnel2 77 | 78 | # records name not equal 79 | cfg_changed = copy(cfg_test) 80 | cfg_changed['records'][0]['name'] = '12:wx:34:yz:56:ab' 81 | tunnel2 = FDBTunnel(**cfg_changed) 82 | assert tunnel1 != tunnel2 83 | 84 | # records endpoint not equal 85 | cfg_changed = copy(cfg_test) 86 | cfg_changed['records'][0]['endpoint'] = '5.6.7.8' 87 | tunnel2 = FDBTunnel(**cfg_changed) 88 | assert tunnel1 != tunnel2 89 | 90 | 91 | def test_hash(): 92 | """Test FDBTunnel hash.""" 93 | tunnel1 = FDBTunnel(**cfg_test) 94 | tunnel2 = FDBTunnel(**cfg_test) 95 | 96 | cfg_changed = copy(cfg_test) 97 | cfg_changed['name'] = 'new_tunnel' 98 | tunnel3 = FDBTunnel(**cfg_changed) 99 | 100 | cfg_changed = copy(cfg_test) 101 | cfg_changed['partition'] = 'other' 102 | tunnel4 = FDBTunnel(**cfg_changed) 103 | 104 | assert tunnel1 105 | assert tunnel2 106 | assert tunnel3 107 | assert tunnel4 108 | 109 | assert hash(tunnel1) == hash(tunnel2) 110 | assert hash(tunnel1) != hash(tunnel3) 111 | assert hash(tunnel1) != hash(tunnel4) 112 | 113 | 114 | def test_uri_path(bigip): 115 | """Test FDBTunnel URI.""" 116 | tunnel = FDBTunnel(**cfg_test) 117 | assert tunnel._uri_path(bigip) == bigip.tm.net.fdb.tunnels.tunnel 118 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/fdb/tunnel.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP FDB tunnel resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | from f5_cccl.resource.net.fdb.record import Record 23 | 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | class FDBTunnel(Resource): 29 | """FDBTunnel class for managing network configuration on BIG-IP.""" 30 | properties = dict(name=None, 31 | partition=None, 32 | records=list()) 33 | 34 | def __init__(self, name, partition, default_route_domain, **data): 35 | """Create a tunnel from CCCL fdbTunnelType.""" 36 | super(FDBTunnel, self).__init__(name, partition) 37 | 38 | records = data.get('records', list()) 39 | self._data['records'] = self._create_records( 40 | default_route_domain, records) 41 | 42 | def __eq__(self, other): 43 | if not isinstance(other, FDBTunnel): 44 | LOGGER.warning( 45 | "Invalid comparison of FDBTunnel object with object " 46 | "of type %s", type(other)) 47 | return False 48 | 49 | for key in self.properties: 50 | if key == 'records': 51 | if len(self._data[key]) != len(other.data[key]): 52 | return False 53 | for record in self._data[key]: 54 | if record not in other.data[key]: 55 | return False 56 | idx = other.data[key].index(record) 57 | if record != other.data[key][idx]: 58 | return False 59 | continue 60 | if self._data[key] != other.data.get(key): 61 | return False 62 | 63 | return True 64 | 65 | def _create_records(self, default_route_domain, records): 66 | """Create a list of records for the tunnel.""" 67 | new_records = list() 68 | for record in records: 69 | record['default_route_domain'] = default_route_domain 70 | new_records.append(Record(**record).data) 71 | return new_records 72 | 73 | def __hash__(self): # pylint: disable=useless-super-delegation 74 | return super(FDBTunnel, self).__hash__() 75 | 76 | def _uri_path(self, bigip): 77 | return bigip.tm.net.fdb.tunnels.tunnel 78 | 79 | 80 | class IcrFDBTunnel(FDBTunnel): 81 | """FDBTunnel object created from the iControl REST object.""" 82 | pass 83 | 84 | 85 | class ApiFDBTunnel(FDBTunnel): 86 | """FDBTunnel object created from the API configuration object.""" 87 | pass 88 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/route.py: -------------------------------------------------------------------------------- 1 | """Provides a class for managing BIG-IP Route resources.""" 2 | # coding=utf-8 3 | # 4 | # Copyright (c) 2017-2021 F5 Networks, Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import logging 20 | 21 | from f5_cccl.resource import Resource 22 | 23 | 24 | LOGGER = logging.getLogger(__name__) 25 | 26 | 27 | class Route(Resource): 28 | """Route class for managing network configuration on BIG-IP.""" 29 | properties = dict(name=None, 30 | partition=None, 31 | network=None, 32 | gw=None, 33 | description=None) 34 | 35 | def __init__(self, name, partition, **data): 36 | """Create an Route entry from CCCL routeType.""" 37 | super(Route, self).__init__(name, partition) 38 | 39 | for key, value in list(self.properties.items()): 40 | if key in ["name", "partition"]: 41 | continue 42 | self._data[key] = data.get(key, value) 43 | 44 | def __eq__(self, other): 45 | if not isinstance(other, Route): 46 | LOGGER.warning( 47 | "Invalid comparison of Route object with object " 48 | "of type %s", type(other)) 49 | return False 50 | 51 | for key in self.properties: 52 | if self._data[key] != other.data.get(key, None): 53 | return False 54 | 55 | return True 56 | 57 | def __hash__(self): # pylint: disable=useless-super-delegation 58 | return super(Route, self).__hash__() 59 | 60 | def _uri_path(self, bigip): 61 | return bigip.tm.net.routes.route 62 | 63 | 64 | class IcrRoute(Route): 65 | """Route object created from the iControl REST object.""" 66 | pass 67 | 68 | 69 | class ApiRoute(Route): 70 | """Route object created from the API configuration object.""" 71 | pass 72 | -------------------------------------------------------------------------------- /f5_cccl/resource/net/test/test_arp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from copy import copy 18 | from f5_cccl.resource.net.arp import Arp 19 | from mock import Mock 20 | import pytest 21 | 22 | 23 | cfg_test = { 24 | 'name': '1.2.3.4', 25 | 'partition': 'test_partition', 26 | 'ipAddress': '1.2.3.4', 27 | 'macAddress': '12:ab:34:cd:56:ef' 28 | } 29 | 30 | 31 | @pytest.fixture 32 | def bigip(): 33 | bigip = Mock() 34 | return bigip 35 | 36 | 37 | def test_create_arp(): 38 | """Test Arp creation.""" 39 | arp = Arp(**cfg_test) 40 | assert Arp 41 | 42 | # verify cfg items 43 | for k, v in list(cfg_test.items()): 44 | assert arp.data[k] == v 45 | 46 | 47 | def test_eq(): 48 | """Test Arp equality.""" 49 | arp1 = Arp(**cfg_test) 50 | arp2 = Arp(**cfg_test) 51 | assert arp1 52 | assert arp2 53 | assert arp1 == arp2 54 | 55 | # name not equal 56 | cfg_changed = copy(cfg_test) 57 | cfg_changed['name'] = '4.3.2.1' 58 | arp2 = Arp(**cfg_changed) 59 | assert arp1 != arp2 60 | 61 | # partition not equal 62 | cfg_changed = copy(cfg_test) 63 | cfg_changed['partition'] = 'other' 64 | arp2 = Arp(**cfg_changed) 65 | assert arp1 != arp2 66 | 67 | # ipAddress not equal 68 | cfg_changed = copy(cfg_test) 69 | cfg_changed['ipAddress'] = '4.3.2.1' 70 | arp2 = Arp(**cfg_changed) 71 | assert arp1 != arp2 72 | 73 | # macAddress not equal 74 | cfg_changed = copy(cfg_test) 75 | cfg_changed['macAddress'] = '98:ab:76:cd:54:ef' 76 | arp2 = Arp(**cfg_changed) 77 | assert arp1 != arp2 78 | 79 | 80 | def test_hash(): 81 | """Test Arp hash.""" 82 | arp1 = Arp(**cfg_test) 83 | arp2 = Arp(**cfg_test) 84 | 85 | cfg_changed = copy(cfg_test) 86 | cfg_changed['name'] = '4.3.2.1' 87 | arp3 = Arp(**cfg_changed) 88 | 89 | cfg_changed = copy(cfg_test) 90 | cfg_changed['partition'] = 'other' 91 | arp4 = Arp(**cfg_changed) 92 | 93 | assert arp1 94 | assert arp2 95 | assert arp3 96 | assert arp4 97 | 98 | assert hash(arp1) == hash(arp2) 99 | assert hash(arp1) != hash(arp3) 100 | assert hash(arp1) != hash(arp4) 101 | 102 | 103 | def test_uri_path(bigip): 104 | """Test Arp URI.""" 105 | arp = Arp(**cfg_test) 106 | assert arp._uri_path(bigip) == bigip.tm.net.arps.arp 107 | -------------------------------------------------------------------------------- /f5_cccl/schemas/cccl-net-api-schema.yml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2017-2021 F5 Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | # This is the schema definition of the cccl-api that 17 | # represents the list of service definitions to apply 18 | # to a partition. 19 | 20 | # What should this be? 21 | $schema: "http://json-schema.org/draft-04/schema#" 22 | id: "http://github.com/f5devcentral/f5-cccl/schemas/cccl-net-api-schema.yml" 23 | type: "object" 24 | description: | 25 | The CCCL "Cecil" library allows clients to define services that describe 26 | NET resources on a managed partition of the BIG-IP. The managed resources 27 | are defined in this schema definitions section. 28 | The structure of the service definition is a collection of lists of 29 | supported resources. Initially this is ARPs and FDB tunnel records. 30 | Where appropriate some basic constraints are defined by the schema; however, 31 | not all actual constraints can be enforced by the schema. It is the 32 | responsibility of the client application to ensure that all dependencies 33 | among the specified resources are met; otherwise, the service will be deployed 34 | in a degraded state. 35 | definitions: 36 | 37 | arpType: 38 | type: "object" 39 | description: "Defines an ARP entry." 40 | properties: 41 | name: 42 | type: "string" 43 | ipAddress: 44 | type: "string" 45 | macAddress: 46 | type: "string" 47 | required: 48 | - "name" 49 | - "ipAddress" 50 | - "macAddress" 51 | 52 | fdbTunnelType: 53 | type: "object" 54 | description: "Defines an FDB tunnel." 55 | properties: 56 | name: 57 | type: "string" 58 | records: 59 | items: 60 | type: "array" 61 | description: > 62 | "List of records associated with this tunnel." 63 | $ref: "#definitions/recordType" 64 | required: 65 | - "name" 66 | - "records" 67 | 68 | recordType: 69 | type: "object" 70 | properties: 71 | name: 72 | description: "Name of the record (MAC address)." 73 | type: "string" 74 | endpoint: 75 | type: "string" 76 | required: 77 | - "name" 78 | - "endpoint" 79 | 80 | properties: 81 | arps: 82 | description: "List of all ARP resources that should exist" 83 | items: 84 | $ref: "#/definitions/arpType" 85 | type: "array" 86 | fdbTunnels: 87 | description: "List of all FDB tunnel resources that should exist" 88 | items: 89 | $ref: "#/definitions/fdbTunnelType" 90 | type: "array" 91 | userFdbTunnels: 92 | description: > 93 | List of user-created FDB tunnel resources to be updated. These are expected to be 94 | administratively created beforehand. CCCL will perform updates only on these tunnels, 95 | no deletion or creation. 96 | items: 97 | $ref: "#/definitions/fdbTunnelType" 98 | type: "array" 99 | -------------------------------------------------------------------------------- /f5_cccl/schemas/tests/net_service.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "arps": [ 4 | { 5 | "name": "test-arp1", 6 | "ipAddress": "4.3.2.1", 7 | "macAddress": "12:ab:34:cd:56:ef" 8 | } 9 | ], 10 | "fdbTunnels": [ 11 | { 12 | "name": "test-tunnel1", 13 | "records": [ 14 | { 15 | "name": "12:ab:34:cd:56:ef", 16 | "endpoint": "1.2.3.4" 17 | }, 18 | { 19 | "name": "98:ab:76:cd:54:ef", 20 | "endpoint": "10.2.3.4" 21 | } 22 | ] 23 | } 24 | ], 25 | "userFdbTunnels": [ 26 | { 27 | "name": "pre-existing-tunnel", 28 | "records": [ 29 | { 30 | "name": "13:ab:57:cd:90:ef", 31 | "endpoint": "5.6.7.8" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /f5_cccl/schemas/tests/test_policy_schema_01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test1", 3 | "l7Policies": [ 4 | { 5 | "name": "test_address_policy", 6 | "strategy": "/Common/first-match", 7 | "rules": [ 8 | { 9 | "conditions": [ 10 | { 11 | "tcp": true, 12 | "address": true, 13 | "matches": true, 14 | "external": true, 15 | "values": [ 16 | "10.10.10.10/32" 17 | ] 18 | } 19 | ], 20 | "name": "rule1", 21 | "actions": [ 22 | { 23 | "forward": true, 24 | "select": true 25 | } 26 | ] 27 | }, 28 | { 29 | "conditions": [ 30 | { 31 | "tcp": true, 32 | "address": true, 33 | "matches": true, 34 | "external": true, 35 | "values": [ 36 | "20.20.20.20/32" 37 | ] 38 | } 39 | ], 40 | "name": "rule2", 41 | "actions": [ 42 | { 43 | "forward": true, 44 | "select": true 45 | } 46 | ] 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /f5_cccl/service/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/service/__init__.py -------------------------------------------------------------------------------- /f5_cccl/service/test/bad_decode_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | 4 | "type": "object", 5 | "properties": { 6 | "name": {"type": "string"}, 7 | "email": {"type": "string"}, 8 | }, 9 | "required": ["email"] 10 | } 11 | -------------------------------------------------------------------------------- /f5_cccl/service/test/bad_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | 4 | "type": "object", 5 | "properties": { 6 | "name": {"type": "string"}, 7 | "email": {"type": "foo"} 8 | }, 9 | "required": ["email"] 10 | } 11 | -------------------------------------------------------------------------------- /f5_cccl/service/test/test_config_reader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import json 18 | import pytest 19 | 20 | from f5_cccl.api import F5CloudServiceManager 21 | from f5_cccl.exceptions import F5CcclConfigurationReadError 22 | from f5_cccl.resource import ltm 23 | from f5_cccl.resource.ltm.virtual import ApiVirtualServer 24 | from f5_cccl.service.manager import ServiceConfigDeployer 25 | from f5_cccl.service.config_reader import ServiceConfigReader 26 | 27 | from mock import MagicMock 28 | from mock import Mock 29 | from mock import patch 30 | 31 | class TestServiceConfigReader: 32 | 33 | def setup(self): 34 | self.partition = "Test" 35 | 36 | svcfile_ltm = 'f5_cccl/schemas/tests/ltm_service.json' 37 | with open(svcfile_ltm, 'r') as fp: 38 | self.ltm_service = json.loads(fp.read()) 39 | svcfile_net = 'f5_cccl/schemas/tests/net_service.json' 40 | with open(svcfile_net, 'r') as fp: 41 | self.net_service = json.loads(fp.read()) 42 | 43 | def test_create_reader(self): 44 | reader = ServiceConfigReader( 45 | self.partition) 46 | 47 | assert reader 48 | assert reader._partition == self.partition 49 | 50 | def test_get_config(self): 51 | reader = ServiceConfigReader(self.partition) 52 | config = reader.read_ltm_config(self.ltm_service, 0, 53 | 'marathon-bigip-ctlr-v1.2.1') 54 | 55 | assert len(config.get('virtuals')) == 2 56 | assert len(config.get('pools')) == 1 57 | assert len(config.get('http_monitors')) == 1 58 | assert len(config.get('https_monitors')) == 1 59 | assert len(config.get('icmp_monitors')) == 1 60 | assert len(config.get('tcp_monitors')) == 1 61 | assert len(config.get('l7policies')) == 3 62 | assert len(config.get('iapps')) == 1 63 | 64 | config = reader.read_net_config(self.net_service, 0) 65 | assert len(config.get('arps')) == 1 66 | assert len(config.get('fdbTunnels')) == 1 67 | assert len(config.get('userFdbTunnels')) == 1 68 | 69 | def test_create_config_item_exception(self): 70 | 71 | with patch.object(ApiVirtualServer, '__init__', side_effect=ValueError("test exception")): 72 | reader = ServiceConfigReader(self.partition) 73 | with pytest.raises(F5CcclConfigurationReadError) as e: 74 | reader.read_ltm_config(self.ltm_service, 0, 75 | 'marathon-bigip-ctlr-v1.2.1') 76 | -------------------------------------------------------------------------------- /f5_cccl/service/validation.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """This module defines the schema validator used by f5-cccl.""" 18 | 19 | 20 | 21 | import logging 22 | from time import time 23 | 24 | import jsonschema 25 | from jsonschema import Draft4Validator 26 | from jsonschema import validators 27 | import simplejson as json 28 | import yaml 29 | 30 | import f5_cccl.exceptions as cccl_exc 31 | 32 | LOGGER = logging.getLogger(__name__) 33 | DEFAULT_LTM_SCHEMA = "./f5_cccl/schemas/cccl-ltm-api-schema.yml" 34 | DEFAULT_NET_SCHEMA = "./f5_cccl/schemas/cccl-net-api-schema.yml" 35 | 36 | 37 | def read_yaml(target): 38 | """Open and read a yaml file.""" 39 | with open(target, 'r') as yaml_file: 40 | yaml_data = yaml.load(yaml_file, Loader=yaml.FullLoader) 41 | return yaml_data 42 | 43 | 44 | def read_json(target): 45 | """Open and read a json file.""" 46 | with open(target, 'r') as json_file: 47 | json_data = json.loads(json_file.read()) 48 | return json_data 49 | 50 | 51 | def read_yaml_or_json(target): 52 | """Read json or yaml, return a dict.""" 53 | if target.lower().endswith('.json'): 54 | return read_json(target) 55 | if target.lower().endswith('.yaml') or target.lower().endswith('.yml'): 56 | return read_yaml(target) 57 | raise cccl_exc.F5CcclError( 58 | 'CCCL API schema json or yaml file expected.') 59 | 60 | 61 | class ServiceConfigValidator(object): 62 | """A schema validator used by f5-cccl service manager. 63 | 64 | Accepts a json BIG-IP service configuration and validates it against 65 | against the default schema. 66 | 67 | Optionally accepts an alternate json or yaml schema to validate against. 68 | 69 | """ 70 | def __init__(self, schema=DEFAULT_LTM_SCHEMA): 71 | """Choose schema and initialize extended Draft4Validator. 72 | 73 | Raises: 74 | F5CcclSchemaError: Failed to read or validate the CCCL 75 | API schema file. 76 | """ 77 | 78 | try: 79 | self.schema = read_yaml_or_json(schema) 80 | except json.JSONDecodeError as error: 81 | LOGGER.error("%s", error) 82 | raise cccl_exc.F5CcclSchemaError( 83 | 'CCCL API schema could not be decoded.') 84 | except IOError as error: 85 | LOGGER.error("%s", error) 86 | raise cccl_exc.F5CcclSchemaError( 87 | 'CCCL API schema could not be read.') 88 | 89 | try: 90 | Draft4Validator.check_schema(self.schema) 91 | self.validate_properties = Draft4Validator.VALIDATORS["properties"] 92 | validator_with_defaults = validators.extend( 93 | Draft4Validator, 94 | {"properties": self.__set_defaults}) 95 | self.validator = validator_with_defaults(self.schema) 96 | except jsonschema.SchemaError as error: 97 | LOGGER.error("%s", error) 98 | raise cccl_exc.F5CcclSchemaError("Invalid API schema") 99 | 100 | def __set_defaults(self, validator, properties, instance, schema): 101 | """Helper function to simply return when setting defaults.""" 102 | for item, subschema in list(properties.items()): 103 | if "default" in subschema: 104 | instance.setdefault(item, subschema["default"]) 105 | 106 | for error in self.validate_properties(validator, properties, instance, 107 | schema): 108 | yield error 109 | 110 | def validate(self, cfg): 111 | """Check a config against the schema, returns `None` at succeess.""" 112 | LOGGER.debug("Validating desired config against CCCL API schema.") 113 | start_time = time() 114 | 115 | try: 116 | LOGGER.debug("validate start") 117 | self.validator.validate(cfg) 118 | except jsonschema.exceptions.ValidationError as err: 119 | msg = str(err) 120 | raise cccl_exc.F5CcclValidationError(msg) 121 | finally: 122 | LOGGER.debug("validate took %.5f seconds.", (time() - start_time)) 123 | -------------------------------------------------------------------------------- /f5_cccl/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/test/__init__.py -------------------------------------------------------------------------------- /f5_cccl/test/bigip_net_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "arps": [ 3 | { 4 | "kind": "tm:net:arp:arpstate", 5 | "name": "arp1", 6 | "partition": "test", 7 | "fullPath": "/test/arp1", 8 | "generation": 0, 9 | "selfLink": "https://localhost/mgmt/tm/net/arp/~test~arp1?ver=12.1.0", 10 | "ipAddress": "1.2.3.4", 11 | "macAddress": "12:ab:34:cd:56:ef" 12 | } 13 | ], 14 | "fdbTunnels": [ 15 | { 16 | "kind": "tm:net:fdb:tunnel:tunnelstate", 17 | "name": "tunnel1", 18 | "partition": "test", 19 | "fullPath": "/test/tunnel1", 20 | "generation": 80760, 21 | "selfLink": "https://localhost/mgmt/tm/net/fdb/tunnel/~test~tunnel1?ver=12.1.0", 22 | "recordsReference": { 23 | "kind": "tm:net:fdb:tunnel:records:recordscollectionstate", 24 | "selfLink": "https://localhost/mgmt/tm/net/fdb/tunnel/~test~tunnel1/records?ver=12.1.0", 25 | "isSubcollection": true, 26 | "items": [ 27 | { 28 | "kind": "tm:net:fdb:tunnel:records:recordsstate", 29 | "name": "12:ab:34:cd:56:ef", 30 | "fullPath": "12:ab:34:cd:56:ef", 31 | "generation": 80760, 32 | "selfLink": "https://localhost/mgmt/tm/net/fdb/tunnel/~test~tunnel1/records/12:ab:34:cd:56:ef?ver=12.1.0", 33 | "endpoint": "10.1.2.3" 34 | }, 35 | { 36 | "kind": "tm:net:fdb:tunnel:records:recordsstate", 37 | "name": "98:ab:76:cd:54:ef", 38 | "fullPath": "98:ab:76:cd:54:ef", 39 | "generation": 80760, 40 | "selfLink": "https://localhost/mgmt/tm/net/fdb/tunnel/~test~tunnel1/records/98:ab:76:cd:54:ef?ver=12.1.0", 41 | "endpoint": "10.2.3.4" 42 | } 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /f5_cccl/test/test_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | from f5_cccl.api import F5CloudServiceManager 17 | 18 | def test_create_cccl(bigip_proxy): 19 | """Test CCCL instantiation.""" 20 | bigip = bigip_proxy.mgmt_root() 21 | partition = 'test' 22 | user_agent = 'k8s-bigip-ctlr-1.2.1-abcdef' 23 | prefix = 'myprefix' 24 | 25 | cccl = F5CloudServiceManager( 26 | bigip, 27 | partition, 28 | user_agent=user_agent, 29 | prefix=prefix) 30 | 31 | assert partition == cccl.get_partition() 32 | assert user_agent in bigip.icrs.session.headers['User-Agent'] 33 | assert prefix == cccl._bigip_proxy._prefix 34 | -------------------------------------------------------------------------------- /f5_cccl/test/test_bigip.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # LTM resources 18 | from f5_cccl.resource.ltm.pool import IcrPool 19 | from f5_cccl.resource.ltm.virtual import VirtualServer 20 | from f5_cccl.resource.ltm.node import IcrNode 21 | from f5_cccl.resource.ltm.app_service import IcrApplicationService 22 | 23 | # NET resources 24 | from f5_cccl.resource.net.arp import IcrArp 25 | from f5_cccl.resource.net.fdb.tunnel import IcrFDBTunnel 26 | 27 | 28 | def test_bigip_refresh_ltm(bigip_proxy): 29 | """Test BIG-IP refresh_ltm function.""" 30 | big_ip = bigip_proxy.mgmt_root() 31 | 32 | test_pools = [ 33 | IcrPool(**p) for p in big_ip.bigip_data['pools'] 34 | if p['partition'] == 'test' 35 | ] 36 | test_virtuals = [ 37 | VirtualServer(default_route_domain=0, **v) 38 | for v in big_ip.bigip_data['virtuals'] 39 | if v['partition'] == 'test' 40 | ] 41 | test_iapps = [ 42 | IcrApplicationService(**i) for i in big_ip.bigip_data['iapps'] 43 | if i['partition'] == 'test' 44 | ] 45 | test_nodes = [ 46 | IcrNode(default_route_domain=0, **n) for n in big_ip.bigip_data['nodes'] 47 | if n['partition'] == 'test' 48 | ] 49 | 50 | # refresh the BIG-IP state 51 | bigip_proxy.refresh_ltm() 52 | 53 | # verify pools and pool members 54 | assert big_ip.tm.ltm.pools.get_collection.called 55 | assert len(bigip_proxy._pools) == 1 56 | 57 | assert len(bigip_proxy._pools) == len(test_pools) 58 | for pool in test_pools: 59 | assert bigip_proxy._pools[pool.name] == pool 60 | # Make a change, pools will not be equal 61 | pool._data['loadBalancingMode'] = 'Not a valid LB mode' 62 | assert bigip_proxy._pools[pool.name] != pool 63 | 64 | # verify virtual servers 65 | assert big_ip.tm.ltm.virtuals.get_collection.called 66 | assert len(bigip_proxy._virtuals) == 2 67 | 68 | assert len(bigip_proxy._virtuals) == len(test_virtuals) 69 | for v in test_virtuals: 70 | assert bigip_proxy._virtuals[v.name] == v 71 | # Make a change, virtuals will not be equal 72 | v._data['partition'] = 'NoPartition' 73 | assert bigip_proxy._virtuals[v.name] != v 74 | 75 | # verify application services 76 | assert big_ip.tm.sys.application.services.get_collection.called 77 | assert len(bigip_proxy._iapps) == 2 78 | 79 | assert len(bigip_proxy._iapps) == len(test_iapps) 80 | for i in test_iapps: 81 | assert bigip_proxy._iapps[i.name] == i 82 | # Make a change, iapps will not be equal 83 | i._data['template'] = '/Common/NoTemplate' 84 | assert bigip_proxy._iapps[i.name] != i 85 | 86 | # verify nodes 87 | assert big_ip.tm.ltm.nodes.get_collection.called 88 | assert len(bigip_proxy._nodes) == 4 89 | 90 | assert len(bigip_proxy._nodes) == len(test_nodes) 91 | for n in test_nodes: 92 | assert bigip_proxy._nodes[n.name] == n 93 | 94 | 95 | def test_bigip_refresh_net(bigip_proxy): 96 | """Test BIG-IP refresh_net function.""" 97 | bigip = bigip_proxy.mgmt_root() 98 | 99 | test_arps = [ 100 | IcrArp(**a) for a in bigip.bigip_net_data['arps'] 101 | if a['partition'] == 'test' 102 | ] 103 | test_tunnels = [ 104 | IcrFDBTunnel(default_route_domain=0, **t) for t in bigip.bigip_net_data['fdbTunnels'] 105 | if t['partition'] == 'test' 106 | ] 107 | 108 | # refresh the BIG-IP state 109 | bigip_proxy.refresh_net() 110 | 111 | # verify arps 112 | assert bigip.tm.net.arps.get_collection.called 113 | assert len(bigip_proxy._arps) == len(test_arps) 114 | for a in test_arps: 115 | assert bigip_proxy._arps[a.name] == a 116 | 117 | # verify fdb tunnels 118 | assert bigip.tm.net.fdb.tunnels.get_collection.called 119 | assert len(bigip_proxy._fdb_tunnels) == len(test_tunnels) 120 | for t in test_tunnels: 121 | assert bigip_proxy._fdb_tunnels[t.name] == t 122 | 123 | def test_bigip_properties(bigip_proxy): 124 | """Test BIG-IP properties function.""" 125 | big_ip = bigip_proxy 126 | 127 | test_pools = [ 128 | IcrPool(**p) for p in big_ip.mgmt_root().bigip_data['pools'] 129 | if p['partition'] == 'test' 130 | ] 131 | test_virtuals = [ 132 | VirtualServer(default_route_domain=0, **v) 133 | for v in big_ip.mgmt_root().bigip_data['virtuals'] 134 | if v['partition'] == 'test' 135 | ] 136 | 137 | # refresh the BIG-IP state 138 | big_ip.refresh_ltm() 139 | 140 | assert len(big_ip.get_pools()) == len(test_pools) 141 | for p in test_pools: 142 | assert big_ip._pools[p.name] == p 143 | 144 | assert len(big_ip.get_virtuals()) == len(test_virtuals) 145 | for v in test_virtuals: 146 | assert big_ip._virtuals[v.name] == v 147 | 148 | http_hc = big_ip.get_http_monitors() 149 | https_hc = big_ip.get_https_monitors() 150 | tcp_hc = big_ip.get_tcp_monitors() 151 | udp_hc = big_ip.get_udp_monitors() 152 | icmp_hc = big_ip.get_icmp_monitors() 153 | -------------------------------------------------------------------------------- /f5_cccl/test/test_exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | from f5_cccl import exceptions 18 | import pytest 19 | 20 | 21 | def test_create_f5ccclerror_nomsg(): 22 | """Test the creation of F5CcclError without message.""" 23 | e = exceptions.F5CcclError() 24 | 25 | assert e 26 | assert not e.msg 27 | assert "{}".format(e) == "F5CcclError" 28 | 29 | 30 | def test_create_f5ccclerror_msg(): 31 | """Test the creation of F5CcclError with message.""" 32 | error_msg = "Test CCCL Error" 33 | e = exceptions.F5CcclError(error_msg) 34 | 35 | assert e 36 | assert e.msg == error_msg 37 | assert "{}".format(e) == "F5CcclError - Test CCCL Error" 38 | 39 | 40 | def test_raise_f5ccclerror(): 41 | """Test raising a F5CcclError.""" 42 | with pytest.raises(exceptions.F5CcclError): 43 | def f(): 44 | raise exceptions.F5CcclError() 45 | 46 | f() 47 | 48 | 49 | def test_raise_f5cccl_resource_create_error(): 50 | """Test raising a F5CcclResourceCreateError.""" 51 | with pytest.raises(exceptions.F5CcclResourceCreateError): 52 | def f(): 53 | raise exceptions.F5CcclResourceCreateError() 54 | 55 | f() 56 | 57 | 58 | def test_raise_f5cccl_resource_conflict_error(): 59 | """Test raising a F5CcclConflictError.""" 60 | with pytest.raises(exceptions.F5CcclResourceConflictError): 61 | def f(): 62 | raise exceptions.F5CcclResourceConflictError() 63 | 64 | f() 65 | 66 | 67 | def test_raise_f5cccl_resource_notfound_error(): 68 | """Test raising a F5CcclResourceNotFoundError.""" 69 | with pytest.raises(exceptions.F5CcclResourceNotFoundError): 70 | def f(): 71 | raise exceptions.F5CcclResourceNotFoundError() 72 | 73 | f() 74 | 75 | 76 | def test_raise_f5cccl_resource_request_error(): 77 | """Test raising a F5CcclResourceRequestError.""" 78 | with pytest.raises(exceptions.F5CcclResourceRequestError): 79 | def f(): 80 | raise exceptions.F5CcclResourceRequestError() 81 | 82 | f() 83 | 84 | 85 | def test_raise_f5cccl_resource_update_error(): 86 | """Test raising a F5CcclResourceUpdateError.""" 87 | with pytest.raises(exceptions.F5CcclResourceUpdateError): 88 | def f(): 89 | raise exceptions.F5CcclResourceUpdateError() 90 | 91 | f() 92 | 93 | 94 | def test_raise_f5cccl_resource_delete_error(): 95 | """Test raising a F5CcclResourceDeleteError.""" 96 | with pytest.raises(exceptions.F5CcclResourceDeleteError): 97 | def f(): 98 | raise exceptions.F5CcclResourceDeleteError() 99 | 100 | f() 101 | 102 | 103 | def test_raise_f5cccl_configuration_read_error(): 104 | """Test raising a F5CcclConfigurationReadError.""" 105 | with pytest.raises(exceptions.F5CcclConfigurationReadError): 106 | def f(): 107 | raise exceptions.F5CcclConfigurationReadError() 108 | 109 | f() 110 | -------------------------------------------------------------------------------- /f5_cccl/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/f5devcentral/f5-cccl/497c325211de2191afe1ffaef673df07f7bb7c26/f5_cccl/utils/__init__.py -------------------------------------------------------------------------------- /f5_cccl/utils/json_pos_patch.py: -------------------------------------------------------------------------------- 1 | """Provides functions for making jsonpatch positionally independent.""" 2 | # coding=utf-8 3 | # 4 | # Copyright 2018 F5 Networks Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | 20 | import hashlib 21 | import json 22 | import logging 23 | 24 | 25 | LOGGER = logging.getLogger(__name__) 26 | 27 | 28 | def convert_from_positional_patch(data, patch_obj): 29 | """Replace array indexes with unique ID (hash). 30 | 31 | Patches that refer to an array index must be modified so 32 | that they refer to the hash value of the content (whether it's 33 | a simple value or a complex dictionary). 34 | 35 | Patches are a list of dictionaries values with each value 36 | specifying a json path, an operation, and possibly a value. 37 | For example: 38 | {'path': '/c/0/value', 'value': 1, 'op': 'replace'} 39 | (other operations are 'add', 'remove', and 'move') 40 | 41 | Note: This requires the content to be unique among array entries 42 | (seems to be the case for Big-IP objects) 43 | """ 44 | 45 | LOGGER.debug("convert_from_positional_patch data: %s", data) 46 | if patch_obj is None: 47 | return 48 | patch = patch_obj.patch 49 | LOGGER.debug("convert_from_positional_patch indexed patch: %s", patch) 50 | 51 | for entry in patch: 52 | ptr = data 53 | new_path = "" 54 | for sub_path in entry['path'].split('/'): 55 | if not sub_path: 56 | # ignore the first subpath (before the leading slash) 57 | continue 58 | elif ptr is None: 59 | # continue to add path since we are done with substitutions 60 | new_path += '/' 61 | new_path += sub_path 62 | elif sub_path.isdigit(): 63 | new_path += '/' 64 | ptr = ptr[int(sub_path)] 65 | str_content = json.dumps(ptr) 66 | new_path += \ 67 | '[{}]'.format(hashlib.sha256(bytes( 68 | str_content.encode('utf-8'))).hexdigest()) 69 | else: 70 | new_path += '/' 71 | new_path += sub_path 72 | if sub_path in ptr: 73 | ptr = ptr[sub_path] 74 | else: 75 | # done with substitutions, so just finish up remaining path 76 | ptr = None 77 | entry['path'] = new_path 78 | LOGGER.debug("convert_from_positional_patch hashed patch: %s", 79 | patch_obj.patch) 80 | 81 | 82 | def convert_to_positional_patch(data, # pylint: disable=too-many-branches 83 | patch_obj): 84 | """Replace arrays indexed by a hash value with a positional index value. 85 | 86 | The index ID is the shasum hash value of the object. If the ID isn't 87 | found remove the patch entry. 88 | 89 | Note: A limitation of this function is if the user managed to 90 | change the content of the hashed object, we would treat 91 | it as a uniquely different object. Fortunately, this 92 | doesn't seem possible with Big-IP gui. 93 | """ 94 | 95 | LOGGER.debug("convert_to_positional_patch data: %s", data) 96 | if patch_obj is None: 97 | return 98 | patch = patch_obj.patch 99 | LOGGER.debug("convert_to_positional_patch hashed patch: %s", patch) 100 | 101 | entry_cnt = len(patch) 102 | entry_idx = 0 103 | while entry_idx < entry_cnt: 104 | entry = patch[entry_idx] 105 | ptr = data 106 | new_path = "" 107 | for sub_path in entry['path'].split('/'): 108 | if not sub_path: 109 | # ignore the first subpath (before the leading slash) 110 | continue 111 | elif ptr is None: 112 | # continue to add path since we are done with substitutions 113 | new_path += '/' 114 | new_path += sub_path 115 | elif sub_path[0] == '[' and sub_path[len(sub_path)-1] == ']': 116 | uuid = sub_path[1:-1] 117 | new_path += '/' 118 | for content_idx, content in enumerate(ptr): 119 | str_content = json.dumps(content) 120 | content_uuid = hashlib.sha256(bytes( 121 | str_content.encode('utf-8'))).hexdigest() 122 | if content_uuid == uuid: 123 | new_path += str(content_idx) 124 | ptr = ptr[content_idx] 125 | break 126 | else: 127 | # entry no longer exists so we can't remove it 128 | new_path = None 129 | break 130 | else: 131 | new_path += '/' 132 | new_path += sub_path 133 | if sub_path in ptr: 134 | ptr = ptr[sub_path] 135 | else: 136 | # done with substitutions so just finish up remaining path 137 | ptr = None 138 | if entry['op'] == 'remove': 139 | # entry no longer exists so we must delete this patch 140 | new_path = None 141 | break 142 | if new_path: 143 | entry['path'] = new_path 144 | entry_idx += 1 145 | else: 146 | del patch[entry_idx] 147 | entry_cnt -= 1 148 | LOGGER.debug("convert_to_positional_patch indexed patch: %s", 149 | patch_obj.patch) 150 | -------------------------------------------------------------------------------- /f5_cccl/utils/mgmt.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Wrapper functions for the f5-sdk""" 18 | 19 | from f5.bigip import ManagementRoot 20 | 21 | 22 | def mgmt_root(host, username, password, port, token): 23 | """Create a BIG-IP Management Root object""" 24 | return ManagementRoot(host, username, password, port=port, token=token) 25 | -------------------------------------------------------------------------------- /f5_cccl/utils/resource_merge.py: -------------------------------------------------------------------------------- 1 | """Provides functions for merging identical BIG-IP resources togehter.""" 2 | # coding=utf-8 3 | # 4 | # Copyright 2018 F5 Networks Inc. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import copy 20 | import logging 21 | 22 | from functools import reduce as reducer # name conflict between python 2 & 3 23 | from itertools import groupby 24 | from operator import itemgetter 25 | 26 | 27 | LOGGER = logging.getLogger(__name__) 28 | 29 | 30 | def _merge_dict_by(): 31 | """Returns a function that merges two dictionaries dst and src. 32 | 33 | Keys are merged together between the dst and src dictionaries. 34 | If there is a conflict, the src values take precedence. 35 | """ 36 | 37 | return lambda dst, src: { 38 | key: src[key] if key in src else dst[key] 39 | for key in list(set(dst.keys()) | set(src.keys())) 40 | } 41 | 42 | 43 | def _merge_list_of_dict_by(key): 44 | """Returns a function that merges a list of dictionary records 45 | 46 | Records are grouped by the specified key. 47 | """ 48 | 49 | keyprop = itemgetter(key) 50 | return lambda lst: [ 51 | reducer(_merge_dict_by(), records) 52 | for _, records in groupby(sorted(lst, key=keyprop), keyprop) 53 | ] 54 | 55 | 56 | def _merge_list_of_dict_by_name(dst, src): 57 | """Merge list of Big-IP dictionary records uniquely identified by name. 58 | 59 | Duplicates are not merged, but replaced. Src is added to front. 60 | """ 61 | merge_list = [] 62 | merge_set = set() 63 | for record in src: 64 | merge_list.append(record) 65 | merge_set.add(record['name']) 66 | for record in dst: 67 | if record['name'] not in merge_set: 68 | merge_list.append(record) 69 | return merge_list 70 | 71 | 72 | def _merge_list_of_scalars(dst, src): 73 | """Merge list of scalars (add src first, then remaining unique dst)""" 74 | dst_copy = copy.copy(dst) 75 | src_set = set(src) 76 | dst = copy.copy(src) 77 | for val in dst_copy: 78 | if val not in src_set: 79 | dst.append(val) 80 | return dst 81 | 82 | 83 | def _merge_list(dst, src): 84 | """Merge lists of a particular type 85 | 86 | Limitations: lists must be of a uniform type: scalar, list, or dict 87 | """ 88 | 89 | if not dst: 90 | return src 91 | if isinstance(dst[0], dict): 92 | dst = _merge_list_of_dict_by_name(dst, src) 93 | elif isinstance(dst[0], list): 94 | # May cause duplicates (what is a duplicate for lists of lists?) 95 | dst = src + dst 96 | else: 97 | dst = _merge_list_of_scalars(dst, src) 98 | return dst 99 | 100 | 101 | def _merge_dict(dst, src): 102 | """Merge two dictionaries together, with src overridding dst fields.""" 103 | 104 | for key in list(src.keys()): 105 | dst[key] = merge(dst[key], src[key]) if key in dst else src[key] 106 | return dst 107 | 108 | 109 | def merge(dst, src): 110 | """Merge two resources together with the src fields taking precedence. 111 | 112 | Note: this is specifically tailored for Big-IP resources and 113 | does not generically support all type variations) 114 | """ 115 | 116 | LOGGER.debug("Merging source: %s", src) 117 | LOGGER.debug("Merging destination: %s", dst) 118 | # pylint: disable=C0123 119 | if type(dst) != type(src): 120 | # can't merge differing types, src wins everytime 121 | # (maybe this should be an error) 122 | dst = copy.deepcopy(src) 123 | elif isinstance(dst, dict): 124 | return _merge_dict(dst, src) 125 | elif isinstance(dst, list): 126 | return _merge_list(dst, src) 127 | else: 128 | # scalar 129 | dst = src 130 | LOGGER.debug("Merged result: %s", dst) 131 | return dst 132 | -------------------------------------------------------------------------------- /f5_cccl/utils/route_domain.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | """Helper functions for supporting route domains""" 18 | 19 | import re 20 | from requests.utils import quote as urlquote 21 | from requests.utils import unquote as urlunquote 22 | 23 | 24 | # Pattern: % 25 | ip_rd_re = re.compile(r'^([^%]*)%(\d+)$') 26 | 27 | # Pattern: /%[:|.] 28 | path_ip_rd_port_re = re.compile(r'^(.+)/(.+)%(\d+)[:|\.](.+)$') 29 | 30 | 31 | def combine_ip_and_route_domain(ip, route_domain): 32 | """Return address that includes IP and route domain 33 | 34 | Input ip format must be of the form: 35 | 36 | """ 37 | address = "{}%{}".format(ip, route_domain) 38 | return address 39 | 40 | 41 | def split_ip_with_route_domain(address): 42 | """Return ip and route-domain parts of address 43 | 44 | Input ip format must be of the form: 45 | [%] 46 | """ 47 | match = ip_rd_re.match(address) 48 | if match: 49 | ip = match.group(1) 50 | route_domain = int(match.group(2)) 51 | else: 52 | ip = address 53 | route_domain = None 54 | 55 | return ip, route_domain 56 | 57 | 58 | def normalize_address_with_route_domain(address, default_route_domain): 59 | """Return address with the route domain 60 | 61 | Return components of address, using the default route domain 62 | for the partition if one is not already specified. 63 | 64 | Input address is of the form: 65 | [%] 66 | """ 67 | match = ip_rd_re.match(address) 68 | if match: 69 | ip = match.group(1) 70 | route_domain = int(match.group(2)) 71 | else: 72 | route_domain = default_route_domain 73 | ip = address 74 | address = combine_ip_and_route_domain(ip, route_domain) 75 | 76 | return address, ip, route_domain 77 | 78 | 79 | def encoded_normalize_address_with_route_domain(address, 80 | default_route_domain, 81 | inputUrlEncoded, 82 | outputUrlEncoded): 83 | """URL Encoded-aware version of normalize_address_with_route_domain""" 84 | if inputUrlEncoded: 85 | address = urlunquote(address) 86 | 87 | address = normalize_address_with_route_domain(address, 88 | default_route_domain)[0] 89 | 90 | if outputUrlEncoded: 91 | address = urlquote(address) 92 | return address 93 | 94 | 95 | def split_fullpath_with_route_domain(address): 96 | """Determine the individual components of an address path 97 | 98 | Input address format must be of the form: 99 | /%[:|.] 100 | """ 101 | match = path_ip_rd_port_re.match(address) 102 | if match: 103 | path = match.group(1) 104 | ip = match.group(2) 105 | route_domain = int(match.group(3)) 106 | port = int(match.group(4)) 107 | return path, ip, route_domain, port 108 | 109 | # Future enhancment: we could pass in the default route domain 110 | # and then return path, ip, default_route_domain, port 111 | # (current implementation doesn't need this) 112 | return None, None, None, None 113 | -------------------------------------------------------------------------------- /f5_cccl/utils/test/test_route_domain.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | from requests.utils import quote as urlquote 19 | 20 | from f5_cccl.utils.route_domain \ 21 | import combine_ip_and_route_domain 22 | from f5_cccl.utils.route_domain \ 23 | import encoded_normalize_address_with_route_domain 24 | from f5_cccl.utils.route_domain \ 25 | import normalize_address_with_route_domain 26 | from f5_cccl.utils.route_domain \ 27 | import split_ip_with_route_domain 28 | from f5_cccl.utils.route_domain \ 29 | import split_fullpath_with_route_domain 30 | 31 | 32 | def test_combine_ip_and_route_domain(): 33 | """Test proper behavior of combine_ip_and_route_domain.""" 34 | 35 | tests = [ 36 | ["1.2.3.4", 12, "1.2.3.4%12"], 37 | ["64:ff9b::", 13, "64:ff9b::%13"] 38 | ] 39 | for test in tests: 40 | result = combine_ip_and_route_domain(test[0], test[1]) 41 | assert result == test[2] 42 | 43 | # def combine_ip_and_route_domain(ip, route_domain): 44 | # u"""Return address that includes IP and route domain 45 | 46 | # Input ip format must be of the form: 47 | # 48 | # """ 49 | # address = "{}%{}".format(ip, route_domain) 50 | # return address 51 | 52 | 53 | def test_split_ip_with_route_domain(): 54 | """Test proper behavior of split_ip_with_route_domain.""" 55 | 56 | tests = [ 57 | ["1.2.3.4%1", "1.2.3.4", 1], 58 | ["1.2.3.4", "1.2.3.4", None], 59 | ["64:ff9b::%2", "64:ff9b::", 2], 60 | ["64:ff9b::", "64:ff9b::", None] 61 | ] 62 | for test in tests: 63 | results = split_ip_with_route_domain(test[0]) 64 | assert results[0] == test[1] 65 | assert results[1] == test[2] 66 | 67 | def test_normalize_address_with_route_domain(): 68 | """Test proper behavior of normalize_address_with_route_domain.""" 69 | 70 | # If route domain is not specified, add the default 71 | tests = [ 72 | ["1.2.3.4%1", 2, "1.2.3.4%1", "1.2.3.4", 1], 73 | ["1.2.3.4", 2, "1.2.3.4%2", "1.2.3.4", 2], 74 | ["64:ff9b::%1", 2, "64:ff9b::%1", "64:ff9b::", 1], 75 | ["64:ff9b::", 2, "64:ff9b::%2", "64:ff9b::", 2] 76 | ] 77 | for test in tests: 78 | results = normalize_address_with_route_domain(test[0], test[1]) 79 | assert results[0] == test[2] 80 | assert results[1] == test[3] 81 | assert results[2] == test[4] 82 | 83 | def test_encoded_normalize_address_with_route_domain(): 84 | """Test proper behavior of encoded_normalize_address_with_route_domain.""" 85 | 86 | # test wrapper for test_normalize_address_with_route_domain but with 87 | # address input/output being either url encoded or url unencoded 88 | tests = [ 89 | ["1.2.3.4%1", 2, False, False, "1.2.3.4%1"], 90 | ["1.2.3.4%1", 2, False, True, urlquote("1.2.3.4%1")], 91 | [urlquote("1.2.3.4%1"), 2, True, False, "1.2.3.4%1"], 92 | [urlquote("1.2.3.4%1"), 2, True, True, urlquote("1.2.3.4%1")], 93 | 94 | ["64:ff9b::", 2, False, False, "64:ff9b::%2"], 95 | ["64:ff9b::", 2, False, True, urlquote("64:ff9b::%2")], 96 | [urlquote("64:ff9b::"), 2, True, False, "64:ff9b::%2"], 97 | [urlquote("64:ff9b::"), 2, True, True, urlquote("64:ff9b::%2")] 98 | ] 99 | 100 | for test in tests: 101 | result = encoded_normalize_address_with_route_domain( 102 | test[0], test[1], test[2], test[3]) 103 | assert result == test[4] 104 | 105 | def test_split_fullpath_with_route_domain(): 106 | """Test proper behavior of split_fullpath_with_route_domain.""" 107 | 108 | # Expected input must have route specified, otherwise reject 109 | tests = [ 110 | ["/Partition/1.2.3.4%0:80", "/Partition", "1.2.3.4", 0, 80], 111 | ["/Part/Folder/1.2.3.4%1:443", "/Part/Folder", "1.2.3.4", 1, 443], 112 | ["/Part/::ffff:0:0%2.8080", "/Part", "::ffff:0:0", 2, 8080], 113 | ["/Part/1.2.3.4:8080", None, None, None, None], 114 | ["/Part/::ffff:0:0.8080", None, None, None, None] 115 | ] 116 | 117 | for test in tests: 118 | results = split_fullpath_with_route_domain(test[0]) 119 | assert results[0] == test[1] 120 | assert results[1] == test[2] 121 | assert results[2] == test[3] 122 | assert results[3] == test[4] 123 | -------------------------------------------------------------------------------- /requirements.docs.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | # Docs requirements 4 | Sphinx>=1.4.1 5 | six>=1.10.0 6 | sphinx_rtd_theme 7 | -------------------------------------------------------------------------------- /requirements.test.txt: -------------------------------------------------------------------------------- 1 | # Test Requirements 2 | 3 | f5-sdk==3.0.21 4 | #f5-icontrol-rest==1.3.13 5 | ipaddress==1.0.17 6 | PyJWT==2.4.0 7 | mock==2.0.0 8 | pytest==4.6.11 9 | pytest-cov>=2.2.1 10 | pytest-benchmark==3.1.1 11 | python-coveralls==2.9.3 12 | pyOpenSSL==17.5.0 13 | requests-mock==1.2.0 14 | flake8 15 | pylint 16 | netaddr 17 | q 18 | PyYAML==5.4.1 19 | simplejson 20 | jsonschema==3.0.0 21 | jsonpatch==1.16 22 | requests==2.31.0 23 | asn1crypto==0.24.0 24 | certifi==2023.7.22 25 | chardet==3.0.4 26 | enum34==1.1.6 27 | cryptography==3.3.2 28 | idna==2.7 29 | pycparser==2.19 30 | urllib3==1.26.18 31 | six==1.15.0 32 | attrs==18.2.0 33 | cffi==1.12.2 34 | pyrsistent==0.14.11 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2014-2021, F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | try: # for pip >= 10 19 | from pip._internal.req import parse_requirements as parse_reqs 20 | except ImportError: # for pip <= 9.0.3 21 | from pip.req import parse_requirements as parse_reqs 22 | from setuptools import setup 23 | from setuptools import find_packages 24 | 25 | import f5_cccl 26 | 27 | install_requires = [str(x.req) for x in parse_reqs('./setup_requirements.txt', 28 | session='setup')] 29 | 30 | print(('install_requires', install_requires)) 31 | setup( 32 | name='f5-cccl', 33 | description='F5 Networks Common Controller Core Library', 34 | license='Apache License, Version 2.0', 35 | version=f5_cccl.__version__, 36 | author='F5 Networks', 37 | url='https://github.com/f5devcentral/f5-cccl', 38 | keywords=['F5', 'big-ip'], 39 | install_requires=install_requires, 40 | packages=find_packages(exclude=['*.test', '*.test.*', 'test*', 'test']), 41 | data_files=[], 42 | package_data={ 43 | 'f5_cccl': ['schemas/*.yaml', 'schemas/*.json', 'schemas/*.yml'], 44 | }, 45 | classifiers=[ 46 | ], 47 | entry_points={} 48 | ) 49 | -------------------------------------------------------------------------------- /setup_requirements.txt: -------------------------------------------------------------------------------- 1 | # F5-CCCL Install Requirements 2 | #f5-icontrol-rest==1.3.13 3 | f5-sdk==3.0.21 4 | ipaddress==1.0.17 5 | netaddr==0.7.19 6 | PyJWT==2.4.0 7 | PyYAML==6.0.1 8 | requests==2.32.0 9 | simplejson==3.10.0 10 | jsonpatch==1.16 11 | jsonpointer==2.0 12 | jsonschema==3.0.0 13 | asn1crypto==0.24.0 14 | certifi==2024.07.04 15 | chardet==3.0.4 16 | enum34==1.1.6 17 | idna==3.7 18 | pycparser==2.19 19 | urllib3==1.26.19 20 | six==1.12.0 21 | attrs==18.2.0 22 | cffi==1.15.0 23 | pyrsistent==0.14.11 24 | setuptools==78.1.1 25 | -------------------------------------------------------------------------------- /test/f5_cccl/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import pytest 19 | import requests 20 | 21 | from mock import MagicMock 22 | 23 | from f5_cccl.api import F5CloudServiceManager 24 | 25 | from f5.bigip import ManagementRoot 26 | 27 | from icontrol.exceptions import iControlUnexpectedHTTPError 28 | 29 | requests.packages.urllib3.disable_warnings() 30 | 31 | 32 | def _wrap_instrument(f, counters, name): 33 | def instrumented(*args, **kwargs): 34 | counters[name] += 1 35 | return f(*args, **kwargs) 36 | return instrumented 37 | 38 | 39 | def instrument_bigip(mgmt_root): 40 | icr = mgmt_root.__dict__['_meta_data']['icr_session'] 41 | counters = {} 42 | mgmt_root.test_rest_calls = counters 43 | 44 | for method in ['get', 'put', 'delete', 'patch', 'post']: 45 | counters[method] = 0 46 | orig = getattr(icr.session, method) 47 | instrumented = _wrap_instrument(orig, counters, method) 48 | setattr(icr.session, method, instrumented) 49 | return mgmt_root 50 | 51 | 52 | @pytest.fixture(scope="module") 53 | def bigip(): 54 | if pytest.symbols: 55 | hostname = pytest.symbols.bigip_mgmt_ip 56 | username = pytest.symbols.bigip_username 57 | password = pytest.symbols.bigip_password 58 | port = pytest.symbols.bigip_port 59 | 60 | bigip_fix = ManagementRoot(hostname, username, password, port=port) 61 | bigip_fix = instrument_bigip(bigip_fix) 62 | else: 63 | bigip_fix = MagicMock() 64 | 65 | yield bigip_fix 66 | 67 | 68 | @pytest.fixture(scope="function") 69 | def bigip_rest_counters(bigip): 70 | counters = bigip.test_rest_calls 71 | for k in list(counters.keys()): 72 | counters[k] = 0 73 | 74 | yield counters 75 | 76 | for k in list(counters.keys()): 77 | counters[k] = 0 78 | 79 | 80 | @pytest.fixture(scope="function") 81 | def partition(bigip): 82 | name = "Test1" 83 | partition = None 84 | 85 | # Cleanup partition, in case previous runs were interrupted 86 | try: 87 | bigip.tm.ltm.virtuals.virtual.load( 88 | name="test_virtual", partition=name).delete() 89 | except iControlUnexpectedHTTPError as icr_error: 90 | pass 91 | try: 92 | bigip.tm.auth.partitions.partition.load(name=name).delete() 93 | except iControlUnexpectedHTTPError as icr_error: 94 | pass 95 | 96 | try: 97 | partition = bigip.tm.auth.partitions.partition.create(subPath="/", name=name) 98 | except iControlUnexpectedHTTPError as icr_error: 99 | code = icr_error.response.status_code 100 | if code == 400: 101 | print(("Can't create partition {}".format(name))) 102 | elif code == 409: 103 | print(("Partition {} already exists".format(name))) 104 | partition = bigip.tm.auth.partitions.partition.load(subPath="/", name=name) 105 | else: 106 | print("Unknown error creating partition.") 107 | print(icr_error) 108 | 109 | yield name 110 | 111 | for pool in bigip.tm.ltm.pools.get_collection(): 112 | if pool.partition == name: 113 | pool.delete() 114 | for virtual in bigip.tm.ltm.virtuals.get_collection(): 115 | if virtual.partition == name: 116 | virtual.delete() 117 | partition.delete() 118 | 119 | 120 | @pytest.fixture(scope="function") 121 | def cccl(bigip, partition): 122 | cccl = F5CloudServiceManager(bigip, partition) 123 | yield cccl 124 | cccl.apply_ltm_config({}) 125 | 126 | 127 | @pytest.fixture() 128 | def pool(bigip, partition): 129 | name = "pool1" 130 | partition = partition 131 | model = {'name': name, 'partition': partition} 132 | 133 | try: 134 | pool = bigip.tm.ltm.pools.pool.create(**model) 135 | except iControlUnexpectedHTTPError as icr_error: 136 | code = icr_error.response.status_code 137 | if code == 400: 138 | print(("Can't create pool {}".format(name))) 139 | elif code == 409: 140 | print(("Pool {} already exists".format(name))) 141 | partition = bigip.tm.ltm.pools.pool.load(partition=partition, 142 | name=name) 143 | else: 144 | print("Unknown error creating pool.") 145 | print(icr_error) 146 | 147 | yield name 148 | 149 | pool.delete() 150 | -------------------------------------------------------------------------------- /test/f5_cccl/perf/test_perf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Copyright (c) 2017-2021 F5 Networks, Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | import pytest 19 | import requests 20 | 21 | from pprint import pprint 22 | 23 | requests.packages.urllib3.disable_warnings() 24 | 25 | req_symbols = ['bigip_mgmt_ip', 'bigip_username', 'bigip_password', 'bigip_port'] 26 | 27 | 28 | def missing_bigip_symbols(): 29 | for sym in req_symbols: 30 | if not hasattr(pytest.symbols, sym): 31 | return True 32 | return False 33 | 34 | 35 | pytestmark = pytest.mark.skipif(missing_bigip_symbols(), 36 | reason="Need symbols pointing at a real bigip.") 37 | 38 | 39 | def _make_svc_config(partition, num_virtuals=0, num_members=0): 40 | base_virtual = { 41 | 'name': 'Virtual-1', 42 | 'destination': '/{}/1.2.3.4:80'.format(partition), 43 | 'ipProtocol': 'tcp', 44 | 'profiles': [ 45 | { 46 | 'name': "tcp", 47 | 'partition': "Common", 48 | 'context': "all" 49 | } 50 | ], 51 | "enabled": True, 52 | "vlansEnabled": True, 53 | "sourceAddressTranslation": { 54 | "type": "automap", 55 | } 56 | } 57 | base_pool = { 58 | "name": "pool1", 59 | "monitors": ["/Common/http"] 60 | } 61 | base_member = { 62 | "address": "172.16.0.100%0", "port": 8080 63 | } 64 | cfg = { 65 | 'virtualServers': [], 66 | 'pools': [], 67 | } 68 | for i in range(num_virtuals): 69 | v = {} 70 | v.update(base_virtual) 71 | v['name'] = "virtual-{}".format(i) 72 | v['pool'] = "/{}/pool-{}".format(partition, i) 73 | cfg['virtualServers'].append(v) 74 | 75 | p = {} 76 | p.update(base_pool) 77 | p['name'] = "pool-{}".format(i) 78 | 79 | members = [] 80 | for j in range(num_members): 81 | m = {} 82 | m.update(base_member) 83 | m['address'] = '172.16.0.{}'.format(j) 84 | members.append(m) 85 | p['members'] = members 86 | cfg['pools'].append(p) 87 | return cfg 88 | 89 | 90 | testdata = [ 91 | (1, 1), 92 | (10, 10), 93 | (100, 10), 94 | (10, 100), 95 | ] 96 | 97 | 98 | @pytest.mark.parametrize("nv,nm", testdata) 99 | @pytest.mark.benchmark(group="apply-new") 100 | def test_apply_new(partition, cccl, bigip_rest_counters, benchmark, nv, nm): 101 | cfg = _make_svc_config(partition, num_virtuals=nv, num_members=nm) 102 | 103 | def setup(): 104 | cccl.apply_ltm_config({}) 105 | 106 | def apply_cfg(): 107 | cccl.apply_ltm_config(cfg) 108 | 109 | benchmark.pedantic(apply_cfg, setup=setup, rounds=2, iterations=1) 110 | pprint(bigip_rest_counters) 111 | 112 | 113 | @pytest.mark.parametrize("nv,nm", testdata) 114 | @pytest.mark.benchmark(group="apply-no-change") 115 | def test_apply_no_change(partition, cccl, bigip_rest_counters, benchmark, nv, nm): 116 | cfg = _make_svc_config(partition, num_virtuals=nv, num_members=nm) 117 | 118 | def apply_cfg(): 119 | cccl.apply_ltm_config(cfg) 120 | 121 | apply_cfg() 122 | benchmark.pedantic(apply_cfg, rounds=2, iterations=1) 123 | pprint(bigip_rest_counters) 124 | -------------------------------------------------------------------------------- /test/f5_cccl/resource/ltm/monitor/monitor_schemas.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import f5_cccl.resource.ltm.monitor.monitor as default_monitor 18 | import f5_cccl.resource.ltm.monitor.http_monitor as http 19 | import f5_cccl.resource.ltm.monitor.https_monitor as https 20 | import f5_cccl.resource.ltm.monitor.icmp_monitor as icmp 21 | import f5_cccl.resource.ltm.monitor.tcp_monitor as tcp 22 | 23 | """A repository of default schemas importable abstracted for tests. 24 | """ 25 | 26 | # Monitor: 27 | default_schema = default_monitor.default_schema 28 | 29 | # HTTP: 30 | http_default = http.default_schema 31 | 32 | # HTTPS: 33 | https_default = https.default_schema 34 | 35 | # ICMP: 36 | icmp_default = icmp.default_schema 37 | 38 | # TCP: 39 | tcp_default = tcp.default_schema 40 | -------------------------------------------------------------------------------- /test/f5_cccl/service/test_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # Copyright (c) 2017-2021 F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | import json 18 | import pickle 19 | import pytest 20 | 21 | from f5_cccl.bigip import BigIPProxy 22 | from f5_cccl.resource.ltm.app_service import ApplicationService 23 | from f5_cccl.resource.ltm.virtual import VirtualServer 24 | from f5_cccl.resource.ltm.pool import Pool 25 | from f5_cccl.resource.ltm.monitor.http_monitor import HTTPMonitor 26 | from f5_cccl.resource.ltm.policy.policy import Policy 27 | from f5_cccl.resource.ltm.internal_data_group import InternalDataGroup 28 | from f5_cccl.resource.ltm.irule import IRule 29 | from f5_cccl.resource.net.arp import Arp 30 | from f5_cccl.resource.net.fdb.tunnel import FDBTunnel 31 | 32 | from f5_cccl.service.manager import ServiceConfigDeployer 33 | from f5_cccl.service.manager import ServiceManager 34 | from f5_cccl.service.config_reader import ServiceConfigReader 35 | 36 | from icontrol.exceptions import iControlUnexpectedHTTPError 37 | 38 | from mock import MagicMock 39 | from mock import Mock 40 | from mock import patch 41 | 42 | import logging 43 | 44 | LOGGER = logging.getLogger(__name__) 45 | LOGGER.setLevel(logging.DEBUG) 46 | 47 | TEST_USER_AGENT='k8s-bigip-ctlr-v1.4.0' 48 | 49 | 50 | req_symbols = ['bigip_mgmt_ip', 'bigip_username', 'bigip_password', 'bigip_port'] 51 | 52 | 53 | def missing_bigip_symbols(): 54 | for sym in req_symbols: 55 | if not hasattr(pytest.symbols, sym): 56 | return True 57 | return False 58 | 59 | 60 | pytestmark = pytest.mark.skipif(missing_bigip_symbols(), 61 | reason="Need symbols pointing at a real bigip.") 62 | 63 | 64 | @pytest.fixture 65 | def bigip_proxy(bigip, partition): 66 | yield BigIPProxy(bigip, partition) 67 | 68 | 69 | @pytest.fixture 70 | def ltm_service_manager(bigip_proxy, partition): 71 | schema = 'f5_cccl/schemas/cccl-ltm-api-schema.yml' 72 | service_mgr = ServiceManager( 73 | bigip_proxy, 74 | partition, 75 | schema 76 | ) 77 | return service_mgr 78 | 79 | 80 | class TestServiceConfigDeployer: 81 | 82 | def _get_policy_from_bigip(self, name, bigip, partition): 83 | try: 84 | icr_policy = bigip.tm.ltm.policys.policy.load( 85 | name=name, partition=partition, 86 | requests_params={'params': "expandSubcollections=true"}) 87 | code = 200 88 | except iControlUnexpectedHTTPError as err: 89 | icr_policy = None 90 | code = err.response.status_code 91 | 92 | return icr_policy, code 93 | 94 | def test_deploy_ltm(self, bigip, partition, ltm_service_manager): 95 | ltm_svcfile = 'f5_cccl/schemas/tests/test_policy_schema_01.json' 96 | with open(ltm_svcfile, 'r') as fp: 97 | test_service1 = json.loads(fp.read()) 98 | 99 | policy1 = test_service1['l7Policies'][0]['name'] 100 | tasks_remaining = ltm_service_manager.apply_ltm_config( 101 | test_service1, TEST_USER_AGENT 102 | ) 103 | assert 0 == tasks_remaining 104 | 105 | # Get the policy from the bigip. 106 | (icr_policy, code) = self._get_policy_from_bigip( 107 | policy1, bigip, partition 108 | ) 109 | 110 | # Assert object exists and test attributes. 111 | assert icr_policy 112 | assert icr_policy.raw['name'] == policy1 113 | 114 | tasks_remaining = ltm_service_manager.apply_ltm_config( 115 | {}, TEST_USER_AGENT 116 | ) 117 | assert 0 == tasks_remaining 118 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | unit 4 | style 5 | coverage 6 | functional 7 | flake 8 | docs 9 | 10 | [testenv] 11 | basepython = 12 | unit: python 13 | style: python 14 | coverage: python 15 | functional: python 16 | flake: python 17 | docs: python 18 | passenv = COVERALLS_REPO_TOKEN 19 | deps = 20 | -rrequirements.test.txt 21 | 22 | # To get the lines that were not executed in unit testing add --cov-report term-missing 23 | # 24 | commands = 25 | # Misc tests 26 | unit: py.test --cov=f5_cccl/ {posargs:./f5_cccl} 27 | style: flake8 {posargs:.} 28 | style: pylint f5_cccl/ 29 | coverage: coveralls 30 | flake: flake8 {posargs:.} 31 | functional: py.test {posargs:./test} 32 | docs: bash ./devtools/bin/build-docs.sh 33 | usedevelop = true 34 | 35 | [flake8] 36 | exclude = docs/conf.py,docs/userguide/code_example.py,docs/conf.py,.tox,.git,__pycache__,build,*.pyc,docs,devtools,*.tmpl,*test* 37 | --------------------------------------------------------------------------------