├── ChangeLog ├── octavia_f5 ├── controller │ ├── statusmanager │ │ ├── legacy_healthmanager │ │ │ ├── health_drivers │ │ │ │ ├── __init__.py │ │ │ │ └── update_base.py │ │ │ └── __init__.py │ │ └── __init__.py │ ├── __init__.py │ └── worker │ │ ├── __init__.py │ │ ├── flows │ │ └── f5_flows_rseries.py │ │ ├── set_queue.py │ │ └── quirks.py ├── db │ ├── migration │ │ ├── alembic_migrations │ │ │ ├── script.py.mako │ │ │ ├── env.py │ │ │ └── versions │ │ │ │ └── 5f23f3721f6b_inital_create.py │ │ ├── __init__.py │ │ ├── alembic.ini │ │ └── cli.py │ ├── __init__.py │ ├── models.py │ ├── api.py │ └── scheduler.py ├── __init__.py ├── api │ ├── __init__.py │ └── drivers │ │ ├── __init__.py │ │ └── f5_driver │ │ ├── __init__.py │ │ └── arbiter.py ├── cmd │ ├── __init__.py │ ├── house_keeping.py │ ├── status_manager.py │ └── f5_util.py ├── common │ ├── __init__.py │ ├── data_models.py │ ├── constants.py │ └── backdoor.py ├── network │ ├── __init__.py │ ├── drivers │ │ ├── __init__.py │ │ ├── neutron │ │ │ ├── __init__.py │ │ │ └── utils.py │ │ └── noop_driver_f5 │ │ │ ├── __init__.py │ │ │ └── driver.py │ └── data_models.py ├── tests │ ├── __init__.py │ └── unit │ │ ├── __init__.py │ │ ├── api │ │ ├── __init__.py │ │ └── drivers │ │ │ ├── __init__.py │ │ │ └── f5_provider_driver │ │ │ └── __init__.py │ │ ├── db │ │ ├── __init__.py │ │ └── test_scheduler.py │ │ ├── utils │ │ ├── __init__.py │ │ └── test_decorators.py │ │ ├── controller │ │ ├── __init__.py │ │ └── worker │ │ │ ├── __init__.py │ │ │ ├── test_sync_manager.py │ │ │ ├── test_set_queue.py │ │ │ ├── test_endpoint.py │ │ │ └── test_controller_worker.py │ │ └── restclient │ │ ├── __init__.py │ │ ├── test_as3types.py │ │ ├── as3objects │ │ └── test_service.py │ │ ├── test_as3declaration.py │ │ └── test_tenant.py ├── utils │ ├── __init__.py │ ├── exceptions.py │ ├── driver_utils.py │ ├── decorators.py │ ├── cert_manager.py │ └── esd_repo.py ├── certificates │ ├── __init__.py │ └── manager │ │ ├── __init__.py │ │ └── noop.py ├── restclient │ ├── __init__.py │ ├── bigip │ │ ├── __init__.py │ │ ├── timeout_http_adapter.py │ │ └── bigip_auth.py │ ├── as3objects │ │ ├── __init__.py │ │ ├── application.py │ │ ├── as3.py │ │ ├── persist.py │ │ ├── pool_member.py │ │ ├── certificate.py │ │ ├── tenant.py │ │ ├── cipher.py │ │ ├── policy_endpoint.py │ │ ├── pool.py │ │ └── monitor.py │ ├── as3types.py │ ├── as3exceptions.py │ ├── as3logging.py │ └── as3declaration.py ├── etc │ └── f5 │ │ └── esd │ │ └── demo.json └── opts.py ├── AUTHORS ├── ci └── terraform │ ├── Chart.yaml │ ├── templates │ ├── etc │ │ ├── _vars.tf │ │ ├── _secrets.tfvars.tpl │ │ └── _main.tf │ ├── tests │ │ └── test-connection.yaml │ ├── configmap.yaml │ ├── service.yaml │ ├── bin │ │ └── _scripted.sh │ ├── _helpers.tpl │ └── deployment.yaml │ ├── .helmignore │ └── values.yaml ├── renovate.json ├── .github ├── CODEOWNERS └── workflows │ ├── lint.yml │ └── test.yml ├── test-requirements.txt ├── requirements.txt ├── setup.py ├── setup.cfg ├── .gitignore ├── tools └── coding-checks.sh └── .pylintrc /ChangeLog: -------------------------------------------------------------------------------- 1 | CHANGES 2 | ======= 3 | 4 | * Initial commit 5 | -------------------------------------------------------------------------------- /octavia_f5/controller/statusmanager/legacy_healthmanager/health_drivers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Andrew Karpow 2 | Benjamin Ludwig 3 | Michal Kratochvil 4 | -------------------------------------------------------------------------------- /ci/terraform/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "1.0" 3 | description: A Helm chart for Kubernetes 4 | name: terraform 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @notandy @velp @sapcc/network-api-contributors @sapcc/cc_github_managers_approval 2 | /.github/CODEOWNERS @sapcc/cc_github_managers_approval 3 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # The order of packages is significant, because pip processes them in the order 2 | # of appearance. Changing the order has an impact on the overall integration 3 | # process, which may cause wedges in the gate later. 4 | oslotest>=3.2.0 # Apache-2.0 5 | pylint>=3.0 # GPLv2 6 | -------------------------------------------------------------------------------- /ci/terraform/templates/etc/_vars.tf: -------------------------------------------------------------------------------- 1 | variable "os_username" {} 2 | variable "os_user_domain" { 3 | default = "" 4 | } 5 | variable "os_domain" {} 6 | variable "os_project" {} 7 | variable "os_password" {} 8 | variable "os_auth_url" {} 9 | variable "os_region" {} 10 | variable "priv_network" {} 11 | 12 | -------------------------------------------------------------------------------- /ci/terraform/templates/etc/_secrets.tfvars.tpl: -------------------------------------------------------------------------------- 1 | os_username = "{{.Values.os_username}}" 2 | os_user_domain = "{{.Values.os_user_domain}}" 3 | os_domain = "{{.Values.os_user_domain}}" 4 | os_password = "{{.Values.os_password}}" 5 | os_auth_url = "{{.Values.os_auth_url}}" 6 | os_region = "{{.Values.os_region}}" 7 | os_project = "{{.Values.os_project}}" 8 | priv_network = "{{.Values.priv_network}}" 9 | -------------------------------------------------------------------------------- /ci/terraform/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/alembic_migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | 13 | from alembic import op 14 | import sqlalchemy as sa 15 | ${imports if imports else ""} 16 | 17 | def upgrade(): 18 | ${upgrades if upgrades else "pass"} 19 | -------------------------------------------------------------------------------- /ci/terraform/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "terraform.fullname" . }}-test-connection" 5 | labels: 6 | {{ include "terraform.labels" . | indent 4 }} 7 | annotations: 8 | "helm.sh/hook": test-success 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "terraform.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tenacity>=5.0.4 # Apache-2.0 2 | stevedore>=1.20.0 # Apache-2.0 3 | futurist>=1.2.0 # Apache-2.0 4 | SQLAlchemy>=1.3.0 # MIT 5 | mock>=2.0.0 # BSD 6 | octavia>=7.0.0 # Apache-2.0 7 | openstacksdk>=0.103.0 # Apache-2.0 8 | oslo.concurrency>=3.26.0 # Apache-2.0 9 | oslo.context>=2.22.0 # Apache-2.0 10 | oslo.db>=4.27.0 # Apache-2.0 11 | oslo.log>=4.3.0 # Apache-2.0 12 | requests>=2.23.0 # Apache-2.0 13 | prometheus_client>=0.6.0 14 | taskflow>=5.9.0 # Apache-2.0 15 | manhole>=1.8.0 # BSD 16 | guppy3>=3.0.1 17 | -------------------------------------------------------------------------------- /ci/terraform/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "terraform.fullname" . }} 5 | labels: 6 | {{ include "terraform.labels" . | indent 4 }} 7 | data: 8 | scripted.sh: | 9 | {{ include (print .Template.BasePath "/bin/_scripted.sh") . | indent 4 }} 10 | main.tf: | 11 | {{ include (print .Template.BasePath "/etc/_main.tf") . | indent 4 }} 12 | vars.tf: | 13 | {{ include (print .Template.BasePath "/etc/_vars.tf") . | indent 4 }} 14 | secrets.tfvars: | 15 | {{ include (print .Template.BasePath "/etc/_secrets.tfvars.tpl") . | indent 4 }} 16 | -------------------------------------------------------------------------------- /ci/terraform/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "terraform.fullname" . }} 5 | labels: 6 | {{ include "terraform.labels" . | indent 4 }} 7 | annotations: 8 | prometheus.io/scrape: "true" 9 | prometheus.io/port: "8080" 10 | prometheus.io/targets: openstack 11 | spec: 12 | type: {{ .Values.service.type }} 13 | ports: 14 | - port: 8080 15 | targetPort: 8080 16 | protocol: TCP 17 | name: http 18 | selector: 19 | app.kubernetes.io/name: {{ include "terraform.name" . }} 20 | app.kubernetes.io/instance: {{ .Release.Name }} 21 | -------------------------------------------------------------------------------- /octavia_f5/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/db/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/cmd/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/common/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/network/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/api/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/certificates/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/controller/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/restclient/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/controller/worker/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/network/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/restclient/bigip/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/api/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/db/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/api/drivers/f5_driver/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/certificates/manager/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/api/drivers/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/restclient/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/controller/statusmanager/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/network/drivers/neutron/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/network/drivers/noop_driver_f5/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/worker/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/api/drivers/f5_provider_driver/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/controller/statusmanager/legacy_healthmanager/__init__.py: -------------------------------------------------------------------------------- 1 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 2 | # not use this file except in compliance with the License. You may obtain 3 | # a copy of the License at 4 | # 5 | # http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software 8 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 | # License for the specific language governing permissions and limitations 11 | # under the License. 12 | -------------------------------------------------------------------------------- /octavia_f5/etc/f5/esd/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "esd_demo_1": { 3 | "lbaas_ctcp": "tcp-mobile-optimized", 4 | "lbaas_stcp": "tcp-lan-optimized", 5 | "lbaas_cssl_profile": "clientssl", 6 | "lbaas_sssl_profile": "serverssl", 7 | "lbaas_irule": ["_sys_https_redirect"], 8 | "lbaas_policy": [], 9 | "lbaas_persist": "hash", 10 | "lbaas_fallback_persist": "source_addr" 11 | }, 12 | 13 | "esd_demo_2": { 14 | "lbaas_irule": [ 15 | "_sys_https_redirect", 16 | "_sys_APM_ExchangeSupport_helper" 17 | ] 18 | }, 19 | 20 | "esd_demo_3": { 21 | "lbaas_ctcp": "tcp-wan-optimized", 22 | "lbaas_stcp": "tcp-lan-optimized" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linting the repository 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: [3.12] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v5 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | set -xe 20 | python -m pip install --upgrade pip 21 | pip install -r test-requirements.txt -c https://raw.githubusercontent.com/sapcc/requirements/stable/2025.1-m3/upper-constraints.txt 22 | - name: Analysing the code with pylint 23 | run: | 24 | tools/coding-checks.sh --pylint 25 | 26 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/application.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_log import log as logging 16 | from octavia_f5.common import constants 17 | 18 | LOG = logging.getLogger(__name__) 19 | 20 | 21 | def get_name(loadbalancer_id): 22 | return f"{constants.PREFIX_LOADBALANCER}{loadbalancer_id}" 23 | -------------------------------------------------------------------------------- /octavia_f5/opts.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018, 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import octavia_f5.common.config 16 | 17 | 18 | def list_opts(): 19 | return [ 20 | ('f5_agent', octavia_f5.common.config.f5_agent_opts), 21 | ('f5_tls_server', octavia_f5.common.config.f5_tls_server_opts), 22 | ('f5_tls_client', octavia_f5.common.config.f5_tls_client_opts), 23 | ] 24 | -------------------------------------------------------------------------------- /octavia_f5/controller/statusmanager/legacy_healthmanager/health_drivers/update_base.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 GoDaddy 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import abc 16 | 17 | 18 | class HealthUpdateBase(object): 19 | @abc.abstractmethod 20 | def update_health(self, health, srcaddr): 21 | raise NotImplementedError() 22 | 23 | 24 | class StatsUpdateBase(object): 25 | @abc.abstractmethod 26 | def update_stats(self, health_message, srcaddr): 27 | raise NotImplementedError() 28 | -------------------------------------------------------------------------------- /octavia_f5/common/data_models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 SAP SE 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 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | 17 | from octavia.common.data_models import BaseDataModel 18 | 19 | 20 | class ESD(BaseDataModel): 21 | def __init__(self, id=None, name=None, attributes=None): 22 | self.id = id 23 | self.name = name 24 | self.attributes = attributes or [] 25 | 26 | 27 | class ESDAttributes(object): 28 | def __init__(self, esd_id=None, name=None, type=None): 29 | self.esd_id = esd_id 30 | self.type = type 31 | self.name = name 32 | -------------------------------------------------------------------------------- /octavia_f5/controller/worker/flows/f5_flows_rseries.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_log import log as logging 16 | from octavia_f5.controller.worker.flows import f5_flows_iseries 17 | from octavia_f5.controller.worker.tasks import f5_tasks_rseries 18 | 19 | LOG = logging.getLogger(__name__) 20 | 21 | 22 | class F5Flows(f5_flows_iseries.F5Flows): 23 | """Class that configures flows to use tasks specific to rSeries devices.""" 24 | 25 | def __init__(self): 26 | super(F5Flows, self).__init__(tasks=f5_tasks_rseries) 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 Hewlett-Packard Development Company, L.P. 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 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT 17 | import setuptools 18 | 19 | # In python < 2.7.4, a lazy loading of package `pbr` will break 20 | # setuptools if some other modules registered functions in `atexit`. 21 | # solution from: http://bugs.python.org/issue15881#msg170215 22 | try: 23 | import multiprocessing # noqa 24 | except ImportError: 25 | pass 26 | 27 | setuptools.setup( 28 | setup_requires=['pbr>=2.0.0'], 29 | pbr=True) 30 | -------------------------------------------------------------------------------- /ci/terraform/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for terraform. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: hashicorp/terraform 9 | tag: light 10 | pullPolicy: IfNotPresent 11 | 12 | imagePullSecrets: [] 13 | nameOverride: "" 14 | fullnameOverride: "" 15 | 16 | podSecurityContext: {} 17 | # fsGroup: 2000 18 | 19 | securityContext: {} 20 | # capabilities: 21 | # drop: 22 | # - ALL 23 | # readOnlyRootFilesystem: true 24 | # runAsNonRoot: true 25 | # runAsUser: 1000 26 | 27 | service: 28 | type: ClusterIP 29 | port: 80 30 | 31 | resources: {} 32 | # We usually recommend not to specify default resources and to leave this as a conscious 33 | # choice for the user. This also increases chances charts run on environments with little 34 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 35 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 36 | # limits: 37 | # cpu: 100m 38 | # memory: 128Mi 39 | # requests: 40 | # cpu: 100m 41 | # memory: 128Mi 42 | 43 | nodeSelector: {} 44 | 45 | tolerations: [] 46 | 47 | affinity: {} 48 | -------------------------------------------------------------------------------- /octavia_f5/network/drivers/neutron/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from octavia_f5.network import data_models as network_models 16 | 17 | 18 | def convert_network_to_model(nw): 19 | return network_models.Network( 20 | id=nw.id, 21 | name=nw.name, 22 | subnets=nw.subnet_ids, 23 | project_id=nw.project_id, 24 | admin_state_up=nw.is_admin_state_up, 25 | mtu=nw.mtu, 26 | provider_network_type=nw.provider_network_type, 27 | provider_physical_network=nw.provider_physical_network, 28 | provider_segmentation_id=nw.provider_segmentation_id, 29 | router_external=nw.is_router_external, 30 | port_security_enabled=nw.is_port_security_enabled, 31 | segments=nw.segments 32 | ) 33 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = %(here)s/alembic_migrations 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | sqlalchemy.url = 24 | 25 | 26 | # Logging configuration 27 | [loggers] 28 | keys = root,sqlalchemy,alembic 29 | 30 | [handlers] 31 | keys = console 32 | 33 | [formatters] 34 | keys = generic 35 | 36 | [logger_root] 37 | level = WARN 38 | handlers = console 39 | qualname = 40 | 41 | [logger_sqlalchemy] 42 | level = WARN 43 | handlers = 44 | qualname = sqlalchemy.engine 45 | 46 | [logger_alembic] 47 | level = INFO 48 | handlers = 49 | qualname = alembic 50 | 51 | [handler_console] 52 | class = StreamHandler 53 | args = (sys.stderr,) 54 | level = NOTSET 55 | formatter = generic 56 | 57 | [formatter_generic] 58 | format = %(levelname)-5.5s [%(name)s] %(message)s 59 | datefmt = %H:%M:%S 60 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/as3.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import time 16 | 17 | from oslo_config import cfg 18 | from oslo_log import log as logging 19 | 20 | from octavia_f5.restclient.as3classes import AS3 21 | 22 | CONF = cfg.CONF 23 | LAST_PERSIST = 0 24 | LOG = logging.getLogger(__name__) 25 | 26 | 27 | def get_as3(): 28 | action = 'deploy' 29 | persist = False 30 | global LAST_PERSIST 31 | 32 | if CONF.f5_agent.persist_every == 0: 33 | persist = True 34 | elif CONF.f5_agent.persist_every > 0: 35 | persist = time.time() - CONF.f5_agent.persist_every > LAST_PERSIST 36 | if persist: 37 | LAST_PERSIST = time.time() 38 | 39 | return AS3( 40 | persist=persist, 41 | action=action, 42 | historyLimit=2, 43 | _log_level=LOG.logger.level 44 | ) 45 | -------------------------------------------------------------------------------- /ci/terraform/templates/bin/_scripted.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | TERMINATE=0 4 | 5 | _term() { 6 | echo "Caught SIGTERM signal!" 7 | TERMINATE=1 8 | killall terraform 9 | } 10 | 11 | trap _term SIGTERM 12 | 13 | terraform init 14 | 15 | 16 | # Minimal prometheus exporter 17 | echo "# terraform octavia performance metrics" > prom.html 18 | sh -c 'while true; do { echo -ne "HTTP/1.0 200 OK\r\n\r\n"; cat prom.html; } | nc -l -p 8080; done' & 19 | 20 | 21 | while true 22 | do 23 | START_TIME=$(date +%s) 24 | echo START: $START_TIME 25 | time terraform apply -auto-approve -var-file="secrets.tfvars" -parallelism=15 26 | apply_duration=$(($(date +%s) - $START_TIME)) 27 | echo DURATION: $apply_duration 28 | echo "$(($apply_duration / 60)) minutes and $(($apply_duration % 60)) seconds elapsed." 29 | if [ $TERMINATE = 1 ]; then exit; fi 30 | sleep 10 31 | 32 | START_TIME=$(date +%s) 33 | echo START: $START_TIME 34 | time terraform destroy -auto-approve -var-file="secrets.tfvars" -parallelism=15 35 | destroy_duration=$(($(date +%s) - $START_TIME)) 36 | echo DURATION: $destroy_duration 37 | echo "$(($destroy_duration / 60)) minutes and $(($destroy_duration % 60)) seconds elapsed." 38 | if [ $TERMINATE = 1 ]; then exit; fi 39 | 40 | echo -e "# terraform octavia performance metrics\nterraform_octavia_apply_duration $apply_duration\nterraform_octavia_destroy_duration $destroy_duration\n" > prom.html 41 | sleep 60 42 | done 43 | -------------------------------------------------------------------------------- /octavia_f5/restclient/bigip/timeout_http_adapter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from requests.adapters import HTTPAdapter 16 | 17 | DEFAULT_TIMEOUT = 120 # 120 seconds 18 | 19 | 20 | class TimeoutHTTPAdapter(HTTPAdapter): 21 | """ An requests HTTPAdapter that implements a default 22 | timeout for a requests session. 23 | Based on https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/ 24 | """ 25 | def __init__(self, *args, **kwargs): 26 | self.timeout = DEFAULT_TIMEOUT 27 | if "timeout" in kwargs: 28 | self.timeout = kwargs["timeout"] 29 | del kwargs["timeout"] 30 | super().__init__(*args, **kwargs) 31 | 32 | def send(self, request, **kwargs): 33 | timeout = kwargs.get("timeout") 34 | if timeout is None: 35 | kwargs["timeout"] = self.timeout 36 | return super().send(request, **kwargs) 37 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3types.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | # Copyright 2019, 2020 SAP SE 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import string 17 | 18 | REMARK_FORBIDDEN = ["\"", "\\"] 19 | LABEL_FORBIDDEN = ["#", "&", "*", "<", ">", "?", "[", "\\", "]", "`", "\""] 20 | 21 | 22 | def f5remark(remark): 23 | if not remark: 24 | return "" 25 | 26 | # Remove control characters. 27 | nstr = "".join(ch for ch in remark if 28 | ch in string.printable and 29 | ch not in REMARK_FORBIDDEN) 30 | 31 | # Remove double-quote ("), and backslash (\), limit to 64 characters. 32 | return nstr[:64] 33 | 34 | 35 | def f5label(label): 36 | if not label: 37 | return "" 38 | 39 | # Remove control characters and limit to 64 characters. 40 | nstr = "".join(ch for ch in label if 41 | ch in string.printable and 42 | ch not in LABEL_FORBIDDEN) 43 | return nstr[:64] 44 | -------------------------------------------------------------------------------- /octavia_f5/db/models.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 SAP SE 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 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sqlalchemy as sa 17 | 18 | from sqlalchemy import orm 19 | from octavia.db import base_models 20 | from octavia_f5.common import data_models 21 | 22 | 23 | class ESDAttributes(base_models.BASE, base_models.NameMixin): 24 | 25 | __data_model__ = data_models.ESDAttributes 26 | 27 | __tablename__ = "f5_esd_attributes" 28 | esd_id = sa.Column( 29 | sa.String(36), 30 | sa.ForeignKey("f5_esd.id", 31 | name="fk_f5_esd_attributes_f5_esd_id"), 32 | nullable=False) 33 | type = sa.Column(sa.String(255), nullable=False) 34 | 35 | 36 | class ESD(base_models.BASE, base_models.IdMixin, 37 | base_models.NameMixin): 38 | 39 | __data_model__ = data_models.ESD 40 | 41 | __tablename__ = "f5_esd" 42 | 43 | attributes = orm.relationship( 44 | 'ESDAttributes', cascade='delete', uselist=True) 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Testing the repository 5 | 6 | on: 7 | push: 8 | branches: [ stable/2025.1-m3 ] 9 | pull_request: 10 | 11 | env: 12 | UPPER_CONSTRAINTS: "https://raw.githubusercontent.com/sapcc/requirements/stable/2025.1-m3/upper-constraints.txt" 13 | 14 | jobs: 15 | test: 16 | 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | python: [3.12] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python }} 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip 31 | pip install pbr oslotest -c $UPPER_CONSTRAINTS 32 | pip install --exists-action w -e git+https://github.com/sapcc/octavia.git@stable/2025.1-m3#egg=octavia -c $UPPER_CONSTRAINTS 33 | pip install -e . -c $UPPER_CONSTRAINTS 34 | - name: Lint with flake8 35 | run: | 36 | pip install flake8 37 | flake8 ./octavia_f5 --count --ignore H104,W504 --enable-extensions H106,H203,H204,H205,H904 --max-line-length=127 --statistics 38 | - name: Test with pytest 39 | run: | 40 | pip install pytest -c $UPPER_CONSTRAINTS 41 | pytest octavia_f5/tests/unit/ 42 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/persist.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import hashlib 16 | 17 | from octavia_f5.common import constants 18 | from octavia_f5.restclient.as3classes import Persist 19 | 20 | 21 | def get_source_ip(timeout, granularity): 22 | m = hashlib.md5() 23 | persist = {'persistenceMethod': 'source-address'} 24 | if timeout: 25 | persist['duration'] = timeout 26 | m.update(str(timeout).encode('utf-8')) 27 | if granularity: 28 | persist['addressMask'] = granularity 29 | m.update(granularity.encode('utf-8')) 30 | name = f'persist_{m.hexdigest()}' 31 | persist = Persist(**persist) 32 | return name, persist 33 | 34 | 35 | def get_app_cookie(cookie_name): 36 | persist = Persist( 37 | persistenceMethod='universal', 38 | iRule=f'{constants.PREFIX_IRULE}app_cookie_{cookie_name}', 39 | duration=3600 40 | ) 41 | name = f'persist_app_cookie_{cookie_name}' 42 | return name, persist 43 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/restclient/test_as3types.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from octavia.tests.unit import base 16 | from octavia_f5.restclient.as3types import f5label, f5remark 17 | 18 | 19 | class TestAS3Types(base.TestCase): 20 | def test_f5remark(self): 21 | self.assertEqual(f5remark("test test test"), "test test test") 22 | self.assertEqual(f5remark("\\Programm Files"), "Programm Files") 23 | self.assertEqual(len(f5remark(65 * "A")), 64) 24 | 25 | FORBIDDEN_CHARACTERS = "\\\"" 26 | for forbidden_char in FORBIDDEN_CHARACTERS: 27 | self.assertEqual(f5label(forbidden_char), "") 28 | 29 | def test_f5label(self): 30 | self.assertEqual(f5label("test test test"), "test test test") 31 | self.assertEqual(f5label("[dev]"), "dev") 32 | self.assertEqual(len(f5label(65 * "A")), 64) 33 | 34 | FORBIDDEN_CHARACTERS = "#&*<>?[\\]`\"" 35 | for forbidden_char in FORBIDDEN_CHARACTERS: 36 | self.assertEqual(f5label(forbidden_char), "") 37 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/utils/test_decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from octavia.tests.unit import base 16 | from octavia_f5.utils.decorators import RunHookOnException 17 | 18 | 19 | class ToMockedClass(object): 20 | def __init__(self): 21 | self.hooked_func_calls = 0 22 | self.hook_called = False 23 | 24 | def _hook_func(self): 25 | self.hook_called = True 26 | 27 | @RunHookOnException(hook=_hook_func) 28 | def hooked_func_raising_exception(self): 29 | self.hooked_func_calls += 1 30 | 31 | if self.hooked_func_calls <= 1: 32 | # Raise exception on first call 33 | raise Exception() 34 | 35 | 36 | class TestRunHookOnException(base.TestCase): 37 | def test_run_hook(self): 38 | hooked_class = ToMockedClass() 39 | self.assertEqual(hooked_class.hooked_func_calls, 0) 40 | self.assertFalse(hooked_class.hook_called) 41 | 42 | hooked_class.hooked_func_raising_exception() 43 | self.assertEqual(hooked_class.hooked_func_calls, 2) 44 | self.assertTrue(hooked_class.hook_called) 45 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | class TypeNotSupportedException(Exception): 16 | """Exception raised for not supported types. 17 | 18 | """ 19 | 20 | 21 | class RequiredKeyMissingException(Exception): 22 | """Exception raised for missing keys. 23 | 24 | Attributes: 25 | message -- explanation of the error 26 | """ 27 | 28 | def __init__(self, key): 29 | super().__init__() 30 | self.message = f"Missing required key '{key}'." 31 | 32 | 33 | class IncompatibleSubTypeException(Exception): 34 | """Exception raised for wrong subtype. 35 | 36 | Attributes: 37 | message -- explanation of the error 38 | """ 39 | 40 | def __init__(self, got, expected): 41 | super().__init__() 42 | self.message = f"Incompatible subtype '{got}', expected '{expected}'." 43 | 44 | 45 | class DuplicatedKeyException(Exception): 46 | """Exception raised for duplicated keys. 47 | 48 | """ 49 | 50 | 51 | class UnprocessableEntityException(Exception): 52 | """Exception raised for generic F5 AS3 failure. 53 | 54 | """ 55 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = octavia-f5-provider-driver 3 | summary = F5 Networks Provider Driver for OpenStack Octavia 4 | description-file = 5 | README.md 6 | author = Andrew Karpow (SAP SE) 7 | author-email = andrew.karpow@sap.com 8 | classifier = 9 | Development Status :: 5 - Production/Stable 10 | Environment :: OpenStack 11 | Intended Audience :: Developers 12 | Intended Audience :: Information Technology 13 | Intended Audience :: System Administrators 14 | License :: OSI Approved :: Apache Software License 15 | Operating System :: POSIX :: Linux 16 | Programming Language :: Python 17 | Programming Language :: Python :: 3 18 | Programming Language :: Python :: 3.9 19 | Programming Language :: Python :: 3.10 20 | Programming Language :: Python :: 3.11 21 | 22 | [files] 23 | packages = 24 | octavia_f5 25 | 26 | [wheel] 27 | universal = 1 28 | 29 | [entry_points] 30 | console_scripts = 31 | octavia-f5-status-manager = octavia_f5.cmd.status_manager:main 32 | octavia-f5-housekeeping = octavia_f5.cmd.house_keeping:main 33 | octavia-f5-util = octavia_f5.cmd.f5_util:main 34 | octavia.api.drivers = 35 | f5 = octavia_f5.api.drivers.f5_driver.driver:F5ProviderDriver 36 | F5Networks = octavia_f5.api.drivers.f5_driver.driver:F5ProviderDriver 37 | octavia.plugins = 38 | f5_plugin = octavia_f5.controller.worker.controller_worker:ControllerWorker 39 | octavia.network.drivers = 40 | neutron_client = octavia_f5.network.drivers.neutron.neutron_client:NeutronClient 41 | network_noop_driver_f5 = octavia_f5.network.drivers.noop_driver_f5.driver:NoopNetworkDriverF5 42 | octavia.cert_manager = 43 | noop_cert_manager = octavia_f5.certificates.manager.noop:NoopCertManager 44 | -------------------------------------------------------------------------------- /octavia_f5/db/api.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Rackspace, 2019 SAP SE, 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_db.sqlalchemy import enginefacade 16 | from oslo_log import log as logging 17 | 18 | LOG = logging.getLogger(__name__) 19 | _FACADE = None 20 | 21 | 22 | def _create_facade_lazily(): 23 | global _FACADE 24 | if _FACADE is None: 25 | _FACADE = True 26 | enginefacade.configure(sqlite_fk=True, expire_on_commit=True) 27 | 28 | 29 | def _get_transaction_context(reader=False): 30 | _create_facade_lazily() 31 | # TODO(gthiemonge) Create and use new functions to get read-only sessions 32 | if reader: 33 | context = enginefacade.reader 34 | else: 35 | context = enginefacade.writer 36 | return context 37 | 38 | 39 | def _get_sessionmaker(reader=False): 40 | context = _get_transaction_context(reader) 41 | return context.get_sessionmaker() 42 | 43 | 44 | def get_engine(): 45 | context = _get_transaction_context() 46 | return context.get_engine() 47 | 48 | 49 | def get_session(): 50 | """Helper method to grab session.""" 51 | return _get_sessionmaker()() 52 | 53 | 54 | def session(): 55 | return _get_sessionmaker() 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea/* 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | MANIFEST 30 | src/ 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *.cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # mkdocs documentation 105 | /site 106 | 107 | # mypy 108 | .mypy_cache/ 109 | -------------------------------------------------------------------------------- /tools/coding-checks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # This script is copied from octavia and adapted for octavia-f5-provider. 3 | set -eu 4 | 5 | usage () { 6 | echo "Usage: $0 [OPTION]..." 7 | echo "Run octavia's coding check(s)" 8 | echo "" 9 | echo " -Y, --pylint [] Run pylint check on the entire octavia module or just files changed in basecommit (e.g. HEAD~1)" 10 | echo " -h, --help Print this usage message" 11 | echo 12 | exit 0 13 | } 14 | 15 | join_args() { 16 | if [ -z "$scriptargs" ]; then 17 | scriptargs="$opt" 18 | else 19 | scriptargs="$scriptargs $opt" 20 | fi 21 | } 22 | 23 | process_options () { 24 | i=1 25 | while [ $i -le $# ]; do 26 | eval opt=\$$i 27 | case $opt in 28 | -h|--help) usage;; 29 | -Y|--pylint) pylint=1;; 30 | *) join_args;; 31 | esac 32 | i=$((i+1)) 33 | done 34 | } 35 | 36 | run_pylint () { 37 | local target="${scriptargs:-all}" 38 | 39 | if [ "$target" = "all" ]; then 40 | files="octavia_f5" 41 | else 42 | case "$target" in 43 | *HEAD~[0-9]*) files=$(git diff --diff-filter=AM --name-only $target -- "*.py");; 44 | *) echo "$target is an unrecognized basecommit"; exit 1;; 45 | esac 46 | fi 47 | 48 | echo "Running pylint..." 49 | echo "You can speed this up by running it on 'HEAD~[0-9]' (e.g. HEAD~1, this change only)..." 50 | if [ -n "${files}" ]; then 51 | pylint -j 0 --max-nested-blocks 7 --extension-pkg-whitelist netifaces --rcfile=.pylintrc --output-format=colorized ${files} 52 | else 53 | echo "No python changes in this commit, pylint check not required." 54 | exit 0 55 | fi 56 | } 57 | 58 | scriptargs= 59 | pylint=1 60 | 61 | process_options $@ 62 | 63 | if [ $pylint -eq 1 ]; then 64 | run_pylint 65 | exit 0 66 | fi 67 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3logging.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from urllib import parse 16 | 17 | import json 18 | 19 | 20 | def get_response_log(response): 21 | """ Formats AS3 requests response and prints them pretty 22 | 23 | :param response: requests response 24 | :param error: boolean, true if log to error, else debug 25 | """ 26 | 27 | request = response.request 28 | url = parse.urlparse(response.url) 29 | redacted_url = url._replace(netloc=url.hostname).geturl() 30 | msg = f"{request.method} {redacted_url} finished with code {response.status_code}:\n" 31 | 32 | # Format Request 33 | if request.body: 34 | try: 35 | parsed = json.loads(request.body) 36 | msg += json.dumps(parsed, sort_keys=True, indent=4) 37 | except ValueError: 38 | # No json, just dump 39 | msg += request.body 40 | 41 | # Format Response 42 | if 'application/json' in response.headers.get('Content-Type'): 43 | try: 44 | parsed = response.json() 45 | if 'results' in parsed: 46 | parsed = parsed['results'] 47 | msg += json.dumps(parsed, sort_keys=True, indent=4) 48 | except ValueError: 49 | # No valid json 50 | msg += response.text 51 | else: 52 | msg += response.text 53 | 54 | return msg.strip() 55 | -------------------------------------------------------------------------------- /octavia_f5/network/drivers/noop_driver_f5/driver.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Hewlett-Packard Development Company, L.P. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_config import cfg 16 | from oslo_log import log as logging 17 | 18 | from octavia.network.drivers.noop_driver import driver 19 | from octavia_f5.network import data_models 20 | 21 | LOG = logging.getLogger(__name__) 22 | CONF = cfg.CONF 23 | 24 | 25 | class NoopNetworkDriverF5(driver.NoopNetworkDriver): 26 | def __init__(self): 27 | self.physical_network = 'physnet' 28 | self.physical_interface = 'portchannel1' 29 | super().__init__() 30 | 31 | def allocate_vip(self, load_balancer): # pylint: disable=arguments-renamed 32 | return super().allocate_vip(load_balancer) 33 | 34 | def get_scheduled_host(self, port_id): 35 | return CONF.host 36 | 37 | def get_segmentation_id(self, network_id, host=None): 38 | return 1234 39 | 40 | def get_network(self, network_id, context=None): 41 | return data_models.Network() 42 | 43 | def ensure_selfips(self, load_balancers, agent=None, cleanup_orphans=False): 44 | return ([], []) 45 | 46 | def cleanup_selfips(self, selfips): 47 | return 48 | 49 | def create_vip(self, load_balancer, candidate): 50 | return self.driver.create_port(load_balancer.vip.network_id) 51 | 52 | def invalidate_cache(self, hard=True): 53 | pass 54 | -------------------------------------------------------------------------------- /octavia_f5/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | 16 | class ProviderDriverException(Exception): 17 | pass 18 | 19 | 20 | class AS3Exception(ProviderDriverException): 21 | pass 22 | 23 | 24 | class RetryException(ProviderDriverException): 25 | pass 26 | 27 | 28 | class FailoverException(ProviderDriverException): 29 | pass 30 | 31 | 32 | class IControlRestException(ProviderDriverException): 33 | pass 34 | 35 | 36 | class F5osaException(ProviderDriverException): 37 | pass 38 | 39 | 40 | class PolicyHasNoRules(AS3Exception): 41 | pass 42 | 43 | 44 | class NoActionFoundForPolicy(AS3Exception): 45 | pass 46 | 47 | 48 | class CompareTypeNotSupported(AS3Exception): 49 | pass 50 | 51 | 52 | class PolicyTypeNotSupported(AS3Exception): 53 | pass 54 | 55 | 56 | class PolicyActionNotSupported(AS3Exception): 57 | pass 58 | 59 | 60 | class MonitorDeletionException(AS3Exception): 61 | def __init__(self, tenant, application, monitor): 62 | super(MonitorDeletionException).__init__() 63 | self.tenant = tenant 64 | self.application = application 65 | self.monitor = monitor 66 | 67 | 68 | class DeleteAllTenantsException(AS3Exception): 69 | def __init__(self): 70 | super(DeleteAllTenantsException).__init__() 71 | self.message = 'Delete called without tenant, would wipe all AS3 Declaration, ignoring.' 72 | -------------------------------------------------------------------------------- /ci/terraform/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "terraform.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "terraform.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "terraform.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "terraform.labels" -}} 38 | app.kubernetes.io/name: {{ include "terraform.name" . }} 39 | helm.sh/chart: {{ include "terraform.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Create the name of the service account to use 49 | */}} 50 | {{- define "terraform.serviceAccountName" -}} 51 | {{- if .Values.serviceAccount.create -}} 52 | {{ default (include "terraform.fullname" .) .Values.serviceAccount.name }} 53 | {{- else -}} 54 | {{ default "default" .Values.serviceAccount.name }} 55 | {{- end -}} 56 | {{- end -}} 57 | -------------------------------------------------------------------------------- /octavia_f5/controller/worker/set_queue.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from queue import Queue 16 | 17 | 18 | class SetQueue(Queue): 19 | """ 20 | A thread-safe queue that stores unique items using sets and supports priority items. 21 | 22 | Inherits from `Queue` but overrides the internal storage to use sets, ensuring all items are unique. 23 | Items added via `put_priority` are returned first when retrieving from the queue. 24 | """ 25 | def _init(self, maxsize): 26 | self.maxsize = maxsize 27 | self.queue = set() 28 | self.priority_queue = set() 29 | 30 | def put_priority(self, item): 31 | """Add an item to the priority queue.""" 32 | self.priority_queue.add(item) 33 | self.queue.discard(item) 34 | # notify polling threads 35 | with self.not_empty: # acquire self.mutex for self.not_empty 36 | self.not_empty.notify() 37 | 38 | def _put(self, item): 39 | self.queue.add(item) 40 | 41 | def _qsize(self): 42 | """Return the approximate size of the queue.""" 43 | return len(self.queue) + len(self.priority_queue) 44 | 45 | def _get(self): 46 | if len(self.priority_queue) > 0: 47 | # If there are priority items, return one of them 48 | item = self.priority_queue.pop() 49 | self.queue.discard(item) 50 | return item 51 | return self.queue.pop() 52 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/pool_member.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | import math 15 | 16 | from octavia_f5.common import constants 17 | from octavia_f5.restclient import as3types 18 | from octavia_f5.restclient.as3classes import Member, Pointer 19 | from octavia_f5.restclient.as3objects import monitor as m_monitor 20 | 21 | 22 | def get_name(member_id): 23 | return f"{constants.PREFIX_MEMBER}{member_id}" 24 | 25 | 26 | def normalize_weight(weight): 27 | """AS3 accepts ratios between 0 and 100 whereas Octavia 28 | allows ratios in a range of 0 and 256, so we normalize 29 | the Octavia value for AS3 consumption. 30 | 31 | We also round up the result since we want avoid having a 0 32 | (no traffic received at all) when setting a weight of 1 33 | """ 34 | ratio = int(math.ceil((weight / 256.) * 100.)) 35 | return ratio 36 | 37 | 38 | def get_member(member, enable_priority_group, with_monitors): 39 | args = {} 40 | args['servicePort'] = member.protocol_port 41 | args['serverAddresses'] = [member.ip_address] 42 | 43 | if member.enabled: 44 | args['adminState'] = 'enable' 45 | else: 46 | args['adminState'] = 'disable' 47 | 48 | if member.weight == 0: 49 | args['ratio'] = 1 50 | args['adminState'] = 'disable' 51 | else: 52 | args['ratio'] = normalize_weight(member.weight) 53 | 54 | if enable_priority_group: 55 | # set Priority group for normal pool to 2, backup to 1 56 | args['priorityGroup'] = 1 if member.backup else 2 57 | 58 | if with_monitors and (member.monitor_address or member.monitor_port): 59 | # Add custom monitors 60 | args['monitors'] = [Pointer(use=m_monitor.get_name(member.id))] 61 | 62 | args['remark'] = as3types.f5remark(member.id) 63 | return Member(**args) 64 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/restclient/as3objects/test_service.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from unittest import mock 16 | 17 | from oslo_config import cfg 18 | from oslo_config import fixture as oslo_fixture 19 | 20 | from octavia.db import models 21 | from octavia.tests.unit import base 22 | from octavia_f5.common import config # noqa 23 | from octavia_f5.restclient.as3objects import service 24 | 25 | CONF = None 26 | 27 | 28 | class TestService(base.TestCase): 29 | def setUp(self): 30 | self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 31 | self.conf.config(group='f5_agent', 32 | tcp_service_type='Service_L4') 33 | super().setUp() 34 | 35 | @mock.patch("octavia_f5.utils.esd_repo.EsdRepository") 36 | @mock.patch("octavia_f5.utils.cert_manager.CertManagerWrapper") 37 | def test_get_service_l4(self, cert_manager, esd_repo): 38 | mock_listener = mock.Mock(spec=models.Listener) 39 | mock_listener.id = "test_listener_id" 40 | mock_listener.name = "test_listener" 41 | mock_listener.allowed_cidrs = [] 42 | mock_listener.connection_limit = 0 43 | mock_listener.protocol = "TCP" 44 | mock_listener.l7policies = [] 45 | mock_listener.tags = ["test_l4_tag"] 46 | 47 | test_profile_name = "test_f5_fastl4_profile" 48 | esd_repo.get_esd.return_value = { 49 | "lbaas_fastl4": test_profile_name, 50 | } 51 | 52 | svc = service.get_service(mock_listener, cert_manager, esd_repo) 53 | self.assertEqual(1, len(svc)) 54 | svc_name, svc_as3 = svc[0] 55 | self.assertEqual(f"listener_{mock_listener.id}", svc_name) 56 | self.assertEqual("Service_L4", getattr(svc_as3, "class")) 57 | self.assertEqual(f"/Common/{test_profile_name}", svc_as3.profileL4.bigip) 58 | -------------------------------------------------------------------------------- /octavia_f5/utils/driver_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_config import cfg 16 | from oslo_log import log as logging 17 | from stevedore import driver 18 | 19 | from octavia_lib.api.drivers import data_models 20 | from octavia_lib.common import constants 21 | 22 | from octavia_f5.common import constants as f5_constants 23 | 24 | 25 | CONF = cfg.CONF 26 | LOG = logging.getLogger(__name__) 27 | _NETWORK_DRIVER = None 28 | 29 | 30 | def pending_delete(obj): 31 | return obj.provisioning_status == constants.PENDING_DELETE 32 | 33 | 34 | def get_network_driver(): 35 | global _NETWORK_DRIVER 36 | if _NETWORK_DRIVER is None: 37 | CONF.import_group('controller_worker', 'octavia.common.config') 38 | _NETWORK_DRIVER = driver.DriverManager( 39 | namespace='octavia.network.drivers', 40 | name=CONF.controller_worker.network_driver, 41 | invoke_on_load=True 42 | ).driver 43 | return _NETWORK_DRIVER 44 | 45 | 46 | def lb_to_vip_obj(lb): 47 | vip_obj = data_models.VIP() 48 | if lb.vip_address: 49 | vip_obj.ip_address = lb.vip_address 50 | if lb.vip_network_id: 51 | vip_obj.network_id = lb.vip_network_id 52 | if lb.vip_port_id: 53 | vip_obj.port_id = lb.vip_port_id 54 | if lb.vip_subnet_id: 55 | vip_obj.subnet_id = lb.vip_subnet_id 56 | if lb.vip_qos_policy_id: 57 | vip_obj.qos_policy_id = lb.vip_qos_policy_id 58 | vip_obj.load_balancer = lb 59 | return vip_obj 60 | 61 | 62 | def selfip_for_subnet_exists(subnet_id, selfips): 63 | for selfip in selfips: 64 | for fixed_ip in selfip.fixed_ips: 65 | if fixed_ip.subnet_id == subnet_id: 66 | return True 67 | return False 68 | 69 | 70 | def get_subnet_route_name(network_id, subnet_id): 71 | return f"{f5_constants.PREFIX_NETWORK}{network_id}_{f5_constants.PREFIX_SUBNET}{subnet_id}" 72 | -------------------------------------------------------------------------------- /ci/terraform/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "terraform.fullname" . }} 5 | labels: 6 | {{ include "terraform.labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "terraform.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: {{ include "terraform.name" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | annotations: 19 | configmap-hash: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} 20 | spec: 21 | {{- with .Values.imagePullSecrets }} 22 | imagePullSecrets: 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | securityContext: 26 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 27 | containers: 28 | - name: {{ .Chart.Name }} 29 | command: ['sh', '-c', 'cp -R /var/terraform /tmp/terraform && cd /tmp/terraform && ./scripted.sh'] 30 | securityContext: 31 | {{- toYaml .Values.securityContext | nindent 12 }} 32 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 33 | imagePullPolicy: {{ .Values.image.pullPolicy }} 34 | resources: 35 | {{- toYaml .Values.resources | nindent 12 }} 36 | volumeMounts: 37 | - mountPath: /var/terraform 38 | name: var-terraform 39 | readOnly: true 40 | {{- with .Values.nodeSelector }} 41 | nodeSelector: 42 | {{- toYaml . | nindent 8 }} 43 | {{- end }} 44 | {{- with .Values.affinity }} 45 | affinity: 46 | {{- toYaml . | nindent 8 }} 47 | {{- end }} 48 | {{- with .Values.tolerations }} 49 | tolerations: 50 | {{- toYaml . | nindent 8 }} 51 | {{- end }} 52 | volumes: 53 | - name: var-terraform 54 | projected: 55 | defaultMode: 440 56 | sources: 57 | - configMap: 58 | name: {{ include "terraform.fullname" . }} 59 | items: 60 | - key: scripted.sh 61 | path: scripted.sh 62 | mode: 0550 63 | - key: main.tf 64 | path: main.tf 65 | - key: vars.tf 66 | path: vars.tf 67 | - key: secrets.tfvars 68 | path: secrets.tfvars -------------------------------------------------------------------------------- /octavia_f5/controller/worker/quirks.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_log import log as logging 16 | 17 | from octavia_f5.restclient.as3objects import application as m_app 18 | from octavia_f5.restclient.as3objects import pool as m_pool 19 | from octavia_f5.restclient.as3objects import tenant as m_tenant 20 | from octavia_f5.utils import driver_utils, exceptions 21 | 22 | F5_POOL_PATH = '/mgmt/tm/ltm/pool' 23 | LOG = logging.getLogger(__name__) 24 | 25 | 26 | def workaround_autotool_1469(network_id, loadbalancer_id, pool, bigips): 27 | """ This is a workaround for F5 TMSH / AS3 Bug tracked as 527004 / AUTOTOOL-1469. 28 | -> Custom Monitor noted as in-use and cannot be removed 29 | 30 | Workaround tries to unassociate monitor manually and without transactions 31 | via iControl REST API. 32 | 33 | :param loadbalancers: loadbalancers 34 | :param bigips: bigips 35 | """ 36 | if pool.health_monitor and driver_utils.pending_delete(pool.health_monitor): 37 | LOG.info("Disassociating health-monitor '%s'", pool.health_monitor.id) 38 | for bigip in bigips: 39 | try: 40 | pool_resource_path = (f'{F5_POOL_PATH}/~{m_tenant.get_name(network_id)}' 41 | f'~{m_app.get_name(loadbalancer_id)}~{m_pool.get_name(pool.id)}') 42 | pool_json = bigip.get(pool_resource_path) 43 | 44 | if pool_json.ok: 45 | pool_dict = pool_json.json() 46 | if 'monitor' in pool_dict: 47 | pool_dict['monitor'] = None 48 | bigip.put(pool_resource_path, json=pool_dict) 49 | else: 50 | LOG.warning("Disassociating health-monitor '%s' failed: %s", pool.health_monitor.id, 51 | pool_json.text) 52 | except exceptions.AS3Exception as e: 53 | LOG.warning("Disassociating health-monitor '%s' failed: %s", pool.health_monitor.id, e) 54 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/restclient/test_as3declaration.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from unittest import mock 16 | 17 | from oslo_config import cfg 18 | from oslo_config import fixture as oslo_fixture 19 | 20 | from octavia.db import models 21 | from octavia.tests.unit import base 22 | from octavia_f5.common import config # noqa 23 | from octavia_f5.restclient import as3declaration 24 | 25 | CONF = None 26 | 27 | 28 | class TestGetDeclaration(base.TestCase): 29 | 30 | def setUp(self): 31 | self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 32 | self.conf.config(group='controller_worker', 33 | network_driver='network_noop_driver_f5') 34 | super().setUp() 35 | 36 | @mock.patch("octavia_f5.utils.esd_repo.EsdRepository") 37 | @mock.patch("octavia_f5.network.drivers.noop_driver_f5.driver.NoopNetworkDriverF5" 38 | ".get_segmentation_id") 39 | def test_get_declaration(self, mock_get_segmentation_id, mock_esd_repo): 40 | mock_status_manager = mock.MagicMock() 41 | as3 = as3declaration.AS3DeclarationManager(mock_status_manager) 42 | mock_get_segmentation_id.side_effect = [1234, 2345] 43 | mock_lb = mock.Mock(spec=models.LoadBalancer) 44 | mock_lb.pools = [] 45 | mock_lb.listeners = [] 46 | 47 | self.assertIsInstance(as3, as3declaration.AS3DeclarationManager) 48 | 49 | # Ensure host / segment_ids are depending on the agent host 50 | self.conf.config(host="host1") 51 | decl = as3.get_declaration({'net1': [mock_lb]}, []) 52 | self.assertEqual(decl.declaration.net_net1.defaultRouteDomain, 1234) 53 | mock_get_segmentation_id.assert_called_with('net1', 'host1') 54 | 55 | self.conf.config(host="host2") 56 | decl = as3.get_declaration({'net1': [mock_lb]}, []) 57 | self.assertEqual(decl.declaration.net_net1.defaultRouteDomain, 2345) 58 | mock_get_segmentation_id.assert_called_with('net1', 'host2') 59 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3declaration.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import uuid 16 | 17 | from oslo_config import cfg 18 | 19 | from octavia_f5.restclient.as3classes import ADC 20 | from octavia_f5.restclient.as3objects import as3 as m_as3 21 | from octavia_f5.restclient.as3objects import tenant as m_tenant 22 | from octavia_f5.utils import driver_utils, cert_manager, esd_repo 23 | 24 | CONF = cfg.CONF 25 | 26 | 27 | class AS3DeclarationManager(object): 28 | def __init__(self, status_manager): 29 | self._esd_repo = esd_repo.EsdRepository() 30 | self._network_driver = driver_utils.get_network_driver() 31 | self._cert_manager = cert_manager.CertManagerWrapper() 32 | self._status_manager = status_manager 33 | 34 | def get_declaration(self, tenants, self_ips): 35 | """ Returns complete AS3 declaration 36 | 37 | :param tenants: dict of network_id: loadbalancers, multiple tenants supported 38 | :param self_ips: list of SelfIPs. They are removed from the declaration and an error is printed. 39 | 40 | :return: complete AS3 declaration 41 | """ 42 | 43 | # AS3 wrapper class 44 | declaration = m_as3.get_as3() 45 | 46 | # PUT ADC (Application Delivery Controller) 47 | adc = ADC( 48 | id=f"urn:uuid:{uuid.uuid4()}", 49 | label="F5 BigIP Octavia Provider") 50 | declaration.set_adc(adc) 51 | 52 | for network_id, loadbalancers in tenants.items(): 53 | # Fetch segmentation id 54 | segmentation_id = None 55 | if loadbalancers: 56 | segmentation_id = self._network_driver.get_segmentation_id(network_id, CONF.host) 57 | 58 | # get Tenant 59 | name = m_tenant.get_name(network_id) 60 | tenant = m_tenant.get_tenant(segmentation_id, loadbalancers, self_ips, 61 | self._status_manager, self._cert_manager, self._esd_repo) 62 | adc.set_tenant(name, tenant) 63 | 64 | return declaration 65 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/restclient/test_tenant.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import mock 16 | 17 | from octavia.common import constants 18 | from octavia.db import models 19 | from octavia.tests.unit import base 20 | from octavia_f5.restclient.as3classes import Tenant 21 | from octavia_f5.restclient.as3objects import tenant 22 | 23 | 24 | class TestGetTenant(base.TestCase): 25 | 26 | def test_get_tenant_with_skip_ips(self): 27 | mock_status_manager = mock.MagicMock() 28 | 29 | mock_members = [ 30 | models.Member(id='test_id_1', ip_address='1.2.3.4', weight=1, protocol_port=1234), 31 | models.Member(id='test_id_2', ip_address='2.3.4.5', weight=1, protocol_port=2345), 32 | models.Member(id='test_id_3', ip_address='3.4.5.6', weight=1, protocol_port=3456)] 33 | 34 | mock_lb = models.LoadBalancer( 35 | id='test_lb_id', 36 | vip=models.Vip(ip_address='1.2.3.4'), 37 | listeners=[], 38 | pools=[ 39 | models.Pool( 40 | id='test_pool_id', 41 | name='test_pool', 42 | lb_algorithm=constants.LB_ALGORITHM_ROUND_ROBIN, 43 | members=mock_members 44 | )], 45 | ) 46 | skip_ip = '2.3.4.5' 47 | 48 | as3 = tenant.get_tenant( 49 | segmentation_id=1234, 50 | loadbalancers=[mock_lb], 51 | self_ips=[skip_ip], 52 | status_manager=mock_status_manager, 53 | cert_manager=None, 54 | esd_repo=None) 55 | 56 | self.assertIsInstance(as3, Tenant) 57 | members = as3.lb_test_lb_id.pool_test_pool_id.members 58 | self.assertEqual(1, len(members)) 59 | self.assertEqual(['3.4.5.6'], members[0].serverAddresses) 60 | self.assertEqual(3456, members[0].servicePort) 61 | self.assertEqual('test_id_3', members[0].remark) 62 | mock_status_manager.set_error.assert_has_calls([ 63 | mock.call(mock_members[0]), 64 | mock.call(mock_members[1]), 65 | ]) 66 | -------------------------------------------------------------------------------- /ci/terraform/templates/etc/_main.tf: -------------------------------------------------------------------------------- 1 | # provider 2 | terraform { 3 | required_providers { 4 | openstack = { 5 | source = "terraform-providers/openstack" 6 | } 7 | } 8 | } 9 | 10 | provider "openstack" { 11 | version = ">= 1.19.0" 12 | 13 | user_name = var.os_username 14 | user_domain_name = var.os_user_domain 15 | tenant_name = var.os_project 16 | domain_name = var.os_domain 17 | password = var.os_password 18 | auth_url = var.os_auth_url 19 | region = var.os_region 20 | use_octavia = "true" 21 | } 22 | 23 | locals { 24 | count = 100 25 | } 26 | 27 | resource "openstack_lb_loadbalancer_v2" "terraform" { 28 | count = local.count 29 | name = "terraform-generated-loadbalancer-${count.index}" 30 | vip_network_id = var.priv_network 31 | } 32 | 33 | resource "openstack_lb_listener_v2" "terraform" { 34 | count = local.count 35 | name = "terraform-generated-listener-${count.index}" 36 | protocol = "HTTP" 37 | protocol_port = 8080 38 | loadbalancer_id = openstack_lb_loadbalancer_v2.terraform[count.index].id 39 | 40 | insert_headers = { 41 | X-Forwarded-For = "true" 42 | } 43 | } 44 | 45 | resource "openstack_lb_pool_v2" "terraform" { 46 | name = "terraform-generated-pool-${count.index}" 47 | count = local.count 48 | protocol = "HTTP" 49 | lb_method = "ROUND_ROBIN" 50 | listener_id = openstack_lb_listener_v2.terraform[count.index].id 51 | 52 | persistence { 53 | type = "APP_COOKIE" 54 | cookie_name = "testCookie" 55 | } 56 | } 57 | 58 | resource "openstack_lb_members_v2" "terraform" { 59 | pool_id = openstack_lb_pool_v2.terraform[count.index].id 60 | count = local.count 61 | 62 | member { 63 | address = "192.168.199.23" 64 | protocol_port = 8080 65 | } 66 | 67 | member { 68 | address = "192.168.199.24" 69 | protocol_port = 8080 70 | } 71 | } 72 | 73 | resource "openstack_lb_monitor_v2" "terraform" { 74 | count = local.count 75 | pool_id = openstack_lb_pool_v2.terraform[count.index].id 76 | type = "PING" 77 | delay = 20 78 | timeout = 10 79 | max_retries = 5 80 | } 81 | 82 | resource "openstack_lb_l7policy_v2" "terraform" { 83 | count = local.count 84 | name = "terraform-generated-l7policy-${count.index}" 85 | action = "REJECT" 86 | description = "test l7 policy" 87 | position = 1 88 | listener_id = openstack_lb_listener_v2.terraform[count.index].id 89 | } 90 | 91 | resource "openstack_lb_l7rule_v2" "terraform" { 92 | count = local.count 93 | l7policy_id = openstack_lb_l7policy_v2.terraform[count.index].id 94 | type = "PATH" 95 | compare_type = "EQUAL_TO" 96 | value = "/api" 97 | } 98 | 99 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/worker/test_sync_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from unittest import mock 16 | 17 | from oslo_config import cfg 18 | from oslo_config import fixture as oslo_fixture 19 | from oslo_log import log as logging 20 | 21 | import octavia.tests.unit.base as base 22 | from octavia.db import models 23 | from octavia.network import data_models as network_models 24 | # pylint: disable=unused-import 25 | from octavia_f5.common import config # noqa 26 | from octavia_f5.controller.worker import sync_manager 27 | 28 | CONF = cfg.CONF 29 | LOG = logging.getLogger(__name__) 30 | 31 | MOCK_BIGIP_HOSTNAME = 'test-guest-hostname' 32 | 33 | 34 | class TestSyncManager(base.TestCase): 35 | def setUp(self): 36 | conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 37 | conf.config(group='controller_worker', 38 | network_driver='network_noop_driver_f5') 39 | super().setUp() 40 | 41 | @mock.patch("octavia_f5.controller.worker.sync_manager.SyncManager.initialize_bigips") 42 | @mock.patch("octavia_f5.utils.esd_repo.EsdRepository") 43 | @mock.patch("octavia_f5.restclient.as3declaration.AS3DeclarationManager") 44 | def test_tenant_update_skip_selfips(self, mock_as3, mock_esd_repo, mock_init_bigips): 45 | mock_decl_manager = mock.Mock() 46 | mock_as3.return_value = mock_decl_manager 47 | 48 | bigip = mock.Mock() 49 | bigip.hostname = MOCK_BIGIP_HOSTNAME 50 | 51 | mock_init_bigips.side_effect = [[bigip]] 52 | status_manger = mock.MagicMock() 53 | loadbalancer_repo = mock.MagicMock() 54 | manager = sync_manager.SyncManager( 55 | status_manger, loadbalancer_repo) 56 | 57 | selfips = [network_models.Port(fixed_ips=[ 58 | network_models.FixedIP(ip_address='1.2.3.4')])] 59 | 60 | mock_lb = mock.Mock(spec=models.LoadBalancer) 61 | loadbalancer_repo.get_all_by_network.return_value = [mock_lb] 62 | with mock.patch('octavia_f5.db.api.session'): 63 | manager.tenant_update('test-net-id', selfips=selfips) 64 | mock_decl_manager.get_declaration.assert_called_with( 65 | {'test-net-id': [mock_lb]}, ['1.2.3.4']) 66 | pass 67 | -------------------------------------------------------------------------------- /octavia_f5/cmd/house_keeping.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Hewlett-Packard Development Company, L.P. 2 | # Copyright 2020 SAP SE 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | # 16 | 17 | import datetime 18 | import signal 19 | import sys 20 | import time 21 | 22 | import prometheus_client as prometheus 23 | from oslo_config import cfg 24 | from oslo_log import log as logging 25 | from oslo_reports import guru_meditation_report as gmr 26 | 27 | from octavia import version 28 | from octavia.common import service 29 | from octavia.controller.housekeeping import house_keeping 30 | 31 | LOG = logging.getLogger(__name__) 32 | CONF = cfg.CONF 33 | PROMETHEUS_PORT = 8000 34 | 35 | _metric_housekeeping = prometheus.Counter('octavia_housekeeping', 'How often housekeeping has been run.') 36 | _metric_housekeeping_exceptions = prometheus.Counter('octavia_housekeeping_exceptions', 'How often housekeeping failed.') 37 | 38 | 39 | def _mutate_config(): 40 | LOG.info("Housekeeping recieved HUP signal, mutating config.") 41 | CONF.mutate_config_files() 42 | 43 | 44 | def main(): 45 | """Perform db cleanup for old resources. 46 | 47 | Remove load balancers from database, which have been deleted have since expired. 48 | """ 49 | 50 | # perform the rituals 51 | service.prepare_service(sys.argv) 52 | gmr.TextGuruMeditation.setup_autorun(version) 53 | signal.signal(signal.SIGHUP, _mutate_config) 54 | 55 | # Read configuration 56 | interval = CONF.house_keeping.cleanup_interval 57 | lb_expiry = CONF.house_keeping.load_balancer_expiry_age 58 | 59 | # initialize 60 | prometheus.start_http_server(PROMETHEUS_PORT) 61 | db_cleanup = house_keeping.DatabaseCleanup() 62 | LOG.info("Starting house keeping at %s", str(datetime.datetime.utcnow())) 63 | LOG.info("House keeping interval: %s seconds", interval) 64 | LOG.info("House keeping load balancer expiration: %s seconds", lb_expiry) 65 | 66 | # start cleanup cycle 67 | while True: 68 | LOG.debug("Housekeeping") 69 | _metric_housekeeping.inc() 70 | try: 71 | db_cleanup.cleanup_load_balancers() 72 | except Exception as e: 73 | LOG.error(f'Housekeeping caught the following exception: {e}') 74 | _metric_housekeeping_exceptions.inc() 75 | time.sleep(interval) 76 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | # The format of this file isn't really documented; just use --generate-rcfile 2 | [MASTER] 3 | # Add to the black list. It should be a base name, not a 4 | # path. You may set this option multiple times. 5 | ignore=.git,tests 6 | 7 | [MESSAGES CONTROL] 8 | # NOTE: The options which do not need to be suppressed can be removed. 9 | disable= 10 | # "F" Fatal errors that prevent further processing 11 | # "I" Informational noise 12 | c-extension-no-member, 13 | locally-disabled, 14 | # "E" Error for important programming issues (likely bugs) 15 | import-error, 16 | not-callable, 17 | no-member, 18 | # "W" Warnings for stylistic problems or minor programming issues 19 | abstract-method, 20 | anomalous-backslash-in-string, 21 | arguments-differ, 22 | attribute-defined-outside-init, 23 | broad-except, 24 | fixme, 25 | global-statement, 26 | pointless-string-statement, 27 | protected-access, 28 | redefined-builtin, 29 | redefined-outer-name, 30 | signature-differs, 31 | unidiomatic-typecheck, 32 | unused-argument, 33 | unused-variable, 34 | useless-super-delegation, 35 | # "C" Coding convention violations 36 | invalid-name, 37 | line-too-long, 38 | missing-docstring, 39 | # "R" Refactor recommendations 40 | duplicate-code, 41 | too-few-public-methods, 42 | too-many-ancestors, 43 | too-many-arguments, 44 | too-many-branches, 45 | too-many-instance-attributes, 46 | too-many-lines, 47 | too-many-locals, 48 | too-many-public-methods, 49 | too-many-return-statements, 50 | too-many-statements, 51 | multiple-statements, 52 | duplicate-except, 53 | keyword-arg-before-vararg, 54 | useless-object-inheritance 55 | 56 | [BASIC] 57 | # Variable names can be 1 to 31 characters long, with lowercase and underscores 58 | variable-rgx=[a-z_][a-z0-9_]{0,30}$ 59 | 60 | # Argument names can be 2 to 31 characters long, with lowercase and underscores 61 | argument-rgx=[a-z_][a-z0-9_]{1,30}$ 62 | 63 | # Method names should be at least 3 characters long 64 | # and be lowercased with underscores 65 | method-rgx=([a-z_][a-z0-9_]{2,}|setUp|tearDown)$ 66 | 67 | # Module names matching 68 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ 69 | 70 | # Don't require docstrings on tests. 71 | no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ 72 | 73 | [FORMAT] 74 | # Maximum number of characters on a single line. 75 | max-line-length=79 76 | 77 | [VARIABLES] 78 | # List of additional names supposed to be defined in builtins. Remember that 79 | # you should avoid to define new builtins when possible. 80 | additional-builtins= 81 | 82 | [CLASSES] 83 | 84 | [IMPORTS] 85 | # Deprecated modules which should not be used, separated by a comma 86 | deprecated-modules= 87 | 88 | [TYPECHECK] 89 | # List of module names for which member attributes should not be checked 90 | ignored-modules=six.moves,_MovedItems 91 | 92 | [REPORTS] 93 | # Tells whether to display a full report or only the messages 94 | reports=no 95 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/certificate.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import base64 16 | 17 | from octavia_f5.common import constants 18 | from octavia_f5.restclient import as3types 19 | from octavia_f5.restclient.as3classes import Certificate, CA_Bundle 20 | 21 | 22 | def get_name(container_id): 23 | """Return AS3 object name for type certificate 24 | 25 | :param container_id: container_id of barbican container 26 | :return: AS3 object name 27 | """ 28 | return f"{constants.PREFIX_CONTAINER}{container_id}" 29 | 30 | 31 | def get_certificate(remark, tlscontainer): 32 | """Get AS3 Certificate object. 33 | 34 | :param remark: comment 35 | :param tlscontainer: tls container to create certificate object from 36 | :return: AS3 Certificate 37 | """ 38 | 39 | def _decode(pem): 40 | try: 41 | return pem.decode('utf-8').replace('\r', '').replace(' \n', '\n') 42 | except AttributeError: 43 | return pem.replace('\r', '').replace(' \n', '\n') 44 | 45 | # TLS certificate is always the first one 46 | certificates = [_decode(tlscontainer.certificate)] 47 | 48 | for intermediate in tlscontainer.intermediates: 49 | intermediate = _decode(intermediate) 50 | if intermediate not in certificates: 51 | certificates.append(intermediate) 52 | 53 | service_args = { 54 | 'remark': as3types.f5remark(remark), 55 | 'certificate': '\n'.join(certificates) 56 | } 57 | 58 | if tlscontainer.private_key: 59 | service_args['privateKey'] = _decode(tlscontainer.private_key) 60 | 61 | if tlscontainer.passphrase: 62 | service_args['passphrase'] = { 63 | 'ciphertext': base64.urlsafe_b64encode(tlscontainer.passphrase) 64 | } 65 | 66 | return Certificate(**service_args) 67 | 68 | 69 | def get_ca_bundle(bundle, remark='', label=''): 70 | """AS3 Certificate Authority Bundle object. 71 | 72 | :param bundle: the CA certificate bundle as PEM encoded bytes 73 | :param remark: comment 74 | :param label: label 75 | :return: AS3 CA_Bundle 76 | """ 77 | service_args = { 78 | 'remark': as3types.f5remark(remark), 79 | 'label': as3types.f5label(label), 80 | 'bundle': bundle.decode('utf-8').replace('\r', '') 81 | } 82 | return CA_Bundle(**service_args) 83 | -------------------------------------------------------------------------------- /octavia_f5/network/data_models.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_log import log as logging 16 | 17 | from octavia.common.data_models import BaseDataModel 18 | from octavia.network import base 19 | from octavia_f5.utils import driver_utils 20 | 21 | LOG = logging.getLogger(__name__) 22 | 23 | 24 | # pylint: disable=too-many-positional-arguments 25 | class Network(BaseDataModel): 26 | """ This is a helper class what can provide vlan tag and 27 | default gateway """ 28 | def __init__(self, id=None, name=None, subnets=None, 29 | project_id=None, admin_state_up=None, mtu=None, 30 | provider_network_type=None, 31 | provider_physical_network=None, 32 | provider_segmentation_id=None, 33 | router_external=None, 34 | port_security_enabled=None, 35 | segments=None): 36 | self.id = id 37 | self.name = name 38 | self.subnets = subnets 39 | self.project_id = project_id 40 | self.admin_state_up = admin_state_up 41 | self.provider_network_type = provider_network_type 42 | self.provider_physical_network = provider_physical_network 43 | self.provider_segmentation_id = provider_segmentation_id 44 | self.router_external = router_external 45 | self.mtu = mtu 46 | self.port_security_enabled = port_security_enabled 47 | self.segments = segments or [] 48 | self._network_driver = driver_utils.get_network_driver() 49 | 50 | def default_gateway_ip(self, subnet_id): 51 | subnet = self._network_driver.get_subnet(subnet_id) 52 | return subnet.gateway_ip 53 | 54 | def has_bound_segment(self): 55 | for segment in self.segments: 56 | if segment['provider:physical_network'] == self._network_driver.physical_network: 57 | return True 58 | return False 59 | 60 | @property 61 | def vlan_id(self): 62 | for segment in self.segments: 63 | if segment['provider:physical_network'] == self._network_driver.physical_network: 64 | return segment['provider:segmentation_id'] 65 | 66 | err = ('Error retrieving segment id for physical network ' 67 | f'{self._network_driver.physical_network} of network {self.id}') 68 | raise base.NetworkException(err) 69 | -------------------------------------------------------------------------------- /octavia_f5/utils/decorators.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import functools 16 | from contextlib import ContextDecorator 17 | from urllib.parse import urlparse 18 | 19 | from requests import HTTPError 20 | 21 | from octavia_f5.utils.exceptions import IControlRestException, F5osaException 22 | 23 | 24 | class RunHookOnException(object): 25 | def __init__(self, hook, exceptions=Exception): 26 | self.hook = hook 27 | self.exceptions = exceptions 28 | 29 | def __call__(self, func): 30 | functools.update_wrapper(self, func) 31 | 32 | def wrapper(*args, **kwargs): 33 | try: 34 | return func(*args, **kwargs) 35 | except self.exceptions: 36 | self.hook(*args, **kwargs) 37 | return func(*args, **kwargs) 38 | return wrapper 39 | 40 | 41 | class RaisesApiError(ContextDecorator): 42 | 43 | def __init__(self): 44 | self.exception_class = Exception 45 | 46 | def __enter__(self): 47 | return self 48 | 49 | def __exit__(self, exc_type, exc_val, traceback): 50 | if exc_type == HTTPError: 51 | parsed = urlparse(exc_val.request.url) 52 | 53 | # if a username is present, display it, but hide the password, 54 | # otherwise just display the hostname 55 | if parsed.username: 56 | redacted = parsed._replace(netloc=f"{parsed.username}:???@{parsed.hostname}").geturl() 57 | else: 58 | redacted = parsed.hostname 59 | 60 | # get error message from response 61 | try: 62 | err_msg = exc_val.response.json() 63 | if 'message' in err_msg: 64 | err_msg = err_msg['message'] 65 | except Exception: 66 | err_msg = exc_val.response.content 67 | 68 | # raise exception 69 | raise self.exception_class( 70 | f"HTTP {exc_val.response.status_code} for {exc_val.request.method} {redacted}: {err_msg}" 71 | ) 72 | 73 | return False 74 | 75 | 76 | class RaisesIControlRestError(RaisesApiError): 77 | def __init__(self): 78 | super().__init__() 79 | self.exception_class = IControlRestException 80 | 81 | 82 | class RaisesF5osaError(RaisesApiError): 83 | def __init__(self): 84 | super().__init__() 85 | self.exception_class = F5osaException 86 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/alembic_migrations/env.py: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Rackspace 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from __future__ import with_statement 16 | 17 | import sys 18 | 19 | from alembic import context 20 | from sqlalchemy import create_engine 21 | from sqlalchemy import pool 22 | # this is the Alembic Config object, which provides 23 | # access to the values within the .ini file in use. 24 | config = context.config 25 | try: 26 | octavia_config = config.octavia_config 27 | except AttributeError: 28 | print("Error: Please use the octavia-db-manage command for octavia" 29 | " alembic actions.") 30 | sys.exit(1) 31 | 32 | # add your model's MetaData object here 33 | # for 'autogenerate' support 34 | # from myapp import mymodel 35 | # target_metadata = mymodel.Base.metadata 36 | target_metadata = None 37 | 38 | # other values from the config, defined by the needs of env.py, 39 | # can be acquired: 40 | # my_important_option = config.get_main_option("my_important_option") 41 | # ... etc. 42 | OCTAVIA_F5_VERSION_TABLE = 'f5_alembic_version' 43 | 44 | 45 | def run_migrations_offline(): 46 | """Run migrations in 'offline' mode. 47 | 48 | This configures the context with just a URL 49 | and not an Engine, though an Engine is acceptable 50 | here as well. By skipping the Engine creation 51 | we don't even need a DBAPI to be available. 52 | 53 | Calls to context.execute() here emit the given string to the 54 | script output. 55 | 56 | """ 57 | context.configure(url=octavia_config.database.connection, 58 | target_metadata=target_metadata, 59 | version_table=OCTAVIA_F5_VERSION_TABLE) 60 | 61 | with context.begin_transaction(): 62 | context.run_migrations() 63 | 64 | 65 | def run_migrations_online(): 66 | """Run migrations in 'online' mode. 67 | 68 | In this scenario we need to create an Engine 69 | and associate a connection with the context. 70 | 71 | """ 72 | engine = create_engine( 73 | octavia_config.database.connection, 74 | poolclass=pool.NullPool) 75 | 76 | connection = engine.connect() 77 | context.configure( 78 | connection=connection, 79 | target_metadata=target_metadata, 80 | version_table=OCTAVIA_F5_VERSION_TABLE) 81 | 82 | try: 83 | with context.begin_transaction(): 84 | context.run_migrations() 85 | finally: 86 | connection.close() 87 | 88 | 89 | if context.is_offline_mode(): 90 | run_migrations_offline() 91 | else: 92 | run_migrations_online() 93 | -------------------------------------------------------------------------------- /octavia_f5/common/constants.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | PROJECT_ID = 'project_id' 16 | 17 | BIGIP = 'bigip' 18 | PREFIX_PROJECT = 'project_' 19 | PREFIX_LISTENER = 'listener_' 20 | PREFIX_TLS_LISTENER = 'tls_listener_' 21 | PREFIX_TLS_POOL = 'tls_pool_' 22 | PREFIX_CONTAINER = 'container_' 23 | PREFIX_CERTIFICATE = 'cert_' 24 | PREFIX_CIPHER_RULE = 'cipher_rule_' 25 | PREFIX_CIPHER_GROUP = 'cipher_group_' 26 | PREFIX_POOL = 'pool_' 27 | PREFIX_HEALTH_MONITOR = 'hm_' 28 | PREFIX_LOADBALANCER = 'lb_' 29 | PREFIX_POLICY = 'l7policy_' 30 | PREFIX_WRAPPER_POLICY = 'wrapper_policy_' 31 | PREFIX_NETWORK_LEGACY = 'net-' 32 | PREFIX_NETWORK = 'net_' 33 | PREFIX_SUBNET = 'sub_' 34 | PREFIX_IRULE = 'irule_' 35 | PREFIX_MEMBER = 'member_' 36 | PREFIX_SECRET = 'secret_' 37 | SUFFIX_ALLOWED_CIDRS = '_allowed_cidrs' 38 | 39 | APPLICATION_TCP = 'tcp' 40 | APPLICATION_UDP = 'udp' 41 | APPLICATION_HTTP = 'http' 42 | APPLICATION_HTTPS = 'https' 43 | APPLICATION_L4 = 'l4' 44 | APPLICATION_GENERIC = 'generic' 45 | APPLICATION_SHARED = 'shared' 46 | SUPPORTED_APPLICATION_TEMPLATES = (APPLICATION_TCP, APPLICATION_UDP, 47 | APPLICATION_HTTP, APPLICATION_HTTPS, 48 | APPLICATION_L4, APPLICATION_GENERIC, 49 | APPLICATION_SHARED) 50 | 51 | SERVICE_TCP = 'Service_TCP' 52 | SERVICE_UDP = 'Service_UDP' 53 | SERVICE_HTTP = 'Service_HTTP' 54 | SERVICE_HTTPS = 'Service_HTTPS' 55 | SERVICE_L4 = 'Service_L4' 56 | SERVICE_GENERIC = 'Service_Generic' 57 | SUPPORTED_SERVICES = (SERVICE_TCP, SERVICE_UDP, SERVICE_HTTP, 58 | SERVICE_HTTPS, SERVICE_L4, SERVICE_GENERIC) 59 | SERVICE_TCP_TYPES = (SERVICE_TCP, SERVICE_GENERIC, SERVICE_HTTP, SERVICE_HTTPS) 60 | SERVICE_HTTP_TYPES = (SERVICE_HTTP, SERVICE_HTTPS) 61 | 62 | # special listener tags 63 | LISTENER_TAG_NO_SNAT = 'ccloud_special_l4_deactivate_snat' 64 | LISTENER_TAG_MIRRORING = 'ccloud_special_tcp_mirror' 65 | 66 | ROLE_MASTER = 'MASTER' 67 | ROLE_BACKUP = 'BACKUP' 68 | 69 | SEGMENT = 'segment' 70 | VIF_TYPE = 'f5' 71 | ESD = 'esd' 72 | PROFILE_L4 = 'basic' 73 | DEVICE_OWNER_NETWORK_PREFIX = "network:" 74 | DEVICE_OWNER_LISTENER = DEVICE_OWNER_NETWORK_PREFIX + 'f5listener' 75 | DEVICE_OWNER_SELFIP = DEVICE_OWNER_NETWORK_PREFIX + 'f5selfip' 76 | DEVICE_OWNER_LEGACY = DEVICE_OWNER_NETWORK_PREFIX + 'f5lbaasv2' 77 | DEFAULT_PHYSICAL_INTERFACE = 'portchannel1' 78 | 79 | OPEN = 'OPEN' 80 | FULL = 'FULL' 81 | UP = 'UP' 82 | DOWN = 'DOWN' 83 | DRAIN = 'DRAIN' 84 | NO_CHECK = 'no check' 85 | MAINT = 'MAINT' 86 | 87 | F5_NETWORK_AGENT_TYPE = 'F5 Agent' 88 | 89 | HEALTH_MONITOR_DELAY_MAX = 3600 90 | 91 | # The list of required ciphers for HTTP2 92 | CIPHERS_HTTP2 = ['ECDHE-RSA-AES128-GCM-SHA256'] 93 | -------------------------------------------------------------------------------- /octavia_f5/cmd/status_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright 2015 Hewlett-Packard Development Company, L.P. 2 | # Copyright 2020 SAP SE 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | import os 17 | import signal 18 | import sys 19 | import threading 20 | 21 | from futurist import periodics 22 | from oslo_config import cfg 23 | from oslo_log import log as logging 24 | from oslo_reports import guru_meditation_report as gmr 25 | 26 | from octavia import version 27 | from octavia.common import rpc 28 | from octavia_f5.common import config 29 | from octavia_f5.common import backdoor 30 | from octavia_f5.controller.statusmanager import status_manager 31 | 32 | CONF = cfg.CONF 33 | LOG = logging.getLogger(__name__) 34 | 35 | 36 | def _mutate_config(*args, **kwargs): 37 | CONF.mutate_config_files() 38 | 39 | 40 | def _handle_mutate_config(listener_proc_pid, check_proc_pid, *args, **kwargs): 41 | LOG.info("Status Manager recieved HUP signal, mutating config.") 42 | _mutate_config() 43 | os.kill(listener_proc_pid, signal.SIGHUP) 44 | os.kill(check_proc_pid, signal.SIGHUP) 45 | 46 | 47 | def _prepare_service(argv=None): 48 | argv = argv or [] 49 | config.init(argv[1:]) 50 | logging.set_defaults() 51 | config.setup_logging(cfg.CONF) 52 | rpc.init() 53 | 54 | 55 | def main(): 56 | logging.register_options(cfg.CONF) 57 | _prepare_service(sys.argv) 58 | gmr.TextGuruMeditation.setup_autorun(version) 59 | sm = status_manager.StatusManager() 60 | signal.signal(signal.SIGHUP, _mutate_config) 61 | 62 | # most intervals lower than 60 would cause a health check to start while the last hasn't completed yet, 63 | # leading to too high CPU utilization 64 | min_health_check_interval = 60 65 | if CONF.status_manager.health_check_interval < min_health_check_interval: 66 | raise cfg.Error(msg=("To prevent device overload, the health check " 67 | f"interval must not be lower than {min_health_check_interval}")) 68 | 69 | @periodics.periodic(CONF.status_manager.health_check_interval, 70 | run_immediately=True) 71 | def periodic_status(): 72 | sm.heartbeat() 73 | 74 | status_check = periodics.PeriodicWorker( 75 | [(periodic_status, None, None)], 76 | schedule_strategy='aligned_last_finished') 77 | 78 | hm_status_thread = threading.Thread(target=status_check.start) 79 | hm_status_thread.daemon = True 80 | LOG.info("Status Manager process starts") 81 | hm_status_thread.start() 82 | 83 | backdoor.install_backdoor() 84 | 85 | def hm_exit(*args, **kwargs): 86 | status_check.stop() 87 | status_check.wait() 88 | sm.stats_executor.shutdown() 89 | sm.health_executor.shutdown() 90 | LOG.info("Status Manager executors terminated") 91 | signal.signal(signal.SIGINT, hm_exit) 92 | 93 | hm_status_thread.join() 94 | LOG.info("Status Manager terminated") 95 | 96 | 97 | if __name__ == "__main__": 98 | main() 99 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/alembic_migrations/versions/5f23f3721f6b_inital_create.py: -------------------------------------------------------------------------------- 1 | """initial_create 2 | 3 | Revision ID: 5f23f3721f6b 4 | Revises: None 5 | Create Date: 2019-04-08 15:38:36.415727 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '5f23f3721f6b' 13 | down_revision = None 14 | 15 | 16 | def upgrade(): 17 | op.create_table( 18 | u'f5_esd', 19 | sa.Column(u'name', sa.String(63), nullable=False), 20 | sa.PrimaryKeyConstraint(u'name') 21 | ) 22 | 23 | op.create_table( 24 | u'f5_esd_attributes', 25 | sa.Column(u'f5_esd_name', sa.String(36), nullable=False), 26 | sa.Column(u'name', sa.String(255), nullable=False), 27 | sa.Column(u'type', sa.String(255), nullable=False), 28 | sa.ForeignKeyConstraint([u'f5_esd_name'], [u'f5_esd.name'], 29 | name=u'fk_f5_esd_attributes_f5_esd_name') 30 | ) 31 | 32 | insert_table = sa.table( 33 | u'f5_esd', 34 | sa.column(u'name', sa.String) 35 | ) 36 | 37 | op.bulk_insert( 38 | insert_table, 39 | [ 40 | {'name': 'proxy_protocol_2edF_v1_0'}, 41 | {'name': 'proxy_protocol_V2_e8f6_v1_0'}, 42 | {'name': 'standard_tcp_a3de_v1_0'}, 43 | {'name': 'x_forward_5b6e_v1_0'}, 44 | {'name': 'one_connect_dd5c_v1_0'}, 45 | {'name': 'no_one_connect_3caB_v1_0'}, 46 | {'name': 'http_compression_e4a2_v1_0'}, 47 | {'name': 'cookie_encryption_b82a_v1_0'}, 48 | {'name': 'sso_22b0_v1_0'}, 49 | {'name': 'sso_required_f544_v1_0'}, 50 | {'name': 'http_redirect_a26c_v1_0'} 51 | ] 52 | ) 53 | 54 | insert_table = sa.table( 55 | u'f5_esd_attributes', 56 | sa.column(u'f5_esd_name', sa.String), 57 | sa.column(u'name', sa.String), 58 | sa.column(u'type', sa.String), 59 | ) 60 | 61 | op.bulk_insert( 62 | insert_table, 63 | [ 64 | {'f5_esd_name': 'proxy_protocol_2edF_v1_0', 65 | 'type': 'fastl4', 'name': ''}, 66 | {'f5_esd_name': 'proxy_protocol_2edF_v1_0', 67 | 'type': 'ctcp', 'name': 'tcp'}, 68 | {'f5_esd_name': 'proxy_protocol_2edF_v1_0', 69 | 'type': 'irule', 'name': 'proxy_protocol_2edF_v1_0'}, 70 | {'f5_esd_name': 'proxy_protocol_2edF_v1_0', 71 | 'type': 'one_connect', 'name': ''}, 72 | {'f5_esd_name': 'proxy_protocol_V2_e8f6_v1_0', 73 | 'type': 'fastl4', 'name': ''}, 74 | {'f5_esd_name': 'proxy_protocol_V2_e8f6_v1_0', 75 | 'type': 'ctcp', 'name': 'tcp'}, 76 | {'f5_esd_name': 'proxy_protocol_V2_e8f6_v1_0', 77 | 'type': 'irule', 'name': 'cc_proxy_protocol_V2_e8f6_v1_0'}, 78 | {'f5_esd_name': 'proxy_protocol_V2_e8f6_v1_0', 79 | 'type': 'one_connect', 'name': ''}, 80 | {'f5_esd_name': 'standard_tcp_a3de_v1_0', 81 | 'type': 'fastl4', 'name': ''}, 82 | {'f5_esd_name': 'standard_tcp_a3de_v1_0', 83 | 'type': 'ctcp', 'name': 'tcp'}, 84 | {'f5_esd_name': 'standard_tcp_a3de_v1_0', 85 | 'type': 'one_connect', 'name': ''}, 86 | {'f5_esd_name': 'x_forward_5b6e_v1_0', 87 | 'type': 'irule', 'name': 'cc_x_forward_5b6e_v1_0'}, 88 | {'f5_esd_name': 'one_connect_dd5c_v1_0', 89 | 'type': 'one_connect', 'name': 'oneconnect'}, 90 | ] 91 | ) 92 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/worker/test_set_queue.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import octavia.tests.unit.base as base 16 | from octavia_f5.controller.worker.set_queue import SetQueue 17 | 18 | 19 | class TestSetQueue(base.TestCase): 20 | def test_set_queue(self): 21 | queue = SetQueue() 22 | # Add items to the queue 23 | queue.put('item1') 24 | queue.put('item2') 25 | # Check that items are in the queue 26 | self.assertFalse(queue.empty()) 27 | 28 | rest = [queue.get(), queue.get()] 29 | self.assertTrue(queue.empty()) 30 | self.assertIn('item1', rest) 31 | self.assertIn('item2', rest) 32 | 33 | def test_set_queue_priority(self): 34 | # Test priority items in SetQueue 35 | 36 | queue = SetQueue() 37 | queue.put('item1') 38 | queue.put_priority('priority_item1') 39 | queue.put('item2') 40 | 41 | # Check that priority item is returned first 42 | self.assertEqual(queue.get(), 'priority_item1') 43 | 44 | rest = [queue.get(), queue.get()] 45 | self.assertTrue(queue.empty()) 46 | self.assertIn('item1', rest) 47 | self.assertIn('item2', rest) 48 | 49 | def test_set_queue_unique_items(self): 50 | # Test that SetQueue only contains unique items 51 | queue = SetQueue() 52 | queue.put('item1') 53 | queue.put('item1') 54 | queue.put('item2') 55 | queue.put_priority('priority_item1') 56 | queue.put_priority('priority_item1') 57 | queue.put_priority('priority_item2') 58 | 59 | # Check that only unique items are present 60 | self.assertEqual(queue.qsize(), 4) 61 | prio = [queue.get(), queue.get()] 62 | rest = [queue.get(), queue.get()] 63 | self.assertTrue(queue.empty()) 64 | 65 | self.assertIn('priority_item1', prio) 66 | self.assertIn('priority_item2', prio) 67 | self.assertIn('item1', rest) 68 | self.assertIn('item2', rest) 69 | 70 | def test_set_queue_size(self): 71 | # Test the size of the SetQueue 72 | queue = SetQueue() 73 | self.assertEqual(queue.qsize(), 0) 74 | 75 | queue.put('item1') 76 | self.assertEqual(queue.qsize(), 1) 77 | 78 | queue.put('item2') 79 | self.assertEqual(queue.qsize(), 2) 80 | 81 | queue.put_priority('priority_item1') 82 | self.assertEqual(queue.qsize(), 3) 83 | 84 | queue.get() 85 | self.assertEqual(queue.qsize(), 2) 86 | 87 | def test_set_queue_empty(self): 88 | # Test if the SetQueue is empty 89 | queue = SetQueue() 90 | self.assertTrue(queue.empty()) 91 | 92 | queue.put('item1') 93 | self.assertFalse(queue.empty()) 94 | 95 | queue.get() 96 | self.assertTrue(queue.empty()) 97 | 98 | queue.put_priority('priority_item1') 99 | self.assertFalse(queue.empty()) 100 | 101 | queue.get() 102 | self.assertTrue(queue.empty()) 103 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/worker/test_endpoint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import inspect 16 | import mock 17 | from oslo_config import cfg 18 | from oslo_config import fixture as oslo_fixture 19 | from oslo_utils import uuidutils 20 | 21 | from octavia.common import constants 22 | from octavia.controller.queue.v2 import endpoints 23 | from octavia.controller.worker.v2 import controller_worker as orig_controller_worker 24 | from octavia.tests.unit import base 25 | from octavia.tests.unit.controller.queue.v2 import test_endpoints 26 | from octavia_f5.controller.worker import controller_worker 27 | 28 | 29 | class TestEndpoint(test_endpoints.TestEndpoints): 30 | 31 | def setUp(self): 32 | super().setUp() 33 | 34 | conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 35 | conf.config(octavia_plugins='f5_plugin') 36 | 37 | self.worker_patcher = mock.patch('octavia_f5.controller.worker.' 38 | 'controller_worker.ControllerWorker', 39 | spec=controller_worker.ControllerWorker) 40 | self.worker_patcher.start() 41 | 42 | self.ep = endpoints.Endpoints() 43 | self.context = {} 44 | self.resource_updates = {} 45 | self.resource_id = 1234 46 | self.resource = {constants.ID: self.resource_id} 47 | self.server_group_id = 3456 48 | self.listener_dict = {constants.LISTENER_ID: uuidutils.generate_uuid()} 49 | self.loadbalancer_dict = { 50 | constants.LOADBALANCER_ID: uuidutils.generate_uuid() 51 | } 52 | self.flavor_id = uuidutils.generate_uuid() 53 | self.availability_zone = uuidutils.generate_uuid() 54 | 55 | def tearDown(self): 56 | super().tearDown() 57 | self.worker_patcher.stop() 58 | 59 | def test_add_loadbalancer(self): 60 | self.ep.add_loadbalancer(self.context, 61 | self.loadbalancer_dict['loadbalancer_id']) 62 | self.ep.worker.add_loadbalancer.assert_called_once_with( 63 | self.loadbalancer_dict['loadbalancer_id']) 64 | 65 | def test_remove_loadbalancer(self): 66 | self.ep.remove_loadbalancer(self.context, 67 | self.loadbalancer_dict['loadbalancer_id']) 68 | self.ep.worker.remove_loadbalancer.assert_called_once_with( 69 | self.loadbalancer_dict['loadbalancer_id']) 70 | 71 | 72 | class TestEndpointsCompatibillity(base.TestCase): 73 | 74 | def test_compatibility(self): 75 | supported = ['load_balancer', 'listener', 'pool', 'member', 76 | 'l7policy', 'health_monitor'] 77 | orig_methods = [m for m in dir(orig_controller_worker.ControllerWorker) if callable( 78 | getattr(orig_controller_worker.ControllerWorker, m)) and not m.startswith( 79 | '__') and any(r in m for r in supported)] 80 | for m in orig_methods: 81 | orig_args = list(inspect.signature(getattr(orig_controller_worker.ControllerWorker, m)).parameters.keys()) 82 | f5_args = list(inspect.signature(getattr(controller_worker.ControllerWorker, m)).parameters.keys()) 83 | self.assertEqual(orig_args, f5_args, f"Arguments for the method {m} are not identical") 84 | -------------------------------------------------------------------------------- /octavia_f5/db/scheduler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 SAP SE 2 | # # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from sqlalchemy import func, or_, and_ 16 | from oslo_config import cfg 17 | from oslo_log import log as logging 18 | 19 | from octavia.common import constants as consts 20 | from octavia.db import models 21 | from octavia.db import repositories 22 | 23 | CONF = cfg.CONF 24 | LOG = logging.getLogger(__name__) 25 | 26 | 27 | class Scheduler(object): 28 | def __init__(self): 29 | self.az_repo = repositories.AvailabilityZoneRepository() 30 | 31 | def get_candidates(self, session, az_name=None): 32 | """ Get F5 (active) BigIP host candidate depending on the load (amount of listeners in amphora vrrp_priority 33 | column or amount of load balancers) and the desired availability zone. 34 | 35 | :param session: A Sql Alchemy database session. 36 | :param az_name: Name of the availability zone to schedule to. If it is None, all F5 amphora are considered. 37 | """ 38 | 39 | # get all hosts 40 | # pylint: disable=singleton-comparison 41 | candidates = session.query( 42 | models.Amphora.compute_flavor, 43 | func.count(models.LoadBalancer.id) 44 | ).join( 45 | models.LoadBalancer, 46 | and_(models.Amphora.compute_flavor == models.LoadBalancer.server_group_id, 47 | models.LoadBalancer.provisioning_status != consts.DELETED), 48 | isouter=True 49 | ).filter( 50 | models.Amphora.role == consts.ROLE_MASTER, 51 | models.Amphora.load_balancer_id == None, # noqa: E711 52 | or_( 53 | # !='disabled' gives False on NULL, so we need to check for NULL (None) explicitly 54 | models.Amphora.vrrp_interface == None, # noqa: E711 55 | models.Amphora.vrrp_interface != 'disabled') 56 | ).group_by(models.Amphora.compute_flavor) 57 | 58 | if CONF.networking.agent_scheduler == "loadbalancer": 59 | # order by loadbalancer count 60 | candidates = candidates.order_by( 61 | func.count(models.LoadBalancer.id).asc(), 62 | models.Amphora.updated_at.desc()) 63 | else: 64 | # order by listener count 65 | candidates = candidates.order_by( 66 | models.Amphora.vrrp_priority.asc(), 67 | models.Amphora.updated_at.desc()) 68 | 69 | if az_name: 70 | # if az provided, filter hosts 71 | metadata = self.az_repo.get_availability_zone_metadata_dict(session, az_name) 72 | hosts = metadata.get('hosts', []) 73 | candidates = candidates.filter( 74 | models.Amphora.compute_flavor.in_(hosts)) 75 | else: 76 | # we need to filter out all az-aware hosts 77 | azs = set(az.name for az in self.az_repo.get_all(session)[0]) 78 | omit_hosts = set() 79 | for az in azs: 80 | metadata = self.az_repo.get_availability_zone_metadata_dict(session, az) 81 | omit_hosts.update(metadata.get('hosts', [])) 82 | candidates = candidates.filter( 83 | models.Amphora.compute_flavor.notin_(omit_hosts)) 84 | return [candidate[0] for candidate in candidates.all()] 85 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/tenant.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_config import cfg 16 | from oslo_log import log as logging 17 | 18 | from octavia_lib.common import constants as lib_consts 19 | from octavia.common import exceptions as o_exceptions 20 | from octavia_f5.common import constants 21 | from octavia_f5.restclient import as3classes as as3 22 | from octavia_f5.restclient.as3classes import Application 23 | from octavia_f5.restclient.as3objects import application as m_app 24 | from octavia_f5.restclient.as3objects import pool as m_pool 25 | from octavia_f5.restclient.as3objects import service as m_service 26 | from octavia_f5.utils import driver_utils 27 | 28 | CONF = cfg.CONF 29 | LAST_PERSIST = 0 30 | LOG = logging.getLogger(__name__) 31 | 32 | 33 | def get_name(network_id): 34 | return f"{constants.PREFIX_NETWORK}{network_id.replace('-', '_')}" 35 | 36 | 37 | # pylint: disable=too-many-positional-arguments 38 | def get_tenant(segmentation_id, loadbalancers, self_ips, status_manager, cert_manager, esd_repo): 39 | 40 | project_id = None 41 | if loadbalancers: 42 | project_id = loadbalancers[-1].project_id 43 | 44 | tenant_dict = {} 45 | if segmentation_id: 46 | tenant_dict['label'] = f'{constants.PREFIX_PROJECT}{project_id or 'none'}' 47 | tenant_dict['defaultRouteDomain'] = segmentation_id 48 | 49 | tenant = as3.Tenant(**tenant_dict) 50 | 51 | # Skip members with the same IP as a VIP or SelfIP 52 | ips_to_skip = [load_balancer.vip.ip_address for load_balancer in loadbalancers 53 | if not driver_utils.pending_delete(load_balancer)] + self_ips 54 | 55 | for loadbalancer in loadbalancers: 56 | # Skip load balancer in (pending) deletion 57 | if loadbalancer.provisioning_status in [lib_consts.PENDING_DELETE]: 58 | continue 59 | 60 | # Create generic application 61 | app = Application(constants.APPLICATION_GENERIC, label=loadbalancer.id) 62 | 63 | # Attach Octavia listeners as AS3 service objects 64 | for listener in loadbalancer.listeners: 65 | if not driver_utils.pending_delete(listener): 66 | try: 67 | service_entities = m_service.get_service(listener, cert_manager, esd_repo) 68 | app.add_entities(service_entities) 69 | except o_exceptions.CertificateRetrievalException as e: 70 | if getattr(e, 'status_code', 0) != 400: 71 | # Error connecting to keystore, skip tenant update 72 | raise e 73 | 74 | LOG.error("Could not retrieve certificate, assuming it is deleted, skipping " 75 | "listener '%s': %s", listener.id, e) 76 | if status_manager: 77 | # Key / Container not found in keystore 78 | status_manager.set_error(listener) 79 | 80 | # Attach pools 81 | for pool in loadbalancer.pools: 82 | if not driver_utils.pending_delete(pool): 83 | app.add_entities(m_pool.get_pool(pool, ips_to_skip, status_manager)) 84 | 85 | # Attach newly created application 86 | tenant.add_application(m_app.get_name(loadbalancer.id), app) 87 | 88 | return tenant 89 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/cipher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from oslo_config import cfg 16 | from oslo_log import log as logging 17 | 18 | from octavia.common import validate 19 | from octavia_f5.common import constants 20 | from octavia_f5.restclient.as3classes import Cipher_Rule, Cipher_Group 21 | 22 | CONF = cfg.CONF 23 | LOG = logging.getLogger(__name__) 24 | 25 | 26 | def get_cipher_rule_name(object_id, object_type): 27 | """Returns AS3 object name for Cipher Rule related to a listener or a pool 28 | 29 | :param object_id: octavia listener or pool id 30 | :return: AS3 object name 31 | """ 32 | return f"{constants.PREFIX_CIPHER_RULE}{object_type.lower()}_{object_id}" 33 | 34 | 35 | def get_cipher_group_name(object_id, object_type): 36 | """Returns AS3 object name for Cipher Group related to a listener or a pool 37 | 38 | :param object_id: octavia listener or pool id 39 | :return: AS3 object name 40 | """ 41 | return f"{constants.PREFIX_CIPHER_GROUP}{object_type.lower()}_{object_id}" 42 | 43 | 44 | def filter_cipher_suites(cipher_suites, object_name, object_id, http2=False): 45 | """Filter out cipher suites according to blocklist and allowlist. 46 | 47 | This is necessary, because there can be invalid cipher suites if e.g. a 48 | previously allowed cipher suite was added to the blocklist recently and 49 | listeners/pools using the cipher suite already existed. 50 | 51 | :param cipher_suites: String containing colon-separated list of cipher suites. 52 | :param object_name: A printable representation of the object to be logged, e.g. "Listener" or "Pool". 53 | :param object_id: ID of the object the cipher suites belong to. This is used for logging, so it should be a string. 54 | :param http2: HTTP2 protocol using 55 | :return String containing colon-separated list of non-blocked/allowed cipher suites. 56 | """ 57 | 58 | blocked_cipher_suites = validate.check_cipher_prohibit_list(cipher_suites) 59 | disallowed_cipher_suites = validate.check_cipher_allow_list(cipher_suites) 60 | rejected_cipher_suites = list(set(blocked_cipher_suites + disallowed_cipher_suites)) 61 | 62 | cipher_suites_list = cipher_suites.split(':') 63 | if rejected_cipher_suites: 64 | LOG.error(f"{object_name} object with ID {object_id} has invalid cipher suites " 65 | f"which won't be provisioned: {', '.join(rejected_cipher_suites)}") 66 | for c in rejected_cipher_suites: 67 | cipher_suites_list.remove(c) 68 | 69 | # Add required ciphers if HTTP2 is using 70 | for cipher in constants.CIPHERS_HTTP2: 71 | if cipher not in cipher_suites_list: 72 | LOG.warning(f"mandatory cipher {cipher} was added for {object_name} " 73 | f"{object_id} because HTTP2 is being used") 74 | cipher_suites_list.append(cipher) 75 | 76 | return cipher_suites_list 77 | 78 | 79 | def get_cipher_rule_and_group(ciphers, parent_obj, parent_id, http2=False): 80 | rule_name = get_cipher_rule_name(parent_id, parent_obj) 81 | group_name = get_cipher_group_name(parent_id, parent_obj) 82 | rule_args = { 83 | 'cipherSuites': filter_cipher_suites(ciphers, parent_obj, parent_id, http2) 84 | } 85 | group_args = { 86 | 'allowCipherRules': [{'use': rule_name}] 87 | } 88 | return group_name, [ 89 | (rule_name, Cipher_Rule(**rule_args)), 90 | (group_name, Cipher_Group(**group_args)) 91 | ] 92 | -------------------------------------------------------------------------------- /octavia_f5/cmd/f5_util.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | # 15 | 16 | import collections 17 | import sys 18 | 19 | import urllib3 20 | from oslo_config import cfg 21 | from oslo_log import log as logging 22 | 23 | from octavia.db import repositories as repo 24 | from octavia_f5.common import config 25 | from octavia_f5.controller.worker import sync_manager, status_manager 26 | from octavia_f5.db import api as db_apis 27 | from octavia_f5.db import repositories as f5_repos 28 | 29 | CONF = cfg.CONF 30 | 31 | 32 | def main(): 33 | """Manual syncing utility""" 34 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 35 | if len(sys.argv) == 1: 36 | print('Error: Config file must be specified.') 37 | print(f'{sys.argv[0]} --config-file ') 38 | return 1 39 | argv = sys.argv or [] 40 | CONF.register_cli_opts(config.f5_util_opts) 41 | logging.register_options(CONF) 42 | config.init(argv[1:]) 43 | logging.set_defaults() 44 | config.setup_logging(CONF) 45 | LOG = logging.getLogger('f5_util') 46 | CONF.log_opt_values(LOG, logging.DEBUG) 47 | 48 | if not CONF.all and not CONF.lb_id and not CONF.project_id and not CONF.agent_host: 49 | print('Error: One of --all, --lb_id, --project_id, --agent_host must be specified.') 50 | return 1 51 | 52 | _status_manager = status_manager.StatusManager() 53 | _loadbalancer_repo = f5_repos.LoadBalancerRepository() 54 | _sync_manager = sync_manager.SyncManager(_status_manager, _loadbalancer_repo) 55 | _quota_repo = repo.QuotasRepository() 56 | _reset_dict = { 57 | 'in_use_load_balancer': None, 58 | 'in_use_listener': None, 59 | 'in_use_pool': None, 60 | 'in_use_health_monitor': None, 61 | 'in_use_member': None, 62 | } 63 | _filter_dict = {'show_deleted': False, 'host': CONF.host} 64 | 65 | session = db_apis.get_session() 66 | if CONF.lb_id: 67 | _filter_dict.update(id=CONF.lb_id) 68 | elif CONF.project_id: 69 | _filter_dict.update(project_id=CONF.project_id) 70 | elif CONF.agent_host: 71 | _filter_dict.update(host=CONF.agent_host) 72 | # else --all 73 | with session.begin(): 74 | lbs = _loadbalancer_repo.get_all_from_host(session, **_filter_dict) 75 | LOG.info(f'Starting manual sync for load balancers "{[lb.id for lb in lbs]}" ' 76 | f'on host "{_filter_dict['host']}".') 77 | 78 | # deduplicate 79 | networks = collections.defaultdict(list) 80 | for lb in lbs: 81 | if lb not in networks[lb.vip.network_id]: 82 | networks[lb.vip.network_id].append(lb) 83 | 84 | # push configuration 85 | for network_id, loadbalancers in networks.items(): 86 | try: 87 | if _sync_manager.tenant_update(network_id): 88 | _status_manager.update_status(loadbalancers) 89 | lock_session = db_apis.get_session() 90 | lock_session.begin() 91 | for loadbalancer in loadbalancers: 92 | _quota_repo.update(lock_session, project_id=loadbalancer.project_id, quota=_reset_dict) 93 | lock_session.commit() 94 | except Exception as e: 95 | LOG.error("Exception while syncing loadbalancers %s: %s", [lb.id for lb in loadbalancers], e) 96 | 97 | return 0 98 | 99 | 100 | if __name__ == "__main__": 101 | main() 102 | -------------------------------------------------------------------------------- /octavia_f5/api/drivers/f5_driver/arbiter.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import abc 16 | 17 | from oslo_config import cfg 18 | from oslo_log import log as logging 19 | from taskflow import flow 20 | from taskflow.listeners import logging as tf_logging 21 | from taskflow.patterns import linear_flow, unordered_flow 22 | 23 | from octavia.common import base_taskflow 24 | from octavia_f5.controller.worker.tasks import network_tasks 25 | from octavia_f5.api.drivers.f5_driver.tasks import reschedule_tasks 26 | from octavia_f5.utils import driver_utils 27 | 28 | CONF = cfg.CONF 29 | LOG = logging.getLogger(__name__) 30 | 31 | 32 | class RescheduleMixin(object, metaclass=abc.ABCMeta): 33 | """Abstract mixin class for reschedule support in loadbalancer RPC 34 | 35 | """ 36 | def loadbalancer_add(self, loadbalancer_id, target_host): 37 | """Forces a loadbalancer to be added to the target host 38 | 39 | :param loadbalancer_id: loadbalancer id 40 | :param target_host: agent 41 | """ 42 | 43 | def loadbalancer_remove(self, loadbalancer_id, target_host): 44 | """Forces a loadbalancer to be removed from the target host 45 | 46 | :param loadbalancer_id: loadbalancer id 47 | :param target_host: agent 48 | """ 49 | 50 | 51 | class MigrationArbiter(RescheduleMixin): 52 | def __init__(self): 53 | self.tf_engine = base_taskflow.BaseTaskFlowEngine() 54 | self.network_driver = driver_utils.get_network_driver() 55 | 56 | def run_flow(self, func, *args, **kwargs): 57 | tf = self.tf_engine.taskflow_load( 58 | func(*args), **kwargs) 59 | with tf_logging.DynamicLoggingListener(tf, log=LOG): 60 | tf.run() 61 | 62 | def get_reschedule_flow(self) -> flow.Flow: 63 | # Prepare Self-IPs for target 64 | get_loadbalancer_task = reschedule_tasks.GetLoadBalancerByID() 65 | create_selfips_task = network_tasks.CreateSelfIPs(self.network_driver) 66 | wait_for_selfip_task = network_tasks.WaitForNewSelfIPs(self.network_driver) 67 | 68 | add_loadbalancer_task = reschedule_tasks.ForceAddLoadbalancer(rpc=self) 69 | get_old_agent_task = reschedule_tasks.GetOldAgentFromLoadBalancer() 70 | remove_loadbalancer_task = reschedule_tasks.ForceDeleteLoadbalancer(rpc=self) 71 | rewrite_loadbalancer_task = reschedule_tasks.RewriteLoadBalancerEntry() 72 | rewrite_amphora_task = reschedule_tasks.RewriteAmphoraEntry() 73 | 74 | all_selfips_task = network_tasks.AllSelfIPs(self.network_driver) 75 | get_vip_port_task = network_tasks.GetVIPFromLoadBalancer(self.network_driver) 76 | update_aap_task = network_tasks.UpdateAAP(self.network_driver) 77 | update_vip_task = network_tasks.UpdateVIP(self.network_driver) 78 | invalidate_cache_task = network_tasks.InvalidateCache(self.network_driver) 79 | 80 | add_remove_loadbalancer_flow = unordered_flow.Flow('add-remove-lb-flow') 81 | add_remove_loadbalancer_flow.add(add_loadbalancer_task, remove_loadbalancer_task) 82 | 83 | update_vip_sub_flow = linear_flow.Flow("update-vip-sub-flow") 84 | update_vip_sub_flow.add(get_vip_port_task, update_vip_task, all_selfips_task, update_aap_task) 85 | 86 | # update loadbalancer, amphora and vip and invalidate cache can be run parallelized 87 | update_database_flow = unordered_flow.Flow("database-update-flow") 88 | update_database_flow.add(rewrite_loadbalancer_task, rewrite_amphora_task, update_vip_sub_flow, 89 | invalidate_cache_task) 90 | 91 | reschedule_flow = linear_flow.Flow('reschedule-flow') 92 | reschedule_flow.add(get_loadbalancer_task, get_old_agent_task, create_selfips_task, 93 | wait_for_selfip_task, add_remove_loadbalancer_flow, 94 | update_database_flow) 95 | return reschedule_flow 96 | -------------------------------------------------------------------------------- /octavia_f5/certificates/manager/noop.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from octavia.certificates.common import local as local_common 16 | from octavia.certificates.manager import cert_mgr 17 | 18 | 19 | # pylint: disable=too-many-positional-arguments 20 | class NoopCertManager(cert_mgr.CertManager): 21 | def store_cert(self, context, certificate, private_key, intermediates=None, private_key_passphrase=None, 22 | expiration=None, name=None): 23 | pass 24 | 25 | def get_cert(self, context, cert_ref, resource_ref=None, check_only=False, service_name=None): 26 | cert_data = {} 27 | 28 | # Self-Signed test.example key: 29 | cert_data['private_key'] = """-----BEGIN PRIVATE KEY----- 30 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDO1ZiNs+E/NBX9 31 | yfWjojTILtHtD2yfYJxSn4QKYReoAb49SDczfR+N/0ncYDuwbKH6EjC7tl6t97lV 32 | oM4ZbE0UZX0our0/WqE0TwKK6svS55iztbdB3rMBOVLtmwtCG6hyG1JYVEcW5OcX 33 | u+pAVHuViWI1IGqt57/FfCUlJg0BeeS2fheSckRsmWITIdr9gIwR3eHIEHKjLiJH 34 | e5xJKhUErrFS4DmYeh3ZC91q7KxBWObBBXgYD/Pg8NyVj9GvF3VFKlVhytH/coKt 35 | zyiCx4NQs9Z+Ly/fvEQcHc7DJoXvdu6fFFfZjqpSaLngYCR0z7TfRfZYsCh13dt7 36 | dYZyF4Q/AgMBAAECggEAXgEiNrUcmLc5j4EszVQ5nQn6iz3JZp5oLf0l6/m44Lj6 37 | F6wsupARuV3f2fM67bJR4/BEiewXGAZRC6PsSA268pw1yD8nKBYu0jFevHh+brqn 38 | 4nWidqOaw+Gj2S3wbflYE5RrVo3nSXZ7uYPEsbwz9wDby72R/rwnosALudiTbKmC 39 | GHKuoGxdpczRTB4ziOA6EOo71cdIFKsG9f3iTmSYjgbos9dI/PE+v0GH/7tZURDS 40 | EwIyxenR6P3ri+Y+2eiE2+xZy1K3kvAyWOaBliL8oNSFwaOBl3+DV2KNsx7JiQGC 41 | JceL7+7RMLlUFp5Dhb5eZlVCDB0U/fLM+GvPjQgBoQKBgQD3opJGex6LGXuuETYk 42 | DSMEBmfMUOE1mG441e/ToBaoFeuj8Z4aXA3xFGIGRpvi7+DhddbWbIoFPOQL48w/ 43 | 0c5voMZ7EDO2FCAeIgwYwB+U43ZQyDbAcJUfrG5n6yGmyqtz9pwHD72Vw0XUvS1y 44 | EopnJA1Z/HLQM9iYE3KY9K/QFwKBgQDV0jMpBaF4bwcFZAY5NQ5H3gtq+23lBB9M 45 | ZLuVrLZWX/4KJFKH2RnS3SxHch6wFK+0Q848Y8mFn9/gt2L6DB1AZC18S89uPd3j 46 | 0rqrRCwcQUKfWxjX/OutaPKncacUz3m3jHQ+gOBIfezlhCAYx8Y9PaOgxCOevgad 47 | BU3JijKeGQKBgQDazBB0J7pf6r8lmF1+0wCaQNKbaubhhPH2U8hX8n2yO9P9AbHQ 48 | 1n8XAAxwQRjhFVNbwdN1l2cHo7pWawp/ZPACH0rfVvxppzSNi0Wm5LHCyosyawQ9 49 | WfvYhXDzboRIK4/7oOxRLO40kdl0U0YBITKaWPdXB7+mB/kavSwmyyNANwKBgQCL 50 | lyPhNxTYTBuYUFmjxVhiYLqxiB2RcqSAOg8gwtVzBE4UDux2Vax/NfcvWXhhWc/v 51 | bojYcgjhHKOK0A5k0b3TCNONHuz3upn+ntdQ8jud4pj88fsBHtQ5rJcl65O5iU2c 52 | H6zQFVDW4qbim+RcaSepWXFWhlX+z23/2rOSzI8JGQKBgADvpEmnndRFs4oDCPs7 53 | SrFHMnr44JBz11aJs9tzOvTbox17xpgWeSxA0oppaa59B5HCp1EBT7IRtyuU7SBo 54 | W3JrC0JjYlMhuH/qsooPi1eH+jYryrJlwsBwOwkOd5V4xPpDge5+2jWc06TGuaeC 55 | f5IVJc3ipPIWWC8IW8+pClHq 56 | -----END PRIVATE KEY-----""".encode("utf-8") 57 | 58 | cert_data['certificate'] = """-----BEGIN CERTIFICATE----- 59 | MIICqjCCAZICCQC6b92nP8oPhTANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAx0 60 | ZXN0LmV4YW1wbGUwHhcNMjAwMzAyMTU0MTU1WhcNMjEwMzAyMTU0MTU1WjAXMRUw 61 | EwYDVQQDDAx0ZXN0LmV4YW1wbGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK 62 | AoIBAQDO1ZiNs+E/NBX9yfWjojTILtHtD2yfYJxSn4QKYReoAb49SDczfR+N/0nc 63 | YDuwbKH6EjC7tl6t97lVoM4ZbE0UZX0our0/WqE0TwKK6svS55iztbdB3rMBOVLt 64 | mwtCG6hyG1JYVEcW5OcXu+pAVHuViWI1IGqt57/FfCUlJg0BeeS2fheSckRsmWIT 65 | Idr9gIwR3eHIEHKjLiJHe5xJKhUErrFS4DmYeh3ZC91q7KxBWObBBXgYD/Pg8NyV 66 | j9GvF3VFKlVhytH/coKtzyiCx4NQs9Z+Ly/fvEQcHc7DJoXvdu6fFFfZjqpSaLng 67 | YCR0z7TfRfZYsCh13dt7dYZyF4Q/AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAop 68 | 5YS7QPpDhGBs191rWgp00xnIJUtJfxvYJPdQ4M+yRAhlT3ioU4YLpEngLVsHtgtA 69 | +NGw/zoSEZAnQ+BqmIbB6DX3nR83za/LSEr8f6O7rKQrnRR/mYiFj1baR+i3i6fF 70 | 76FdzA/1ERn4l5XoWsu+InUiKx6mfyQc1C/EUjHcMF8CY9AK2LpicDhxaF/wtzNh 71 | 83/U96EXvpvcyaRlOIIv4qNNA2VtP0vjKEqSwZaauwwaPBKGMXr8iwBBrfQJkt7k 72 | xGfp3W7NmA9RJTWG7b7y1G5eZJZSKd7RqseUa6Xs5ddlirW5bNx6ebNBiwFTu+cX 73 | Bn8MYJefqUlQYyi745g= 74 | -----END CERTIFICATE-----""".encode("utf-8") 75 | return local_common.LocalCert(**cert_data) 76 | 77 | def delete_cert(self, context, cert_ref, resource_ref, service_name=None): 78 | pass 79 | 80 | def set_acls(self, context, cert_ref): 81 | pass 82 | 83 | def unset_acls(self, context, cert_ref): 84 | pass 85 | 86 | def get_secret(self, context, secret_ref): 87 | return "" 88 | -------------------------------------------------------------------------------- /octavia_f5/utils/cert_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import hashlib 16 | import tenacity 17 | 18 | from oslo_config import cfg 19 | from oslo_context import context as oslo_context 20 | from stevedore import driver as stevedore_driver 21 | 22 | from octavia.common import exceptions as octavia_exc 23 | from octavia.common.tls_utils import cert_parser 24 | from octavia_f5.common import constants 25 | from octavia_f5.restclient.as3objects import certificate as m_cert 26 | 27 | CONF = cfg.CONF 28 | RETRY_ATTEMPTS = 15 29 | RETRY_INITIAL_DELAY = 1 30 | RETRY_BACKOFF = 1 31 | RETRY_MAX = 5 32 | 33 | 34 | class CertManagerWrapper(object): 35 | def __init__(self): 36 | self.cert_manager = stevedore_driver.DriverManager( 37 | namespace='octavia.cert_manager', 38 | name=CONF.certificates.cert_manager, 39 | invoke_on_load=True, 40 | ).driver 41 | 42 | @tenacity.retry( 43 | retry=tenacity.retry_if_exception_type( 44 | octavia_exc.CertificateRetrievalException), 45 | wait=tenacity.wait_incrementing( 46 | RETRY_INITIAL_DELAY, RETRY_BACKOFF, RETRY_MAX), 47 | stop=tenacity.stop_after_attempt(RETRY_ATTEMPTS)) 48 | def get_certificates(self, obj, context=None): 49 | """Fetches certificates and creates dict out of octavia objects 50 | 51 | :param obj: octavia listener or pool object 52 | :param context: optional oslo_context 53 | :return: certificate dict 54 | """ 55 | certificates = [] 56 | cert_dict = cert_parser.load_certificates_data(self.cert_manager, obj, context) 57 | cert_dict['container_id'] = [] 58 | if obj.tls_certificate_id: 59 | cert_dict['container_id'].append(obj.tls_certificate_id.split('/')[-1]) 60 | if hasattr(obj, 'sni_containers') and obj.sni_containers: 61 | cert_dict['container_id'].extend([sni.tls_container_id.split('/')[-1] 62 | for sni in obj.sni_containers]) 63 | 64 | # Note, the first cert is the TLS default cert 65 | if cert_dict['tls_cert'] is not None: 66 | certificates.append({ 67 | 'id': f'{constants.PREFIX_CERTIFICATE}{cert_dict['tls_cert'].id}', 68 | 'as3': m_cert.get_certificate( 69 | f'Container {', '.join(cert_dict['container_id'])}', 70 | cert_dict['tls_cert']) 71 | }) 72 | 73 | for sni_cert in cert_dict['sni_certs']: 74 | certificates.append({ 75 | 'id': f'{constants.PREFIX_CERTIFICATE}{sni_cert.id}', 76 | 'as3': m_cert.get_certificate( 77 | f'Container {', '.join(cert_dict['container_id'])}', 78 | sni_cert) 79 | }) 80 | 81 | return certificates 82 | 83 | @tenacity.retry( 84 | retry=tenacity.retry_if_exception_type( 85 | octavia_exc.CertificateRetrievalException), 86 | wait=tenacity.wait_incrementing( 87 | RETRY_INITIAL_DELAY, RETRY_BACKOFF, RETRY_MAX), 88 | stop=tenacity.stop_after_attempt(RETRY_ATTEMPTS)) 89 | def load_secret(self, project_id, secret_ref): 90 | """Loads secrets from secret store 91 | 92 | :param project_id: project_id used for request context 93 | :param secret_ref: secret reference to secret store 94 | :return: tuple of secret name and secret itself 95 | """ 96 | if not secret_ref: 97 | return None 98 | context = oslo_context.RequestContext(project_id=project_id) 99 | secret = self.cert_manager.get_secret(context, secret_ref) 100 | try: 101 | secret = secret.encode('utf-8') 102 | except AttributeError: 103 | pass 104 | id = hashlib.sha1(secret).hexdigest() # nosec 105 | 106 | return f'{constants.PREFIX_SECRET}{id}', secret 107 | -------------------------------------------------------------------------------- /octavia_f5/db/migration/cli.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2019 SAP SE 2 | # Copyright (c) 2016 Catalyst IT Ltd. 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 13 | # implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | import os 18 | 19 | from alembic import command as alembic_cmd 20 | from alembic import config as alembic_cfg 21 | from alembic import util as alembic_u 22 | from oslo_config import cfg 23 | from oslo_db import options 24 | from oslo_log import log 25 | 26 | from octavia.i18n import _ 27 | 28 | CONF = cfg.CONF 29 | options.set_defaults(CONF) 30 | log.set_defaults() 31 | log.register_options(CONF) 32 | log.setup(CONF, 'octavia-f5-db-manage') 33 | 34 | 35 | def do_alembic_command(config, cmd, *args, **kwargs): 36 | try: 37 | getattr(alembic_cmd, cmd)(config, *args, **kwargs) 38 | except alembic_u.CommandError as e: 39 | alembic_u.err(str(e)) 40 | 41 | 42 | def do_check_migration(config, _cmd): 43 | do_alembic_command(config, 'branches') 44 | 45 | 46 | def add_alembic_subparser(sub, cmd): 47 | return sub.add_parser(cmd, help=getattr(alembic_cmd, cmd).__doc__) 48 | 49 | 50 | def do_upgrade(config, cmd): 51 | if not CONF.command.revision and not CONF.command.delta: 52 | raise SystemExit(_('You must provide a revision or relative delta')) 53 | 54 | revision = CONF.command.revision or '' 55 | if '-' in revision: 56 | raise SystemExit(_('Negative relative revision (downgrade) not ' 57 | 'supported')) 58 | 59 | delta = CONF.command.delta 60 | 61 | if delta: 62 | if '+' in revision: 63 | raise SystemExit(_('Use either --delta or relative revision, ' 64 | 'not both')) 65 | if delta < 0: 66 | raise SystemExit(_('Negative delta (downgrade) not supported')) 67 | revision = f'{revision}+{delta}' 68 | 69 | do_alembic_command(config, cmd, revision, sql=CONF.command.sql) 70 | 71 | 72 | def no_downgrade(config, cmd): 73 | raise SystemExit(_("Downgrade no longer supported")) 74 | 75 | 76 | def do_stamp(config, cmd): 77 | do_alembic_command(config, cmd, 78 | CONF.command.revision, 79 | sql=CONF.command.sql) 80 | 81 | 82 | def do_revision(config, cmd): 83 | do_alembic_command(config, cmd, 84 | message=CONF.command.message, 85 | autogenerate=CONF.command.autogenerate, 86 | sql=CONF.command.sql) 87 | 88 | 89 | def add_command_parsers(subparsers): 90 | for name in ['current', 'history', 'branches']: 91 | parser = add_alembic_subparser(subparsers, name) 92 | parser.set_defaults(func=do_alembic_command) 93 | 94 | help_text = (getattr(alembic_cmd, 'branches').__doc__ + 95 | ' and validate head file') 96 | parser = subparsers.add_parser('check_migration', help=help_text) 97 | parser.set_defaults(func=do_check_migration) 98 | 99 | parser = add_alembic_subparser(subparsers, 'upgrade') 100 | parser.add_argument('--delta', type=int) 101 | parser.add_argument('--sql', action='store_true') 102 | parser.add_argument('revision', nargs='?') 103 | parser.set_defaults(func=do_upgrade) 104 | 105 | parser = subparsers.add_parser('downgrade', help="(No longer supported)") 106 | parser.add_argument('None', nargs='?', help="Downgrade not supported") 107 | parser.set_defaults(func=no_downgrade) 108 | 109 | parser = add_alembic_subparser(subparsers, 'stamp') 110 | parser.add_argument('--sql', action='store_true') 111 | parser.add_argument('revision') 112 | parser.set_defaults(func=do_stamp) 113 | 114 | parser = add_alembic_subparser(subparsers, 'revision') 115 | parser.add_argument('-m', '--message') 116 | parser.add_argument('--autogenerate', action='store_true') 117 | parser.add_argument('--sql', action='store_true') 118 | parser.set_defaults(func=do_revision) 119 | 120 | 121 | command_opt = cfg.SubCommandOpt('command', 122 | title='Command', 123 | help='Available commands', 124 | handler=add_command_parsers) 125 | 126 | CONF.register_cli_opt(command_opt) 127 | 128 | 129 | def main(): 130 | config = alembic_cfg.Config( 131 | os.path.join(os.path.dirname(__file__), 'alembic.ini') 132 | ) 133 | config.set_main_option('script_location', 134 | 'octavia_f5.db.migration:alembic_migrations') 135 | # attach the octavia conf to the Alembic conf 136 | config.octavia_config = CONF 137 | 138 | CONF(project='octavia-f5') 139 | CONF.command.func(config, CONF.command.name) 140 | -------------------------------------------------------------------------------- /octavia_f5/common/backdoor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | from collections import Counter 15 | import gc 16 | import os 17 | import sys 18 | import time 19 | import traceback 20 | 21 | import manhole 22 | 23 | from oslo_config import cfg 24 | from oslo_log import log as logging 25 | 26 | LOG = logging.getLogger(__name__) 27 | 28 | 29 | def _find_objects_by_name(name): 30 | """Find all objects of the given class name""" 31 | objs = [o for o in gc.get_objects() 32 | if hasattr(o, "__class__") and o.__class__.__name__.startswith(name) 33 | ] 34 | objs.sort(key=lambda o: o.__class__.__name__) 35 | return objs 36 | 37 | 38 | def _find_objects(cls): 39 | """Find all objects of the given class""" 40 | return [o for o in gc.get_objects() 41 | if hasattr(o, "__class__") and isinstance(o, cls)] 42 | 43 | 44 | def _get_nativethreads(): 45 | """Return tracebacks of all native threads as string""" 46 | lines = [] 47 | for thread_id, stack in sys._current_frames().items(): 48 | lines.append(str(thread_id)) 49 | lines.extend(line.rstrip() for line in traceback.format_stack(stack)) 50 | lines.append('') 51 | return '\n'.join(lines) 52 | 53 | 54 | def _print_nativethreads(): 55 | """Print tracebacks of all native threads""" 56 | print(_get_nativethreads()) 57 | 58 | 59 | def _print_semaphores(): 60 | """Print all Semaphore objects used by oslo_concurrency.lockutils and 61 | their waiter count 62 | """ 63 | # local import as we don't want to keep that local variable in global scope 64 | from oslo_concurrency.lockutils import _semaphores # pylint: disable=C0415 65 | 66 | print('\n'.join(sorted([f"{name} - {len(s._cond._waiters)}" 67 | for name, s in _semaphores._semaphores.items()]))) 68 | 69 | 70 | def _get_heap(): 71 | # local imports as these are most likely never used anywhere 72 | from guppy import hpy # pylint: disable=C0415 73 | hp = hpy() 74 | heap = hp.heap() 75 | print("Heap Size : ", heap.size, " bytes") 76 | return heap 77 | 78 | 79 | def _time_it(fn, *args, **kwargs): 80 | """Call fn, measuring the time it takes with time.time()""" 81 | start = time.time() 82 | fn(*args, **kwargs) 83 | print(time.time() - start) 84 | 85 | 86 | def _profile_it(fn, *args, return_stats=False, **kwargs): 87 | """Call fn with profiling enabled 88 | 89 | Optionally returns the pstats.Stats created while profiling. 90 | """ 91 | # local imports as these are most likely never used anywhere 92 | import cProfile # pylint: disable=C0415 93 | import pstats # pylint: disable=C0415 94 | 95 | pr = cProfile.Profile() 96 | pr.runcall(fn, *args, **kwargs) 97 | pr.create_stats() 98 | ps = pstats.Stats(pr) 99 | 100 | if return_stats: 101 | return ps 102 | 103 | ps.sort_stats('tottime').print_stats(30) 104 | return None 105 | 106 | 107 | def _count_object_types(): 108 | """Return a collections.Counter containing class to count mapping 109 | of objects in gc 110 | """ 111 | return Counter(o.__class__ for o in gc.get_objects() 112 | if hasattr(o, '__class__')) 113 | 114 | 115 | backdoor_opts = [ 116 | cfg.StrOpt('backdoor_socket', 117 | help="Enable manhole backdoor, using the provided path" 118 | " as a unix socket that can receive connections. " 119 | "Inside the path {pid} will be replaced with" 120 | " the PID of the current process.") 121 | ] 122 | 123 | 124 | def install_backdoor(): 125 | """Start a backdoor shell for debugging connectable via UNIX socket""" 126 | cfg.CONF.register_opts(backdoor_opts) 127 | 128 | if not cfg.CONF.backdoor_socket: 129 | return 130 | 131 | try: 132 | socket_path = cfg.CONF.backdoor_socket.format(pid=os.getpid()) 133 | except (KeyError, IndexError, ValueError) as e: 134 | socket_path = cfg.CONF.backdoor_socket 135 | LOG.warning("Could not apply format string to backdoor socket" 136 | f"path ({e}) - continuing with unformatted path") 137 | 138 | manhole.install(patch_fork=False, socket_path=socket_path, 139 | daemon_connection=True, 140 | locals={ 141 | 'fo': _find_objects, 142 | 'fon': _find_objects_by_name, 143 | 'pnt': _print_nativethreads, 144 | 'gnt': _get_nativethreads, 145 | 'print_semaphores': _print_semaphores, 146 | 'time_it': _time_it, 147 | 'profile_it': _profile_it, 148 | 'count_object_types': _count_object_types, 149 | 'get_heap': _get_heap, 150 | }, 151 | redirect_stderr=False) 152 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/controller/worker/test_controller_worker.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | import json 16 | from unittest import mock 17 | 18 | from oslo_config import cfg 19 | from oslo_config import fixture as oslo_fixture 20 | from oslo_utils import uuidutils 21 | 22 | import octavia.tests.unit.base as base 23 | from octavia_f5.controller.worker import controller_worker 24 | 25 | CONF = cfg.CONF 26 | 27 | LB_ID = uuidutils.generate_uuid() 28 | NETWORK_ID = uuidutils.generate_uuid() 29 | _status_manager = mock.MagicMock() 30 | _vip_mock = mock.MagicMock() 31 | _vip_mock.network_id = NETWORK_ID 32 | _listener_mock = mock.MagicMock() 33 | _load_balancer_mock = mock.MagicMock() 34 | _load_balancer_mock.id = LB_ID 35 | _load_balancer_mock.listeners = [_listener_mock] 36 | _load_balancer_mock.vip = _vip_mock 37 | _load_balancer_mock.flavor_id = None 38 | _load_balancer_mock.availability_zone = None 39 | _selfip = mock.MagicMock() 40 | _db_session = mock.MagicMock() 41 | 42 | 43 | @mock.patch('octavia_f5.controller.worker.status_manager.StatusManager') 44 | @mock.patch('octavia_f5.controller.worker.sync_manager.SyncManager') 45 | @mock.patch('octavia_f5.db.api.session', return_value=_db_session) 46 | class TestControllerWorker(base.TestCase): 47 | def setUp(self): 48 | super().setUp() 49 | conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 50 | conf.config(group="f5_agent", prometheus=False) 51 | conf.config(group="controller_worker", network_driver='network_noop_driver_f5') 52 | # prevent ControllerWorker() from spawning threads 53 | conf.config(group="f5_agent", sync_immediately=False) 54 | 55 | @mock.patch('octavia.db.repositories.AvailabilityZoneRepository') 56 | @mock.patch('octavia.db.repositories.AvailabilityZoneProfileRepository') 57 | def test_register_in_availability_zone(self, 58 | mock_azp_repo, 59 | mock_az_repo, 60 | mock_api_session, 61 | mock_sync_manager, 62 | mock_status_manager): 63 | az = 'fake_az' 64 | fake_azp_id = uuidutils.generate_uuid() 65 | cw = controller_worker.ControllerWorker() 66 | 67 | begin_session = mock_api_session().begin().__enter__() 68 | 69 | # existing empty az 70 | mock_az_repo_instance = mock_az_repo.return_value 71 | mock_az_repo_instance.get.return_value.availability_zone_profile_id = fake_azp_id 72 | mock_az_repo_instance.get_availability_zone_metadata_dict.return_value = {'hosts': []} 73 | 74 | cw.register_in_availability_zone(az) 75 | 76 | mock_az_repo_instance.get.assert_called_once_with(begin_session, name=az) 77 | mock_az_repo_instance.get_availability_zone_metadata_dict.assert_called_once_with(begin_session, az) 78 | mock_azp_repo.return_value.update.assert_called_once_with( 79 | begin_session, id=fake_azp_id, availability_zone_data=json.dumps({'hosts': [CONF.host]})) 80 | 81 | # non-existing az 82 | mock_az_repo_instance.get.return_value = None 83 | cw.register_in_availability_zone(az) 84 | mock_az_repo.return_value.create.assert_called_once() 85 | mock_azp_repo.return_value.create.assert_called_once() 86 | 87 | @mock.patch('octavia.db.repositories.LoadBalancerRepository.get', 88 | return_value=_load_balancer_mock) 89 | @mock.patch('octavia_f5.db.repositories.LoadBalancerRepository.get_all_by_network', 90 | return_value=[_load_balancer_mock]) 91 | @mock.patch("octavia_f5.network.drivers.noop_driver_f5.driver.NoopNetworkDriverF5" 92 | ".ensure_selfips", 93 | return_value=([_selfip], [])) 94 | @mock.patch("octavia_f5.network.drivers.noop_driver_f5.driver.NoopNetworkDriverF5" 95 | ".cleanup_selfips") 96 | def test_remove_loadbalancer_last(self, 97 | mock_cleanup_selfips, 98 | mock_ensure_selfips, 99 | mock_lb_repo_get_all_by_network, 100 | mock_lb_repo_get, 101 | mock_api_session, 102 | mock_sync_manager, 103 | mock_status_manager): 104 | cw = controller_worker.ControllerWorker() 105 | cw.remove_loadbalancer(LB_ID) 106 | 107 | begin_session = mock_api_session().begin().__enter__() 108 | 109 | mock_lb_repo_get_all_by_network.assert_called_once_with(begin_session, network_id=NETWORK_ID, show_deleted=False) 110 | mock_lb_repo_get.assert_called_once_with(begin_session, id=LB_ID) 111 | mock_ensure_selfips.assert_called_with([_load_balancer_mock], CONF.host, cleanup_orphans=False) 112 | mock_cleanup_selfips.assert_called_with([_selfip]) 113 | -------------------------------------------------------------------------------- /octavia_f5/restclient/bigip/bigip_auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from urllib import parse 16 | 17 | import requests 18 | import tenacity 19 | from requests.auth import HTTPBasicAuth, AuthBase 20 | 21 | BIGIP_TOKEN_HEADER = 'X-F5-Auth-Token' 22 | BIGIP_TOKEN_HEADER_F5OS_A = 'X-Auth-Token' 23 | BIGIP_TOKEN_MAX_TIMEOUT = '36000' 24 | BIGIP_TOKENS_PATH = '/mgmt/shared/authz/tokens' 25 | BIGIP_LOGIN_PATH = '/mgmt/shared/authn/login' 26 | BIGIP_LOGIN_PATH_F5OS_A = '/api/data/openconfig-system:system/aaa' 27 | 28 | 29 | class BigIPBasicAuth(HTTPBasicAuth): 30 | """ A requests custom BasicAuth provider that just parses username 31 | and password from URL for HTTP basic authentication """ 32 | def __init__(self, url): 33 | self.url = url 34 | parse_result = parse.urlparse(url, allow_fragments=False) 35 | super().__init__(parse_result.username, parse.unquote(parse_result.password)) 36 | 37 | 38 | class BigIPTokenAuth(AuthBase): 39 | """ A requests custom Auth provider that installs a response hook to detect authentication 40 | responses and acquires a BigIP authentication token for follow up http requests. """ 41 | 42 | def __init__(self, url, f5os_a=False): 43 | """The f5os_a parameter defines whether we're talking to a F5OS-A API, which is used on rSeries devices. It 44 | must be supplied by the caller, because this class is instantiated for both, communication with BigIP guests 45 | and hosts, and they might use different APIs.""" 46 | self.url = url 47 | parse_result = parse.urlparse(url, allow_fragments=False) 48 | self.username = parse_result.username 49 | self.password = parse.unquote(parse_result.password) 50 | self.f5os_a = f5os_a 51 | # Use single global token 52 | self.token = None 53 | self._token_endpoint = BIGIP_LOGIN_PATH if not f5os_a else BIGIP_LOGIN_PATH_F5OS_A 54 | self._token_header = BIGIP_TOKEN_HEADER if not f5os_a else BIGIP_TOKEN_HEADER_F5OS_A 55 | 56 | def handle_401(self, r, **kwargs): 57 | """ This response hook will fetch a fresh token if encountered an 401 response code. 58 | It's loosely based on requests digest auth. 59 | 60 | :return: requests.Response 61 | """ 62 | if r.status_code != 401: 63 | return r 64 | 65 | # Consume content and release the original connection 66 | # to allow our new request to reuse the same one. 67 | # pylint: disable=pointless-statement 68 | # noinspection PyStatementEffect 69 | r.content 70 | r.raw.release_conn() 71 | prep = r.request.copy() 72 | prep.headers[self._token_header] = self.get_token() 73 | 74 | _r = r.connection.send(prep, **kwargs) 75 | _r.history.append(r) 76 | _r.request = prep 77 | 78 | return _r 79 | 80 | def __call__(self, r): 81 | # No token, no fun 82 | if self.token: 83 | r.headers[self._token_header] = self.token 84 | 85 | # handle 401 case 86 | r.register_hook('response', self.handle_401) 87 | return r 88 | 89 | @tenacity.retry( 90 | wait=tenacity.wait_incrementing(3, 5, 10), 91 | stop=tenacity.stop_after_attempt(3) 92 | ) 93 | def get_token(self): 94 | """ Get F5-Auth-Token 95 | https://clouddocs.f5.com/products/extensions/f5-declarative-onboarding/latest/authentication.html 96 | """ 97 | 98 | credentials = { 99 | "username": self.username, 100 | "password": self.password, 101 | } 102 | if not self.f5os_a: 103 | credentials["loginProviderName"] = "tmos" 104 | auth = (self.username, self.password) 105 | 106 | token_request_args = [parse.urljoin(self.url, self._token_endpoint)] 107 | token_request_kwargs = {"json": credentials, "auth": auth, "timeout": 10, "verify": False} 108 | if self.f5os_a: 109 | r = requests.head(*token_request_args, **token_request_kwargs) 110 | else: 111 | r = requests.post(*token_request_args, **token_request_kwargs) 112 | 113 | # Handle maximum active login tokens condition 114 | if not self.f5os_a and r.status_code == 400 and 'maximum active login tokens' in r.text: 115 | # Delete all existing tokens 116 | requests.delete(parse.urljoin(self.url, BIGIP_TOKENS_PATH), auth=auth, timeout=10, verify=False) 117 | r = requests.post(parse.urljoin(self.url, self._token_endpoint), json=credentials, 118 | auth=auth, timeout=10, verify=False) 119 | 120 | # Check response code 121 | r.raise_for_status() 122 | 123 | # extract token from response 124 | if self.f5os_a: 125 | token = r.headers[self._token_header] 126 | else: 127 | token = r.json()['token']['token'] 128 | 129 | # Increase timeout to max of 10 hours 130 | if not self.f5os_a: 131 | patch_timeout = {"timeout": BIGIP_TOKEN_MAX_TIMEOUT} 132 | requests.patch(f"{parse.urljoin(self.url, BIGIP_TOKENS_PATH)}/{token}", 133 | auth=auth, json=patch_timeout, timeout=10, verify=False) 134 | 135 | # Store and return token 136 | self.token = token 137 | return token 138 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/policy_endpoint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from octavia_lib.common import constants 16 | 17 | from octavia_f5.common import constants as f5_const 18 | from octavia_f5.restclient import as3types 19 | from octavia_f5.restclient.as3classes import Policy_Compare_String, Policy_Condition, Policy_Action, \ 20 | Endpoint_Policy_Rule, Endpoint_Policy 21 | from octavia_f5.restclient.as3objects import pool 22 | from octavia_f5.utils.exceptions import PolicyTypeNotSupported, CompareTypeNotSupported, PolicyActionNotSupported 23 | 24 | COMPARE_TYPE_MAP = { 25 | 'STARTS_WITH': 'starts-with', 26 | 'ENDS_WITH': 'ends-with', 27 | 'CONTAINS': 'contains', 28 | 'EQUAL_TO': 'equals' 29 | } 30 | COMPARE_TYPE_INVERT_MAP = { 31 | 'STARTS_WITH': 'does-not-start-with', 32 | 'ENDS_WITH': 'does-not-end-with', 33 | 'CONTAINS': 'does-not-contain', 34 | 'EQUAL_TO': 'does-not-equal' 35 | } 36 | COND_TYPE_MAP = { 37 | # constants.L7RULE_TYPE_HOST_NAME: {'match_key': 'host', 'type': 'httpUri'}, 38 | # Workaround for https://github.com/F5Networks/f5-appsvcs-extension/issues/229, match Host in httpHeader 39 | constants.L7RULE_TYPE_HOST_NAME: {'match_key': 'all', 'type': 'httpHeader', 'key_name': 'name', 40 | 'override_key': 'Host'}, 41 | constants.L7RULE_TYPE_PATH: {'match_key': 'path', 'type': 'httpUri'}, 42 | constants.L7RULE_TYPE_FILE_TYPE: {'match_key': 'extension', 'type': 'httpUri'}, 43 | constants.L7RULE_TYPE_HEADER: {'match_key': 'all', 'type': 'httpHeader', 'key_name': 'name'}, 44 | constants.L7RULE_TYPE_SSL_DN_FIELD: {'match_key': 'serverName', 'type': 'sslExtension'}, 45 | constants.L7RULE_TYPE_COOKIE: {'match_key': 'all', 'type': 'httpCookie', 'key_name': 'name'}, 46 | } 47 | SUPPORTED_ACTION_TYPE = [ 48 | constants.L7POLICY_ACTION_REDIRECT_TO_POOL, 49 | constants.L7POLICY_ACTION_REDIRECT_TO_URL, 50 | constants.L7POLICY_ACTION_REDIRECT_PREFIX, 51 | constants.L7POLICY_ACTION_REJECT 52 | ] 53 | 54 | 55 | def get_name(policy_id): 56 | return f"{f5_const.PREFIX_POLICY}{policy_id}" 57 | 58 | 59 | def get_wrapper_name(listener_id): 60 | return f"{f5_const.PREFIX_WRAPPER_POLICY}{listener_id}" 61 | 62 | 63 | def _get_condition(l7rule): 64 | if l7rule.type not in COND_TYPE_MAP: 65 | raise PolicyTypeNotSupported( 66 | f"l7policy-id={l7rule.l7policy_id}, l7rule-id={l7rule.id}, type={l7rule.type}") 67 | if l7rule.compare_type not in COMPARE_TYPE_MAP: 68 | raise CompareTypeNotSupported( 69 | f"l7policy-id={l7rule.l7policy_id}, l7rule-id={l7rule.id}, type={l7rule.compare_type}") 70 | 71 | args = {} 72 | if l7rule.invert: 73 | operand = COMPARE_TYPE_INVERT_MAP[l7rule.compare_type] 74 | else: 75 | operand = COMPARE_TYPE_MAP[l7rule.compare_type] 76 | condition = COND_TYPE_MAP[l7rule.type] 77 | values = [l7rule.value] 78 | compare_string = Policy_Compare_String(operand=operand, values=values) 79 | args[condition['match_key']] = compare_string 80 | args['type'] = condition['type'] 81 | if 'key_name' in condition: 82 | if 'override_key' in condition: 83 | args[condition['key_name']] = condition['override_key'] 84 | else: 85 | args[condition['key_name']] = l7rule.key 86 | return Policy_Condition(**args) 87 | 88 | 89 | def _get_action(l7policy): 90 | if l7policy.action not in SUPPORTED_ACTION_TYPE: 91 | raise PolicyActionNotSupported() 92 | 93 | args = {} 94 | if l7policy.action == constants.L7POLICY_ACTION_REDIRECT_TO_POOL: 95 | args['type'] = 'forward' 96 | pool_name = pool.get_name(l7policy.redirect_pool_id) 97 | args['select'] = {'pool': {'use': pool_name}} 98 | args['event'] = 'request' 99 | elif l7policy.action == constants.L7POLICY_ACTION_REDIRECT_TO_URL: 100 | args['type'] = 'httpRedirect' 101 | args['location'] = l7policy.redirect_url 102 | args['event'] = 'request' 103 | if l7policy.redirect_http_code: 104 | args['code'] = l7policy.redirect_http_code 105 | elif l7policy.action == constants.L7POLICY_ACTION_REDIRECT_PREFIX: 106 | args['type'] = 'httpRedirect' 107 | args['location'] = f'tcl:{l7policy.redirect_prefix}[HTTP::uri]' 108 | args['event'] = 'request' 109 | elif l7policy.action == constants.L7POLICY_ACTION_REJECT: 110 | args['type'] = 'drop' 111 | args['event'] = 'request' 112 | return Policy_Action(**args) 113 | 114 | 115 | def get_endpoint_policy(l7policies): 116 | wrapper_name = ', '.join([l7policy.name for l7policy in l7policies if l7policy.name]) 117 | wrapper_desc = ', '.join([l7policy.description for l7policy in l7policies if l7policy.description]) 118 | 119 | args = {} 120 | args['label'] = as3types.f5label(wrapper_name or wrapper_desc) 121 | args['remark'] = as3types.f5remark(wrapper_desc or wrapper_name) 122 | args['rules'] = [Endpoint_Policy_Rule( 123 | name=get_name(l7policy.id), 124 | label=as3types.f5label(l7policy.name or l7policy.description), 125 | remark=as3types.f5remark(l7policy.description or l7policy.name), 126 | conditions=[_get_condition(l7rule) for l7rule in l7policy.l7rules], 127 | actions=[_get_action(l7policy)] 128 | ) for l7policy in l7policies] 129 | args['strategy'] = 'first-match' 130 | return Endpoint_Policy(**args) 131 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/pool.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018, 2019, 2020 SAP SE 2 | # Copyright (c) 2014-2018, F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from oslo_log import log as logging 17 | 18 | from octavia.common import constants 19 | from octavia_f5.common import constants as f5_consts 20 | from octavia_f5.restclient import as3types 21 | from octavia_f5.restclient.as3classes import Pool, Pointer 22 | from octavia_f5.restclient.as3objects import monitor as m_monitor 23 | from octavia_f5.restclient.as3objects import pool_member as m_member 24 | from octavia_f5.utils import driver_utils as utils 25 | 26 | LOG = logging.getLogger(__name__) 27 | 28 | 29 | def get_name(pool_id): 30 | """Return AS3 object name for type pool 31 | 32 | :param pool_id: pool id 33 | :return: AS3 object name 34 | """ 35 | return f"{f5_consts.PREFIX_POOL}{pool_id}" 36 | 37 | 38 | def get_pool(pool, ips_to_skip, status): 39 | """Map Octavia Pool -> AS3 Pool object 40 | 41 | :param pool: octavia pool object 42 | :param ips_to_skip: already used VIPs and SelfIPs 43 | :param status: status manager instance 44 | :return: AS3 pool 45 | """ 46 | 47 | # Entities is a list of tuples, which each describe AS3 objects 48 | # which may reference each other but do not form a hierarchy. 49 | entities = [] 50 | lbaas_lb_method = pool.lb_algorithm.upper() 51 | lbmode = _set_lb_method(lbaas_lb_method, pool.members) 52 | 53 | service_args = { 54 | 'label': as3types.f5label(pool.name or pool.description), 55 | 'remark': as3types.f5remark(pool.description or pool.name), 56 | 'loadBalancingMode': lbmode, 57 | 'members': [], 58 | } 59 | 60 | # Never set priority group if there is only one member, even if it's a backup member 61 | enable_priority_group = False 62 | if len(pool.members) > 1: 63 | enable_priority_group = any(member.backup for member in pool.members) 64 | 65 | for member in pool.members: 66 | if not utils.pending_delete(member): 67 | if member.ip_address in ips_to_skip: 68 | LOG.warning("The member address %s of member %s (pool %s, LB %s) is already in use by another load balancer.", 69 | member.ip_address, member.id, member.pool.id, member.pool.load_balancer.id) 70 | if status: 71 | status.set_error(member) 72 | continue 73 | 74 | if member.ip_address == '0.0.0.0': 75 | LOG.warning("The member address 0.0.0.0 of member %s is prohibited.", member.id) 76 | if status: 77 | status.set_error(member) 78 | continue 79 | 80 | service_args['members'].append( 81 | m_member.get_member(member, enable_priority_group, pool.health_monitor)) 82 | 83 | # add custom member monitors 84 | if pool.health_monitor and (member.monitor_address or member.monitor_port): 85 | member_hm = m_monitor.get_monitor(pool.health_monitor, 86 | member.monitor_address or member.ip_address, 87 | member.monitor_port or member.protocol_port) 88 | entities.append((m_monitor.get_name(member.id), member_hm)) 89 | 90 | if pool.health_monitor and not utils.pending_delete( 91 | pool.health_monitor): 92 | monitor_name = m_monitor.get_name(pool.health_monitor.id) 93 | entities.append((monitor_name, m_monitor.get_monitor(pool.health_monitor))) 94 | service_args['monitors'] = [Pointer(use=monitor_name)] 95 | 96 | entities.append((get_name(pool.id), Pool(**service_args))) 97 | return entities 98 | 99 | 100 | # from service_adpater.py f5_driver-agent 101 | def _get_lb_method(method): 102 | """ Returns F5 load balancing mode for octavia pool lb-algorithm 103 | 104 | :param method: Octavia lb-algorithm 105 | :return: F5 load balancing mode 106 | """ 107 | lb_method = method.upper() 108 | 109 | if lb_method == constants.LB_ALGORITHM_LEAST_CONNECTIONS: 110 | return 'least-connections-member' 111 | if lb_method == 'RATIO_LEAST_CONNECTIONS': 112 | return 'ratio-least-connections-member' 113 | if lb_method == constants.LB_ALGORITHM_SOURCE_IP: 114 | return 'round-robin' 115 | if lb_method == 'OBSERVED_MEMBER': 116 | return 'observed-member' 117 | if lb_method == 'PREDICTIVE_MEMBER': 118 | return 'predictive-member' 119 | if lb_method == 'RATIO': 120 | return 'ratio-member' 121 | 122 | # every other algorithm are unsupported 123 | return 'round-robin' 124 | 125 | 126 | # from service_adpater.py f5_driver-agent 127 | def _set_lb_method(lbaas_lb_method, members): 128 | """Set pool lb method depending on member attributes. 129 | 130 | :param lbaas_lb_method: octavia loadbalancing method 131 | :param members: octavia members 132 | :return: F5 load balancing method 133 | """ 134 | lb_method = _get_lb_method(lbaas_lb_method) 135 | 136 | if lb_method == constants.LB_ALGORITHM_SOURCE_IP: 137 | return lb_method 138 | 139 | member_has_weight = False 140 | for member in members: 141 | if not utils.pending_delete(member) and member.weight > 1: 142 | member_has_weight = True 143 | break 144 | 145 | if member_has_weight: 146 | if lb_method == constants.LB_ALGORITHM_LEAST_CONNECTIONS: 147 | return _get_lb_method('RATIO_LEAST_CONNECTIONS') 148 | return _get_lb_method('RATIO') 149 | return lb_method 150 | -------------------------------------------------------------------------------- /octavia_f5/utils/esd_repo.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # Copyright 2014-2017 F5 Networks Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | """ 17 | Defines interface for ESD access that Resource or Octavia Controllers may 18 | reference 19 | """ 20 | 21 | import glob 22 | import json 23 | import os 24 | 25 | from oslo_config import cfg 26 | from oslo_log import log as logging 27 | 28 | from octavia.common import exceptions 29 | 30 | CONF = cfg.CONF 31 | 32 | LOG = logging.getLogger(__name__) 33 | 34 | 35 | class EsdJSONValidation(object): 36 | """Class reads the json file(s) 37 | It checks and parses the content of json file(s) to a dictionary 38 | """ 39 | 40 | def __init__(self, esddir): 41 | assert esddir is not None 42 | self.esdJSONFileList = glob.glob(os.path.join(esddir, '*.json')) 43 | assert self.esdJSONFileList 44 | self.esdJSONDict = {} 45 | 46 | def read_json(self): 47 | for fileList in self.esdJSONFileList: 48 | try: 49 | with open(fileList, encoding="utf-8") as json_file: 50 | # Reading each file to a dictionary 51 | file_json_dict = json.load(json_file) 52 | # Combine all dictionaries to one 53 | self.esdJSONDict.update(file_json_dict) 54 | 55 | except ValueError as err: 56 | LOG.error('ESD JSON File is invalid: %s', err) 57 | raise exceptions.InputFileError( 58 | file_name=fileList, 59 | reason=err 60 | ) 61 | 62 | return self.esdJSONDict 63 | 64 | 65 | class EsdRepository(EsdJSONValidation): 66 | """Class processes json dictionary 67 | It checks compares the tags from esdjson dictionary to list of valid tags 68 | """ 69 | 70 | def __init__(self): 71 | self.esd_dict = {} 72 | self.validtags = [] 73 | super().__init__(CONF.f5_agent.esd_dir) 74 | self.process_esd() 75 | 76 | # this function will return intersection of known valid esd tags 77 | # and the ones that user provided 78 | def valid_tag_key_subset(self): 79 | self.validtags = list(set(self.esdJSONDict.keys()) & 80 | set(self.valid_esd_tags.keys())) 81 | if not self.validtags: 82 | LOG.error("Intersect of valid esd tags and user esd tags is empty") 83 | 84 | if set(self.validtags) != set(self.esdJSONDict.keys()): 85 | LOG.error("invalid tags in the user esd tags") 86 | 87 | def process_esd(self): 88 | try: 89 | esd = self.read_json() 90 | self.esd_dict = self.verify_esd_dict(esd) 91 | except exceptions.InputFileError: 92 | self.esd_dict = {} 93 | raise 94 | 95 | def get_esd(self, name): 96 | return self.esd_dict.get(name, None) 97 | 98 | def is_valid_tag(self, tag): 99 | return self.valid_esd_tags.get(tag, None) is not None 100 | 101 | def verify_esd_dict(self, esd_dict): 102 | valid_esd_dict = {} 103 | for esd in esd_dict: 104 | # check that ESD is valid 105 | valid_esd = self.verify_esd(esd, esd_dict[esd]) 106 | if not valid_esd: 107 | break 108 | 109 | # add non-empty valid ESD to return dict 110 | valid_esd_dict[esd] = valid_esd 111 | 112 | return valid_esd_dict 113 | 114 | def verify_esd(self, name, esd): 115 | valid_esd = {} 116 | for tag in esd: 117 | try: 118 | self.verify_tag(tag) 119 | self.verify_value(tag, esd[tag]) 120 | 121 | # add tag to valid ESD 122 | valid_esd[tag] = esd[tag] 123 | LOG.debug(f"Tag {tag} is valid for ESD {name}.") 124 | except exceptions.InputFileError as err: 125 | LOG.info(f'Tag {tag} failed validation for ESD {name} and was not ' 126 | f'added to ESD. Error: {err.message}') 127 | 128 | return valid_esd 129 | 130 | def verify_value(self, tag, value): 131 | tag_def = self.valid_esd_tags.get(tag) 132 | 133 | # verify value type 134 | value_type = tag_def['value_type'] 135 | if not isinstance(value, value_type): 136 | msg = (f'Invalid value {value} for tag {tag}. ' 137 | f'Type must be {value_type}.') 138 | raise exceptions.InputFileError(filename='', reason=msg) 139 | 140 | def verify_tag(self, tag): 141 | if not self.is_valid_tag(tag): 142 | msg = f'Tag {tag} is not valid.' 143 | raise exceptions.InputFileError(filename='', reason=msg) 144 | 145 | # this dictionary contains all the tags 146 | # that are listed in the esd confluence page: 147 | # https://docs.f5net.com/display/F5OPENSTACKPROJ/Enhanced+Service+Definition 148 | # we are implementing the tags that can be applied only to listeners 149 | 150 | valid_esd_tags = { 151 | 'lbaas_fastl4': { 152 | 'value_type': str}, 153 | 'lbaas_ctcp': { 154 | 'value_type': str}, 155 | 'lbaas_stcp': { 156 | 'value_type': str}, 157 | 'lbaas_cudp': { 158 | 'value_type': str}, 159 | 'lbaas_http': { 160 | 'value_type': str}, 161 | 'lbaas_one_connect': { 162 | 'value_type': str}, 163 | 'lbaas_http_compression': { 164 | 'value_type': str}, 165 | 'lbaas_cssl_profile': { 166 | 'value_type': str}, 167 | 'lbaas_sssl_profile': { 168 | 'value_type': str}, 169 | 'lbaas_irule': { 170 | 'value_type': list}, 171 | 'lbaas_policy': { 172 | 'value_type': list}, 173 | 'lbaas_persist': { 174 | 'value_type': str}, 175 | 'lbaas_fallback_persist': { 176 | 'value_type': str} 177 | } 178 | -------------------------------------------------------------------------------- /octavia_f5/restclient/as3objects/monitor.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 SAP SE 2 | # Copyright (c) 2014-2018, F5 Networks, Inc. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # 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, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | 16 | from oslo_config import cfg 17 | from oslo_log import log as logging 18 | 19 | from octavia_f5.common import constants 20 | from octavia_f5.restclient import as3classes as as3 21 | from octavia_f5.restclient import as3types 22 | 23 | CONF = cfg.CONF 24 | LOG = logging.getLogger(__name__) 25 | TEMPLATE = """#!/bin/sh 26 | me=$(basename $0) 27 | 28 | pidfile="/var/run/$me-$1:$2.pid" 29 | 30 | if [ -f "$pidfile" ]; then 31 | kill -9 $(cat $pidfile) > /dev/null 2>&1 32 | fi 33 | 34 | echo "$$" > $pidfile 35 | 36 | node_ip=$(echo $1 | sed 's/::ffff://') 37 | pm_port="$2" 38 | 39 | {} 40 | 41 | if [ $? -eq 0 ] 42 | then 43 | rm -f $PIDFILE 44 | echo "UP" 45 | else 46 | rm -f $PIDFILE 47 | exit 48 | fi""" 49 | 50 | # Source: https://devcentral.f5.com/s/articles/https-monitor-ssl-handshake 51 | TLS_HELLO_CHECK = TEMPLATE.format("echo 'QUIT'|openssl s_client -verify 1 -connect $node_ip:$pm_port >/dev/null 2>&1") 52 | 53 | 54 | def get_name(healthmonitor_id): 55 | return f"{constants.PREFIX_HEALTH_MONITOR}{healthmonitor_id}" 56 | 57 | 58 | def get_monitor(health_monitor, target_address=None, target_port=None): 59 | args = {} 60 | 61 | # Standard Octavia monitor types 62 | if health_monitor.type == 'HTTP': 63 | args['monitorType'] = 'http' 64 | elif health_monitor.type == 'HTTPS': 65 | args['monitorType'] = 'https' 66 | elif health_monitor.type == 'PING': 67 | args['monitorType'] = 'icmp' 68 | elif health_monitor.type == 'TCP': 69 | args['monitorType'] = 'tcp' 70 | args['send'] = '' 71 | args['receive'] = '' 72 | elif health_monitor.type == 'TLS-HELLO': 73 | args['monitorType'] = 'external' 74 | args['script'] = TLS_HELLO_CHECK 75 | args['receive'] = 'UP' 76 | elif health_monitor.type == 'UDP-CONNECT': 77 | args['monitorType'] = 'udp' 78 | args['receive'] = '' 79 | args['send'] = '' 80 | 81 | # F5 specific monitory types 82 | elif health_monitor.type == 'SIP': 83 | args['monitorType'] = 'sip' 84 | elif health_monitor.type == 'SMTP': 85 | args['monitorType'] = 'smtp' 86 | elif health_monitor.type == 'TCP-HALF_OPEN': 87 | args['monitorType'] = 'tcp-half-open' 88 | elif health_monitor.type == 'LDAP': 89 | args['monitorType'] = 'ldap' 90 | elif health_monitor.type == 'DNS': 91 | args['monitorType'] = 'dns' 92 | args['queryName'] = health_monitor.domain_name 93 | # No Health monitor type available 94 | else: 95 | return {} 96 | 97 | if health_monitor.type in ('HTTP', 'HTTPS'): 98 | http_version = '1.0' 99 | if health_monitor.http_version: 100 | http_version = health_monitor.http_version 101 | send = f"{health_monitor.http_method} {health_monitor.url_path} HTTP/{http_version}\\r\\n" 102 | if health_monitor.domain_name: 103 | send += f"Host: {health_monitor.domain_name}\\r\\n\\r\\n" 104 | else: 105 | send += "\\r\\n" 106 | 107 | args['send'] = send 108 | args['receive'] = _get_recv_text(health_monitor) 109 | 110 | # BigIP does not have a semantic equivalent to the max_retries_down API parameter; The only available parameters 111 | # are interval (corresponding to 'delay' in the Octavia API) and timeout, both measured in seconds. So instead of 112 | # having an amount of probes (specified by max_retries_down) fail and each timing out after a number of seconds 113 | # (specified by the timeout API parameter) before setting a member to offline, we use max_retries_down together 114 | # with delay to calculate the timeout that we provision to the BigIP. 115 | args["interval"] = health_monitor.delay 116 | # This timeout must not be confused with the 'timeout' API parameter, which we ignore, because it denotes the 117 | # timeout of one probe, while the timeout AS3 parameter denotes the overall timeout over all probes! 118 | # The max_retries_down API parameter is called fall_threshold in the database 119 | timeout = int(health_monitor.fall_threshold) * int(health_monitor.delay) + 1 120 | 121 | # BigIP LTM (TMOS v15.1.3) maximum health monitor timeout seems to be 86400 seconds, at least on the GUI, but that 122 | # value is undocumented, so we're staying on the safe side with a lower value, until customer demand is present. 123 | args["timeout"] = min(timeout, 900) 124 | if target_address: 125 | args["targetAddress"] = target_address 126 | if target_port: 127 | args["targetPort"] = target_port 128 | 129 | if CONF.f5_agent.profile_healthmonitor_tls and health_monitor.type == 'HTTPS': 130 | args["clientTLS"] = as3.BigIP(CONF.f5_agent.profile_healthmonitor_tls) 131 | 132 | args['label'] = as3types.f5label(health_monitor.name or health_monitor.id) 133 | 134 | return as3.Monitor(**args) 135 | 136 | 137 | def _get_recv_text(healthmonitor): 138 | http_version = "1.(0|1)" 139 | if healthmonitor.http_version: 140 | http_version = f"{healthmonitor.http_version:1.1f}" 141 | 142 | try: 143 | if healthmonitor.expected_codes.find(",") > 0: 144 | status_codes = healthmonitor.expected_codes.split(',') 145 | recv_text = f"HTTP/{http_version} ({'|'.join(status_codes)})" 146 | elif healthmonitor.expected_codes.find("-") > 0: 147 | status_range = healthmonitor.expected_codes.split('-') 148 | start_range = status_range[0] 149 | stop_range = status_range[1] 150 | recv_text = f"HTTP/{http_version} [{start_range}-{stop_range}]" 151 | else: 152 | recv_text = f"HTTP/{http_version} {healthmonitor.expected_codes}" 153 | except Exception as exc: 154 | LOG.error( 155 | f"invalid monitor expected_codes={healthmonitor.expected_codes}, " 156 | f"http_version={healthmonitor.http_version}, defaulting to " 157 | f"'{CONF.f5_agent.healthmonitor_receive}': {exc}") 158 | recv_text = CONF.f5_agent.healthmonitor_receive 159 | return recv_text 160 | -------------------------------------------------------------------------------- /octavia_f5/tests/unit/db/test_scheduler.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 SAP SE 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 4 | # not use this file except in compliance with the License. You may obtain 5 | # 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, WITHOUT 11 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | # License for the specific language governing permissions and limitations 13 | # under the License. 14 | 15 | from unittest import mock 16 | 17 | from octavia.common import constants 18 | from oslo_config import cfg 19 | from oslo_config import fixture as oslo_fixture 20 | from oslo_utils import uuidutils 21 | 22 | from octavia.db import repositories as repo 23 | from octavia.tests.functional.db import base 24 | from octavia_f5.common import config, constants as f5_const # noqa 25 | from octavia_f5.controller.worker import controller_worker 26 | from octavia_f5.db import scheduler 27 | 28 | CONF = cfg.CONF 29 | 30 | 31 | class TestScheduler(base.OctaviaDBTestBase): 32 | FAKE_AZ = "fake-az" 33 | FAKE_LB_ID = uuidutils.generate_uuid() 34 | FAKE_PROJ_ID = uuidutils.generate_uuid() 35 | FAKE_DEVICE_AMPHORA_ID_1 = uuidutils.generate_uuid() 36 | FAKE_DEVICE_AMPHORA_ID_2 = uuidutils.generate_uuid() 37 | FAKE_DEVICE_PAIR_1 = "fake.device.pair1" 38 | FAKE_DEVICE_PAIR_2 = "fake.device.pair2" 39 | 40 | def setUp(self): 41 | super().setUp() 42 | self.repos = repo.Repositories() 43 | self.device_amphora_1 = self.repos.amphora.create( 44 | self.session, id=self.FAKE_DEVICE_AMPHORA_ID_1, 45 | role=constants.ROLE_MASTER, vrrp_interface=None, 46 | status=constants.ACTIVE, compute_flavor=self.FAKE_DEVICE_PAIR_1, 47 | vrrp_priority=1 48 | ) 49 | self.device_amphora_2 = self.repos.amphora.create( 50 | self.session, id=self.FAKE_DEVICE_AMPHORA_ID_2, 51 | role=constants.ROLE_MASTER, vrrp_interface=None, 52 | status=constants.ACTIVE, compute_flavor=self.FAKE_DEVICE_PAIR_2, 53 | vrrp_priority=100 54 | ) 55 | self.scheduler = scheduler.Scheduler() 56 | self.conf = self.useFixture(oslo_fixture.Config(cfg.CONF)) 57 | 58 | def test_get_candidate_without_lbs(self): 59 | self.conf.config(group="networking", agent_scheduler="loadbalancer") 60 | 61 | candidates = self.scheduler.get_candidates(self.session) 62 | self.assertEqual( 63 | candidates, [self.FAKE_DEVICE_PAIR_1, self.FAKE_DEVICE_PAIR_2], 64 | "Active device pairs without lbs not considered as candidates") 65 | 66 | def test_get_candidate_with_lbs(self): 67 | self.conf.config(group="networking", agent_scheduler="loadbalancer") 68 | lb = self._create_lb(self.FAKE_LB_ID, self.FAKE_DEVICE_PAIR_1) 69 | 70 | candidates = self.scheduler.get_candidates(self.session) 71 | self.assertEqual( 72 | candidates, [self.FAKE_DEVICE_PAIR_2, self.FAKE_DEVICE_PAIR_1], 73 | "Order of device pairs not consistent") 74 | self.repos.load_balancer.delete(self.session, id=lb.id) 75 | 76 | def test_get_candidate_by_listener(self): 77 | self.conf.config(group="networking", agent_scheduler="listener") 78 | 79 | candidates = self.scheduler.get_candidates(self.session) 80 | self.assertEqual( 81 | candidates, [self.FAKE_DEVICE_PAIR_1, self.FAKE_DEVICE_PAIR_2], 82 | "Order of device pairs not consistent") 83 | 84 | old_prio = self.device_amphora_1.vrrp_priority 85 | self.repos.amphora.update(self.session, self.device_amphora_1.id, 86 | vrrp_priority=1000) 87 | candidates = self.scheduler.get_candidates(self.session) 88 | self.assertEqual( 89 | candidates, [self.FAKE_DEVICE_PAIR_2, self.FAKE_DEVICE_PAIR_1], 90 | "Order of device pairs not consistent") 91 | self.repos.amphora.update(self.session, self.device_amphora_1.id, 92 | vrrp_priority=old_prio) 93 | 94 | def test_get_candidate_with_deleted(self): 95 | self.conf.config(group="networking", agent_scheduler="loadbalancer") 96 | lbs = [self._create_lb(uuidutils.generate_uuid(), self.FAKE_DEVICE_PAIR_1, 97 | provisioning_status=constants.DELETED), 98 | self._create_lb(uuidutils.generate_uuid(), self.FAKE_DEVICE_PAIR_2)] 99 | 100 | candidates = self.scheduler.get_candidates(self.session) 101 | self.assertEqual( 102 | candidates, [self.FAKE_DEVICE_PAIR_1, self.FAKE_DEVICE_PAIR_2], 103 | "Order of device pairs not consistent") 104 | for lb in lbs: 105 | self.repos.load_balancer.delete(self.session, id=lb.id) 106 | 107 | @mock.patch('octavia_f5.controller.worker.status_manager.StatusManager') 108 | @mock.patch('octavia_f5.controller.worker.sync_manager.SyncManager') 109 | def test_get_candidate_by_az(self, mock_sync_manager, mock_status_manager): 110 | self.conf.config(group="networking", agent_scheduler="loadbalancer") 111 | self.conf.config(group="f5_agent", prometheus=False) 112 | 113 | # Register host FAKE_DEVICE_PAIR_1 as fake-az 114 | cw = controller_worker.ControllerWorker() 115 | with mock.patch('octavia_f5.db.api.get_session', return_value=self.session): 116 | self.session.autocommit = False 117 | self.conf.config(host=self.FAKE_DEVICE_PAIR_1) 118 | cw.register_in_availability_zone(self.FAKE_AZ) 119 | self.session.autocommit = True 120 | 121 | candidates = self.scheduler.get_candidates(self.session, az_name=self.FAKE_AZ) 122 | self.assertEqual([self.FAKE_DEVICE_PAIR_1], candidates, 123 | "Candidates should only include AZ device pairs") 124 | 125 | candidates = self.scheduler.get_candidates(self.session) 126 | self.assertEqual([self.FAKE_DEVICE_PAIR_2], candidates, 127 | "Candidates should only include non-AZ device pairs") 128 | 129 | def _create_lb(self, id, host=FAKE_DEVICE_PAIR_1, 130 | provisioning_status=constants.ACTIVE): 131 | return self.repos.load_balancer.create( 132 | self.session, id=id, project_id=self.FAKE_PROJ_ID, 133 | name="lb_name", description="lb_description", 134 | provisioning_status=provisioning_status, 135 | operating_status=constants.ONLINE, 136 | server_group_id=host, enabled=True 137 | ) 138 | --------------------------------------------------------------------------------