From 2726f7ce906f32fbfcbf917e73d6ea2d4fadae65 Mon Sep 17 00:00:00 2001 From: "tomasz.tutka" Date: Mon, 29 Jun 2026 14:54:07 +0200 Subject: [PATCH 1/4] added tests for EIA and flow to run it --- .../workflows/fabric-integration-tests.yaml | 100 ++++ .gitignore | 5 + RUNNING_TESTS_LOCALLY.md | 193 ++++++++ .../api/internet_access_services_api.py | 9 +- .../models/internet_access_peering_ipv4.py | 16 +- .../models/internet_access_service.py | 16 +- .../fabricv4/models/port_sort_criteria.py | 4 +- .../models/ra_attachment_sort_item.py | 4 +- .../models/rf_attachment_sort_item.py | 4 +- .../route_aggregation_rule_sort_criteria.py | 4 +- .../models/route_aggregation_sort_criteria.py | 4 +- .../models/route_filter_rule_sort_criteria.py | 4 +- .../models/route_filter_sort_criteria.py | 4 +- ..._fix_sort_enum_default_leading_slash.patch | 154 +++++++ ..._fix_create_eia_service_201_response.patch | 55 +++ ...ernet_access_service_optional_fields.patch | 28 ++ ...0_fix_peering_ipv4_allow_cidr_suffix.patch | 48 ++ pytest.ini | 20 + run_fabric_tests.sh | 56 +++ tests/conftest.py | 44 ++ tests/requirements.txt | 6 + tests/services/__init__.py | 0 tests/services/fabricv4/__init__.py | 0 tests/services/fabricv4/helpers/__init__.py | 0 tests/services/fabricv4/helpers/apis.py | 31 ++ .../services/fabricv4/helpers/http_logging.py | 57 +++ .../fabricv4/helpers/token_generator.py | 50 ++ tests/services/fabricv4/helpers/users.py | 60 +++ tests/services/fabricv4/helpers/utils.py | 113 +++++ .../fabricv4/test_internet_access_api.py | 436 ++++++++++++++++++ 30 files changed, 1492 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/fabric-integration-tests.yaml create mode 100644 RUNNING_TESTS_LOCALLY.md create mode 100644 patches/services/fabricv4/20260629_fix_sort_enum_default_leading_slash.patch create mode 100644 patches/services/fabricv4/20260630_fix_create_eia_service_201_response.patch create mode 100644 patches/services/fabricv4/20260630_fix_internet_access_service_optional_fields.patch create mode 100644 patches/services/fabricv4/20260630_fix_peering_ipv4_allow_cidr_suffix.patch create mode 100644 pytest.ini create mode 100755 run_fabric_tests.sh create mode 100644 tests/conftest.py create mode 100644 tests/requirements.txt create mode 100644 tests/services/__init__.py create mode 100644 tests/services/fabricv4/__init__.py create mode 100644 tests/services/fabricv4/helpers/__init__.py create mode 100644 tests/services/fabricv4/helpers/apis.py create mode 100644 tests/services/fabricv4/helpers/http_logging.py create mode 100644 tests/services/fabricv4/helpers/token_generator.py create mode 100644 tests/services/fabricv4/helpers/users.py create mode 100644 tests/services/fabricv4/helpers/utils.py create mode 100644 tests/services/fabricv4/test_internet_access_api.py diff --git a/.github/workflows/fabric-integration-tests.yaml b/.github/workflows/fabric-integration-tests.yaml new file mode 100644 index 00000000..d004386b --- /dev/null +++ b/.github/workflows/fabric-integration-tests.yaml @@ -0,0 +1,100 @@ +name: Fabric integration tests + +on: + workflow_dispatch: + inputs: + env_url: + description: "Fabric base URL the tests run against" + required: true + default: "https://uatapi.equinix.com" + pull_request: + types: [labeled] + +permissions: + contents: read + +jobs: + tests: + if: >- + ${{ github.event_name == 'workflow_dispatch' + || (github.event_name == 'pull_request' + && github.event.label.name == 'run-fabric-tests') }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 + with: + ref: ${{ github.event.pull_request.head.sha || github.ref }} + + - name: Set up Python + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 + with: + python-version-file: .python-version + + - name: Install SDK and test dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e . + python -m pip install -r tests/requirements.txt + + - name: Write test data + env: + TEST_DATA_UAT: ${{ secrets.TEST_DATA_UAT }} + run: | + if [ -z "$TEST_DATA_UAT" ]; then + echo "::error::TEST_DATA_UAT secret is not set" + exit 1 + fi + printf '%s' "$TEST_DATA_UAT" > env.json + + - name: Run integration tests + env: + ENV_URL: ${{ inputs.env_url || 'https://uatapi.equinix.com' }} + run: | + # The suite auto-loads env.json from the repo root (written above). + ./run_fabric_tests.sh + + - name: Upload test reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: fabric-test-reports + path: reports/ + if-no-files-found: warn + retention-days: 30 + + - name: Summarize results + if: always() + id: summary + run: | + passed=0; failed=0; total=0 + if [ -f reports/fabric-tests.json ]; then + passed=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('passed',0))") + failed=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('failed',0)+s.get('error',0))") + total=$(python -c "import json;s=json.load(open('reports/fabric-tests.json'))['summary'];print(s.get('total',0))") + fi + echo "passed=$passed" >> "$GITHUB_OUTPUT" + echo "failed=$failed" >> "$GITHUB_OUTPUT" + echo "total=$total" >> "$GITHUB_OUTPUT" + + - name: Notify Slack + if: always() + uses: slackapi/slack-github-action@v2 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_ACCESS_TOKEN }} + payload: | + channel: digin-panthers-gha-automation + attachments: + - color: ${{ job.status == 'success' && 'good' || job.status == 'failure' && 'danger' || 'warning' }} + text: | + *Repository:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.repository }}> + *Workflow:* ${{ github.workflow }} + *Status:* ${{ job.status }} + *Test Runs:* ${{ steps.summary.outputs.total }} + *Results:* ${{ steps.summary.outputs.passed }} Passed, ${{ steps.summary.outputs.failed }} Failed + *Triggered by:* <@${{ github.actor }}> + + - name: Clean up test data + if: always() + run: rm -f env.json diff --git a/.gitignore b/.gitignore index 19e31eed..fd86244b 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,8 @@ target/ #Ipython Notebook .ipynb_checkpoints .openapi-generator + +# Fabric integration tests — test data / reports (never commit) +env.json +file.json +reports/ diff --git a/RUNNING_TESTS_LOCALLY.md b/RUNNING_TESTS_LOCALLY.md new file mode 100644 index 00000000..95e271d7 --- /dev/null +++ b/RUNNING_TESTS_LOCALLY.md @@ -0,0 +1,193 @@ +# Running the Fabric tests locally (step by step) + +How to run the Fabric integration tests on your own machine from scratch. +The tests are **on-demand** — a plain `pytest` does not run them. + +> Run every command from the repo root: +> `~/PycharmProjects/equinix-sdk-python` + +--- + +## 0. Prerequisites + +- **Python 3.10** (see `.python-version`). +- Network access (corporate VPN) to the Fabric environment you test against + (`ENV_URL`, e.g. `https://uatapi.equinix.com`). +- The **test-data JSON** (credentials per user). See step 3. + +--- + +## 1. Virtualenv + dependencies + +```bash +# venv (a .venv already exists in the repo; from scratch:) +python3 -m venv .venv +source .venv/bin/activate + +# the SDK in editable mode + test deps (pytest, requests) +pip install --upgrade pip +pip install -e . +pip install -r tests/requirements.txt +``` + +Confirm the SDK imports: + +```bash +python -c "from equinix.services import fabricv4; print('SDK import OK')" +``` + +--- + +## 2. Quick sanity check (no network) + +Confirms the tests are collected correctly and **skipped by default**: + +```bash +pytest -q # -> deselected (not run on a build) +pytest -m integration --collect-only -q # -> lists the tests +``` + +--- + +## 3. Provide the test data (`env.json`) + +The tests read user credentials from a JSON document. Save it as **`env.json`** +(or `file.json`) in the repo root — the suite auto-loads it. This is the same +content stored in CI as the `TEST_DATA_UAT` GitHub secret. + +Preferred shape — keyed by user name (`fcr` / `fnv`). You may put `envUrl` +next to the users instead of exporting `ENV_URL`: + +```json +{ + "envUrl": "https://uatapi.equinix.com", + "fnv": { + "client_id": "...", + "client_secret": "...", + "projectId": "...", + "accountNumberEIA": "...", + "iaProfileUuid": "..." + }, + "fcr": { + "client_id": "...", + "client_secret": "...", + "projectId": "..." + } +} +``` + +The legacy shape `{"users": [{"name": "fnv", ...}]}` is also accepted. A user +value may be a JSON-encoded string, and field names may be snake_case or +camelCase (`client_id`/`clientId`, etc.). + +How the tests resolve data (in order): +1. the `TEST_DATA_UAT_USERS` env var (if set), otherwise +2. a JSON file — `TEST_DATA_FILE`, else `env.json`/`file.json` in the cwd, then + the repo root. + +> The user key in the JSON (`fnv`/`fcr`) must match the one the test uses. +> `test_internet_access_api.py` uses `UserName.PANTHERS_FNV` → key `"fnv"`. + +--- + +## 4. Environment base URL + +If you didn't put `envUrl` in the data file, export it: + +```bash +export ENV_URL="https://uatapi.equinix.com" +``` + +--- + +## 5. Run the tests + +```bash +./run_fabric_tests.sh +``` + +The script activates `.venv`, checks that test data (env var or data file) and +a base URL are available, then runs `pytest -m integration -v`. + +Filtering / extra args pass through to pytest: + +```bash +./run_fabric_tests.sh -k test_3 # one scenario +./run_fabric_tests.sh tests/services/fabricv4/test_internet_access_api.py +./run_fabric_tests.sh -s # show print()/logs live +``` + +Or without the script: + +```bash +pytest -m integration -v +``` + +--- + +## See the HTTP requests (logging) + +Set `FABRIC_DEBUG` to log every request, its **body**, and the **response**: + +```bash +FABRIC_DEBUG=1 ./run_fabric_tests.sh +``` + +Example output (run script adds `-s` so logs stream live): + +``` +fabric.http INFO --> POST https://uatapi.equinix.com/fabric/v4/ports/search +fabric.http INFO request body: {'filter': {'or': [...]}, 'pagination': {...}} +fabric.http INFO <-- POST .../fabric/v4/ports/search -> HTTP 200 +fabric.http INFO response body: {"pagination":{...},"data":[...]} +``` + +- `FABRIC_DEBUG=1` — request URL + request body + response status + response + body. Headers are **not** logged, so the auth token never appears. +- `FABRIC_LOG_MAXLEN` — truncate logged bodies to N chars (default 2000; + `0` = no truncation), e.g. `FABRIC_LOG_MAXLEN=0 FABRIC_DEBUG=1 ./run_fabric_tests.sh`. +- `FABRIC_DEBUG=body` — additionally dumps full headers. **Debugging only** — + this prints the `Authorization: Bearer ...` header. + +A non-2xx status in the log is a failed request; the test assertion then +reports the failure (`with_http_info` calls assert on the status code). + +--- + +## Test reports + +`run_fabric_tests.sh` always writes three reports into `./reports/` (git-ignored): + +| File | Format | Use | +|---|---|---| +| `reports/fabric-tests.xml` | JUnit XML | CI (GitHub Actions, Jenkins) | +| `reports/fabric-tests.html` | HTML (self-contained) | open in a browser | +| `reports/fabric-tests.json` | JSON | machine-readable / dashboards | + +Change the directory with `REPORTS_DIR=out ./run_fabric_tests.sh`. + +--- + +## All in one (shortcut) + +```bash +source .venv/bin/activate +# put the test-data JSON in env.json (repo root), then: +export ENV_URL="https://uatapi.equinix.com" # or add "envUrl" to env.json +./run_fabric_tests.sh # data file auto-loaded +``` + +--- + +## Troubleshooting + +| Symptom | Cause / fix | +|---|---| +| `skipped` instead of running | no test data or base URL — provide `env.json` and `ENV_URL` (steps 3–4) | +| `User 'fnv' not found` | the user key in the JSON doesn't match the `UserName` used by the test | +| `401/403` from the Fabric API | wrong `client_id`/`client_secret` or wrong `ENV_URL` | +| timeout / no connection to `ENV_URL` | not on the corporate VPN | +| import error `equinix.services.fabricv4` | running on code before the SDK fixes — update your branch | + +> `env.json` / `file.json` contain secrets — do not commit them (they are +> git-ignored). diff --git a/equinix/services/fabricv4/api/internet_access_services_api.py b/equinix/services/fabricv4/api/internet_access_services_api.py index 50870d45..af743990 100644 --- a/equinix/services/fabricv4/api/internet_access_services_api.py +++ b/equinix/services/fabricv4/api/internet_access_services_api.py @@ -94,6 +94,7 @@ def create_eia_service( _response_types_map: Dict[str, Optional[str]] = { '202': "InternetAccessService", + '201': "InternetAccessService", '400': "List[Error]", '403': "List[Error]", '500': "List[Error]", @@ -164,6 +165,7 @@ def create_eia_service_with_http_info( _response_types_map: Dict[str, Optional[str]] = { '202': "InternetAccessService", + '201': "InternetAccessService", '400': "List[Error]", '403': "List[Error]", '500': "List[Error]", @@ -234,6 +236,7 @@ def create_eia_service_without_preload_content( _response_types_map: Dict[str, Optional[str]] = { '202': "InternetAccessService", + '201': "InternetAccessService", '400': "List[Error]", '403': "List[Error]", '500': "List[Error]", @@ -376,7 +379,7 @@ def delete_eia_service( ) _response_types_map: Dict[str, Optional[str]] = { - '202': "InternetAccessService", + '202': None, '404': "List[Error]", '409': "List[Error]", } @@ -445,7 +448,7 @@ def delete_eia_service_with_http_info( ) _response_types_map: Dict[str, Optional[str]] = { - '202': "InternetAccessService", + '202': None, '404': "List[Error]", '409': "List[Error]", } @@ -514,7 +517,7 @@ def delete_eia_service_without_preload_content( ) _response_types_map: Dict[str, Optional[str]] = { - '202': "InternetAccessService", + '202': None, '404': "List[Error]", '409': "List[Error]", } diff --git a/equinix/services/fabricv4/models/internet_access_peering_ipv4.py b/equinix/services/fabricv4/models/internet_access_peering_ipv4.py index 14c3d6e4..6b33a7fb 100644 --- a/equinix/services/fabricv4/models/internet_access_peering_ipv4.py +++ b/equinix/services/fabricv4/models/internet_access_peering_ipv4.py @@ -48,8 +48,8 @@ def equinix_peer_ip_validate_regular_expression(cls, value): if value is None: return value - if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): - raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") + if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): + raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") return value @field_validator('customer_peer_ip') @@ -58,8 +58,8 @@ def customer_peer_ip_validate_regular_expression(cls, value): if value is None: return value - if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): - raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") + if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): + raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") return value @field_validator('equinix_vrrp_ip') @@ -68,8 +68,8 @@ def equinix_vrrp_ip_validate_regular_expression(cls, value): if value is None: return value - if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): - raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") + if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): + raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") return value @field_validator('customer_vrrp_ip') @@ -78,8 +78,8 @@ def customer_vrrp_ip_validate_regular_expression(cls, value): if value is None: return value - if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): - raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") + if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): + raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") return value model_config = ConfigDict( diff --git a/equinix/services/fabricv4/models/internet_access_service.py b/equinix/services/fabricv4/models/internet_access_service.py index ca936ea5..55496c91 100644 --- a/equinix/services/fabricv4/models/internet_access_service.py +++ b/equinix/services/fabricv4/models/internet_access_service.py @@ -40,15 +40,15 @@ class InternetAccessService(BaseModel): bandwidth: Optional[StrictInt] = Field(default=None, description="Bandwidth of the service") bandwidth_commit: Optional[StrictInt] = Field(default=None, description="Minimum bandwidth commit for burst billing variant of the service", alias="bandwidthCommit") state: InternetAccessServiceState - change: InternetAccessChange + change: Optional[InternetAccessChange] = None locations: Optional[List[InternetAccessLocation]] = Field(default=None, description="List of locations associated with the service") - routing_protocol: InternetAccessRoutingProtocol = Field(alias="routingProtocol") - billing: InternetAccessBilling - account: InternetAccessAccount - project: Project - order: InternetAccessOrder - change_log: Changelog = Field(alias="changeLog") - use_case: InternetAccessUseCase = Field(alias="useCase") + routing_protocol: Optional[InternetAccessRoutingProtocol] = Field(default=None, alias="routingProtocol") + billing: Optional[InternetAccessBilling] = None + account: Optional[InternetAccessAccount] = None + project: Optional[Project] = None + order: Optional[InternetAccessOrder] = None + change_log: Optional[Changelog] = Field(default=None, alias="changeLog") + use_case: Optional[InternetAccessUseCase] = Field(default=None, alias="useCase") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["href", "type", "uuid", "name", "bandwidth", "bandwidthCommit", "state", "change", "locations", "routingProtocol", "billing", "account", "project", "order", "changeLog", "useCase"] diff --git a/equinix/services/fabricv4/models/port_sort_criteria.py b/equinix/services/fabricv4/models/port_sort_criteria.py index 9c0f7433..a2f24867 100644 --- a/equinix/services/fabricv4/models/port_sort_criteria.py +++ b/equinix/services/fabricv4/models/port_sort_criteria.py @@ -25,7 +25,7 @@ class PortSortCriteria(BaseModel): PortSortCriteria """ # noqa: E501 direction: Optional[PortSortDirection] = PortSortDirection.DESC - var_property: Optional[PortSortBy] = Field(default=PortSortBy.DEVICE_SLASH_NAME, alias="property") + var_property: Optional[PortSortBy] = Field(default=PortSortBy.SLASH_DEVICE_SLASH_NAME, alias="property") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["direction", "property"] @@ -88,7 +88,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "direction": obj.get("direction") if obj.get("direction") is not None else PortSortDirection.DESC, - "property": obj.get("property") if obj.get("property") is not None else PortSortBy.DEVICE_SLASH_NAME + "property": obj.get("property") if obj.get("property") is not None else PortSortBy.SLASH_DEVICE_SLASH_NAME }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/equinix/services/fabricv4/models/ra_attachment_sort_item.py b/equinix/services/fabricv4/models/ra_attachment_sort_item.py index dba0e34d..d080ca68 100644 --- a/equinix/services/fabricv4/models/ra_attachment_sort_item.py +++ b/equinix/services/fabricv4/models/ra_attachment_sort_item.py @@ -24,7 +24,7 @@ class RaAttachmentSortItem(BaseModel): """ RaAttachmentSortItem """ # noqa: E501 - var_property: Optional[RaAttachmentSortItemProperty] = Field(default=RaAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RaAttachmentSortItemProperty] = Field(default=RaAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") direction: Optional[RfAttachmentSortItemDirection] = RfAttachmentSortItemDirection.DESC additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["property", "direction"] @@ -87,7 +87,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "property": obj.get("property") if obj.get("property") is not None else RaAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "property": obj.get("property") if obj.get("property") is not None else RaAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, "direction": obj.get("direction") if obj.get("direction") is not None else RfAttachmentSortItemDirection.DESC }) # store additional fields in additional_properties diff --git a/equinix/services/fabricv4/models/rf_attachment_sort_item.py b/equinix/services/fabricv4/models/rf_attachment_sort_item.py index f7fd531b..639efade 100644 --- a/equinix/services/fabricv4/models/rf_attachment_sort_item.py +++ b/equinix/services/fabricv4/models/rf_attachment_sort_item.py @@ -24,7 +24,7 @@ class RfAttachmentSortItem(BaseModel): """ RfAttachmentSortItem """ # noqa: E501 - var_property: Optional[RfAttachmentSortItemProperty] = Field(default=RfAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RfAttachmentSortItemProperty] = Field(default=RfAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") direction: Optional[RfAttachmentSortItemDirection] = RfAttachmentSortItemDirection.DESC additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["property", "direction"] @@ -87,7 +87,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: return cls.model_validate(obj) _obj = cls.model_validate({ - "property": obj.get("property") if obj.get("property") is not None else RfAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "property": obj.get("property") if obj.get("property") is not None else RfAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, "direction": obj.get("direction") if obj.get("direction") is not None else RfAttachmentSortItemDirection.DESC }) # store additional fields in additional_properties diff --git a/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py b/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py index 87aa854c..ef2d153c 100644 --- a/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py +++ b/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py @@ -25,7 +25,7 @@ class RouteAggregationRuleSortCriteria(BaseModel): RouteAggregationRuleSortCriteria """ # noqa: E501 direction: Optional[RouteAggregationRuleSortDirection] = RouteAggregationRuleSortDirection.DESC - var_property: Optional[RouteAggregationRuleSortBy] = Field(default=RouteAggregationRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RouteAggregationRuleSortBy] = Field(default=RouteAggregationRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["direction", "property"] @@ -88,7 +88,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "direction": obj.get("direction") if obj.get("direction") is not None else RouteAggregationRuleSortDirection.DESC, - "property": obj.get("property") if obj.get("property") is not None else RouteAggregationRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME + "property": obj.get("property") if obj.get("property") is not None else RouteAggregationRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py b/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py index 03b5e79e..88660b3a 100644 --- a/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py +++ b/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py @@ -25,7 +25,7 @@ class RouteAggregationSortCriteria(BaseModel): RouteAggregationSortCriteria """ # noqa: E501 direction: Optional[RouteAggregationSortDirection] = RouteAggregationSortDirection.DESC - var_property: Optional[RouteAggregationSortBy] = Field(default=RouteAggregationSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RouteAggregationSortBy] = Field(default=RouteAggregationSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["direction", "property"] @@ -88,7 +88,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "direction": obj.get("direction") if obj.get("direction") is not None else RouteAggregationSortDirection.DESC, - "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME + "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py b/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py index e7068ee3..84b7da2f 100644 --- a/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py +++ b/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py @@ -25,7 +25,7 @@ class RouteFilterRuleSortCriteria(BaseModel): RouteFilterRuleSortCriteria """ # noqa: E501 direction: Optional[RouteFilterRuleSortDirection] = RouteFilterRuleSortDirection.DESC - var_property: Optional[RouteFilterRuleSortBy] = Field(default=RouteFilterRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RouteFilterRuleSortBy] = Field(default=RouteFilterRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["direction", "property"] @@ -88,7 +88,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "direction": obj.get("direction") if obj.get("direction") is not None else RouteFilterRuleSortDirection.DESC, - "property": obj.get("property") if obj.get("property") is not None else RouteFilterRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME + "property": obj.get("property") if obj.get("property") is not None else RouteFilterRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/equinix/services/fabricv4/models/route_filter_sort_criteria.py b/equinix/services/fabricv4/models/route_filter_sort_criteria.py index ee45fc32..d8331d53 100644 --- a/equinix/services/fabricv4/models/route_filter_sort_criteria.py +++ b/equinix/services/fabricv4/models/route_filter_sort_criteria.py @@ -25,7 +25,7 @@ class RouteFilterSortCriteria(BaseModel): RouteFilterSortCriteria """ # noqa: E501 direction: Optional[RouteFilterSortDirection] = RouteFilterSortDirection.DESC - var_property: Optional[RouteFilterSortBy] = Field(default=RouteFilterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + var_property: Optional[RouteFilterSortBy] = Field(default=RouteFilterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") additional_properties: Dict[str, Any] = {} __properties: ClassVar[List[str]] = ["direction", "property"] @@ -88,7 +88,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]: _obj = cls.model_validate({ "direction": obj.get("direction") if obj.get("direction") is not None else RouteFilterSortDirection.DESC, - "property": obj.get("property") if obj.get("property") is not None else RouteFilterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME + "property": obj.get("property") if obj.get("property") is not None else RouteFilterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME }) # store additional fields in additional_properties for _key in obj.keys(): diff --git a/patches/services/fabricv4/20260629_fix_sort_enum_default_leading_slash.patch b/patches/services/fabricv4/20260629_fix_sort_enum_default_leading_slash.patch new file mode 100644 index 00000000..2ba2aac9 --- /dev/null +++ b/patches/services/fabricv4/20260629_fix_sort_enum_default_leading_slash.patch @@ -0,0 +1,154 @@ +diff --git a/equinix/services/fabricv4/models/port_sort_criteria.py b/equinix/services/fabricv4/models/port_sort_criteria.py +index 9c0f743..a2f2486 100644 +--- a/equinix/services/fabricv4/models/port_sort_criteria.py ++++ b/equinix/services/fabricv4/models/port_sort_criteria.py +@@ -25,7 +25,7 @@ class PortSortCriteria(BaseModel): + PortSortCriteria + """ # noqa: E501 + direction: Optional[PortSortDirection] = PortSortDirection.DESC +- var_property: Optional[PortSortBy] = Field(default=PortSortBy.DEVICE_SLASH_NAME, alias="property") ++ var_property: Optional[PortSortBy] = Field(default=PortSortBy.SLASH_DEVICE_SLASH_NAME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class PortSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else PortSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else PortSortBy.DEVICE_SLASH_NAME ++ "property": obj.get("property") if obj.get("property") is not None else PortSortBy.SLASH_DEVICE_SLASH_NAME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/ra_attachment_sort_item.py b/equinix/services/fabricv4/models/ra_attachment_sort_item.py +index dba0e34..d080ca6 100644 +--- a/equinix/services/fabricv4/models/ra_attachment_sort_item.py ++++ b/equinix/services/fabricv4/models/ra_attachment_sort_item.py +@@ -24,7 +24,7 @@ class RaAttachmentSortItem(BaseModel): + """ + RaAttachmentSortItem + """ # noqa: E501 +- var_property: Optional[RaAttachmentSortItemProperty] = Field(default=RaAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RaAttachmentSortItemProperty] = Field(default=RaAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + direction: Optional[RfAttachmentSortItemDirection] = RfAttachmentSortItemDirection.DESC + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["property", "direction"] +@@ -87,7 +87,7 @@ class RaAttachmentSortItem(BaseModel): + return cls.model_validate(obj) + + _obj = cls.model_validate({ +- "property": obj.get("property") if obj.get("property") is not None else RaAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, ++ "property": obj.get("property") if obj.get("property") is not None else RaAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "direction": obj.get("direction") if obj.get("direction") is not None else RfAttachmentSortItemDirection.DESC + }) + # store additional fields in additional_properties +diff --git a/equinix/services/fabricv4/models/rf_attachment_sort_item.py b/equinix/services/fabricv4/models/rf_attachment_sort_item.py +index f7fd531..639efad 100644 +--- a/equinix/services/fabricv4/models/rf_attachment_sort_item.py ++++ b/equinix/services/fabricv4/models/rf_attachment_sort_item.py +@@ -24,7 +24,7 @@ class RfAttachmentSortItem(BaseModel): + """ + RfAttachmentSortItem + """ # noqa: E501 +- var_property: Optional[RfAttachmentSortItemProperty] = Field(default=RfAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RfAttachmentSortItemProperty] = Field(default=RfAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + direction: Optional[RfAttachmentSortItemDirection] = RfAttachmentSortItemDirection.DESC + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["property", "direction"] +@@ -87,7 +87,7 @@ class RfAttachmentSortItem(BaseModel): + return cls.model_validate(obj) + + _obj = cls.model_validate({ +- "property": obj.get("property") if obj.get("property") is not None else RfAttachmentSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, ++ "property": obj.get("property") if obj.get("property") is not None else RfAttachmentSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "direction": obj.get("direction") if obj.get("direction") is not None else RfAttachmentSortItemDirection.DESC + }) + # store additional fields in additional_properties +diff --git a/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py b/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py +index 87aa854..ef2d153 100644 +--- a/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py ++++ b/equinix/services/fabricv4/models/route_aggregation_rule_sort_criteria.py +@@ -25,7 +25,7 @@ class RouteAggregationRuleSortCriteria(BaseModel): + RouteAggregationRuleSortCriteria + """ # noqa: E501 + direction: Optional[RouteAggregationRuleSortDirection] = RouteAggregationRuleSortDirection.DESC +- var_property: Optional[RouteAggregationRuleSortBy] = Field(default=RouteAggregationRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteAggregationRuleSortBy] = Field(default=RouteAggregationRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class RouteAggregationRuleSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else RouteAggregationRuleSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else RouteAggregationRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else RouteAggregationRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py b/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py +index 03b5e79..88660b3 100644 +--- a/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py ++++ b/equinix/services/fabricv4/models/route_aggregation_sort_criteria.py +@@ -25,7 +25,7 @@ class RouteAggregationSortCriteria(BaseModel): + RouteAggregationSortCriteria + """ # noqa: E501 + direction: Optional[RouteAggregationSortDirection] = RouteAggregationSortDirection.DESC +- var_property: Optional[RouteAggregationSortBy] = Field(default=RouteAggregationSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteAggregationSortBy] = Field(default=RouteAggregationSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class RouteAggregationSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else RouteAggregationSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py b/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py +index e7068ee..84b7da2 100644 +--- a/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py ++++ b/equinix/services/fabricv4/models/route_filter_rule_sort_criteria.py +@@ -25,7 +25,7 @@ class RouteFilterRuleSortCriteria(BaseModel): + RouteFilterRuleSortCriteria + """ # noqa: E501 + direction: Optional[RouteFilterRuleSortDirection] = RouteFilterRuleSortDirection.DESC +- var_property: Optional[RouteFilterRuleSortBy] = Field(default=RouteFilterRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteFilterRuleSortBy] = Field(default=RouteFilterRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class RouteFilterRuleSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else RouteFilterRuleSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else RouteFilterRuleSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else RouteFilterRuleSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/route_filter_sort_criteria.py b/equinix/services/fabricv4/models/route_filter_sort_criteria.py +index ee45fc3..d8331d5 100644 +--- a/equinix/services/fabricv4/models/route_filter_sort_criteria.py ++++ b/equinix/services/fabricv4/models/route_filter_sort_criteria.py +@@ -25,7 +25,7 @@ class RouteFilterSortCriteria(BaseModel): + RouteFilterSortCriteria + """ # noqa: E501 + direction: Optional[RouteFilterSortDirection] = RouteFilterSortDirection.DESC +- var_property: Optional[RouteFilterSortBy] = Field(default=RouteFilterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteFilterSortBy] = Field(default=RouteFilterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class RouteFilterSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else RouteFilterSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else RouteFilterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else RouteFilterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): diff --git a/patches/services/fabricv4/20260630_fix_create_eia_service_201_response.patch b/patches/services/fabricv4/20260630_fix_create_eia_service_201_response.patch new file mode 100644 index 00000000..fde614ba --- /dev/null +++ b/patches/services/fabricv4/20260630_fix_create_eia_service_201_response.patch @@ -0,0 +1,55 @@ +diff --git a/equinix/services/fabricv4/api/internet_access_services_api.py b/equinix/services/fabricv4/api/internet_access_services_api.py +index 50870d4..af74399 100644 +--- a/equinix/services/fabricv4/api/internet_access_services_api.py ++++ b/equinix/services/fabricv4/api/internet_access_services_api.py +@@ -94,6 +94,7 @@ class InternetAccessServicesApi: + + _response_types_map: Dict[str, Optional[str]] = { + '202': "InternetAccessService", ++ '201': "InternetAccessService", + '400': "List[Error]", + '403': "List[Error]", + '500': "List[Error]", +@@ -164,6 +165,7 @@ class InternetAccessServicesApi: + + _response_types_map: Dict[str, Optional[str]] = { + '202': "InternetAccessService", ++ '201': "InternetAccessService", + '400': "List[Error]", + '403': "List[Error]", + '500': "List[Error]", +@@ -234,6 +236,7 @@ class InternetAccessServicesApi: + + _response_types_map: Dict[str, Optional[str]] = { + '202': "InternetAccessService", ++ '201': "InternetAccessService", + '400': "List[Error]", + '403': "List[Error]", + '500': "List[Error]", +@@ -376,7 +379,7 @@ class InternetAccessServicesApi: + ) + + _response_types_map: Dict[str, Optional[str]] = { +- '202': "InternetAccessService", ++ '202': None, + '404': "List[Error]", + '409': "List[Error]", + } +@@ -445,7 +448,7 @@ class InternetAccessServicesApi: + ) + + _response_types_map: Dict[str, Optional[str]] = { +- '202': "InternetAccessService", ++ '202': None, + '404': "List[Error]", + '409': "List[Error]", + } +@@ -514,7 +517,7 @@ class InternetAccessServicesApi: + ) + + _response_types_map: Dict[str, Optional[str]] = { +- '202': "InternetAccessService", ++ '202': None, + '404': "List[Error]", + '409': "List[Error]", + } diff --git a/patches/services/fabricv4/20260630_fix_internet_access_service_optional_fields.patch b/patches/services/fabricv4/20260630_fix_internet_access_service_optional_fields.patch new file mode 100644 index 00000000..4449ac5f --- /dev/null +++ b/patches/services/fabricv4/20260630_fix_internet_access_service_optional_fields.patch @@ -0,0 +1,28 @@ +diff --git a/equinix/services/fabricv4/models/internet_access_service.py b/equinix/services/fabricv4/models/internet_access_service.py +index ca936ea..55496c9 100644 +--- a/equinix/services/fabricv4/models/internet_access_service.py ++++ b/equinix/services/fabricv4/models/internet_access_service.py +@@ -40,15 +40,15 @@ class InternetAccessService(BaseModel): + bandwidth: Optional[StrictInt] = Field(default=None, description="Bandwidth of the service") + bandwidth_commit: Optional[StrictInt] = Field(default=None, description="Minimum bandwidth commit for burst billing variant of the service", alias="bandwidthCommit") + state: InternetAccessServiceState +- change: InternetAccessChange ++ change: Optional[InternetAccessChange] = None + locations: Optional[List[InternetAccessLocation]] = Field(default=None, description="List of locations associated with the service") +- routing_protocol: InternetAccessRoutingProtocol = Field(alias="routingProtocol") +- billing: InternetAccessBilling +- account: InternetAccessAccount +- project: Project +- order: InternetAccessOrder +- change_log: Changelog = Field(alias="changeLog") +- use_case: InternetAccessUseCase = Field(alias="useCase") ++ routing_protocol: Optional[InternetAccessRoutingProtocol] = Field(default=None, alias="routingProtocol") ++ billing: Optional[InternetAccessBilling] = None ++ account: Optional[InternetAccessAccount] = None ++ project: Optional[Project] = None ++ order: Optional[InternetAccessOrder] = None ++ change_log: Optional[Changelog] = Field(default=None, alias="changeLog") ++ use_case: Optional[InternetAccessUseCase] = Field(default=None, alias="useCase") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["href", "type", "uuid", "name", "bandwidth", "bandwidthCommit", "state", "change", "locations", "routingProtocol", "billing", "account", "project", "order", "changeLog", "useCase"] + diff --git a/patches/services/fabricv4/20260630_fix_peering_ipv4_allow_cidr_suffix.patch b/patches/services/fabricv4/20260630_fix_peering_ipv4_allow_cidr_suffix.patch new file mode 100644 index 00000000..2de8359c --- /dev/null +++ b/patches/services/fabricv4/20260630_fix_peering_ipv4_allow_cidr_suffix.patch @@ -0,0 +1,48 @@ +diff --git a/equinix/services/fabricv4/models/internet_access_peering_ipv4.py b/equinix/services/fabricv4/models/internet_access_peering_ipv4.py +index 14c3d6e..6b33a7f 100644 +--- a/equinix/services/fabricv4/models/internet_access_peering_ipv4.py ++++ b/equinix/services/fabricv4/models/internet_access_peering_ipv4.py +@@ -48,8 +48,8 @@ class InternetAccessPeeringIpv4(BaseModel): + if value is None: + return value + +- if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): +- raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") ++ if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): ++ raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") + return value + + @field_validator('customer_peer_ip') +@@ -58,8 +58,8 @@ class InternetAccessPeeringIpv4(BaseModel): + if value is None: + return value + +- if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): +- raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") ++ if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): ++ raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") + return value + + @field_validator('equinix_vrrp_ip') +@@ -68,8 +68,8 @@ class InternetAccessPeeringIpv4(BaseModel): + if value is None: + return value + +- if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): +- raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") ++ if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): ++ raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") + return value + + @field_validator('customer_vrrp_ip') +@@ -78,8 +78,8 @@ class InternetAccessPeeringIpv4(BaseModel): + if value is None: + return value + +- if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", value): +- raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/") ++ if not re.match(r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$", value): ++ raise ValueError(r"must validate the regular expression /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?:/\d{1,2})?$/") + return value + + model_config = ConfigDict( diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..cfefca09 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,20 @@ +[pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* + +# Markers. `integration` tests hit the live Equinix Fabric API and are NOT run +# on a normal `pytest` invocation / build — they are opt-in. +markers = + integration: live API tests against Equinix Fabric (require credentials; run on demand) + +# Skip integration tests by default. A command-line `-m` overrides this, so +# `pytest -m integration` (or ./run_fabric_tests.sh) runs them on demand. +addopts = -m "not integration" + +# Capture request/response logs (logger "fabric.http") into the reports. +log_level = INFO +log_format = %(asctime)s %(name)s %(levelname)s %(message)s +junit_logging = all +junit_log_passing_tests = true diff --git a/run_fabric_tests.sh b/run_fabric_tests.sh new file mode 100755 index 00000000..ca65b5c8 --- /dev/null +++ b/run_fabric_tests.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Run the Fabric integration tests ON DEMAND (they are excluded from a normal +# `pytest` / build run by pytest.ini). +# +# Usage: +# ./run_fabric_tests.sh # run all integration tests +# ./run_fabric_tests.sh -k test1 # pass extra args through to pytest +# ./run_fabric_tests.sh tests/services/fabricv4/test_internet_access_api.py +# +# Test data can come from EITHER an env var OR an env.json file: +# TEST_DATA_UAT_USERS JSON document with the per-user credentials, or +# env.json the same JSON written to a file (repo root). May also +# carry an "envUrl" key. +# Base URL: +# ENV_URL base URL of the target environment +# (e.g. https://uatapi.equinix.com), or an "envUrl" +# key inside env.json. +# +set -euo pipefail + +cd "$(dirname "$0")" + +if [[ -z "${TEST_DATA_UAT_USERS:-}" && ! -f env.json ]]; then + echo "error: no test data. Set TEST_DATA_UAT_USERS or create env.json in the repo root" >&2 + exit 1 +fi +if [[ -z "${ENV_URL:-}" ]] && ! grep -q '"envUrl"' env.json 2>/dev/null; then + echo "error: no base URL. Set ENV_URL or add an \"envUrl\" key to env.json" >&2 + exit 1 +fi + +# Activate the project venv if present and not already active. +if [[ -z "${VIRTUAL_ENV:-}" && -f .venv/bin/activate ]]; then + # shellcheck disable=SC1091 + source .venv/bin/activate +fi + +# With FABRIC_DEBUG set, disable output capture so request logs stream live. +EXTRA=() +if [[ -n "${FABRIC_DEBUG:-}" && "${FABRIC_DEBUG}" != "0" ]]; then + EXTRA+=(-s) +fi + +# Write JUnit XML + HTML + JSON reports into ./reports/ (override dir with REPORTS_DIR). +REPORTS_DIR="${REPORTS_DIR:-reports}" +mkdir -p "$REPORTS_DIR" +EXTRA+=( + "--junitxml=${REPORTS_DIR}/fabric-tests.xml" + "--html=${REPORTS_DIR}/fabric-tests.html" "--self-contained-html" + "--json-report" "--json-report-file=${REPORTS_DIR}/fabric-tests.json" +) + +# `-m integration` overrides the default `-m "not integration"` from pytest.ini. +# ${EXTRA[@]+...} guards against "unbound variable" on bash 3.2 (macOS) under set -u. +exec python -m pytest -m integration -v ${EXTRA[@]+"${EXTRA[@]}"} "$@" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..416ada56 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,44 @@ +import logging +import os + +import pytest + +from services.fabricv4.helpers import utils + +_DEBUG_OFF = {"", "0", "false", "off", "no"} + + +def pytest_configure(config): + # Always emit request/response logs at INFO so pytest captures them into the + # reports (junit/html), regardless of FABRIC_DEBUG. + logging.getLogger("fabric.http").setLevel(logging.INFO) + + mode = os.getenv("FABRIC_DEBUG", "").strip().lower() + if mode in _DEBUG_OFF: + return + # FABRIC_DEBUG only adds LIVE console output (and urllib3 / header dumps). + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter("%(asctime)s %(name)s %(levelname)s %(message)s")) + for name in ("fabric.http", "urllib3"): + log = logging.getLogger(name) + log.setLevel(logging.DEBUG) + log.addHandler(handler) + if mode in ("body", "full", "2"): + import http.client as http_client + http_client.HTTPConnection.debuglevel = 1 + + +def pytest_collection_modifyitems(config, items): + missing = [] + if not utils.test_data_available(): + missing.append("test data (TEST_DATA_UAT_USERS or env.json)") + if not utils.env_url_available(): + missing.append("base URL (ENV_URL or envUrl in the data file)") + if not missing: + return + skip_integration = pytest.mark.skip( + reason=f"integration prerequisites missing: {', '.join(missing)}" + ) + for item in items: + if "integration" in item.keywords: + item.add_marker(skip_integration) diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..fde07b53 --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,6 @@ +# Test-only dependencies (not required to use the SDK). +pytest>=7.0 +requests>=2.28 +# Reports (generated by run_fabric_tests.sh into ./reports/). +pytest-html>=4.0 +pytest-json-report>=1.5 diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/fabricv4/__init__.py b/tests/services/fabricv4/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/fabricv4/helpers/__init__.py b/tests/services/fabricv4/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/services/fabricv4/helpers/apis.py b/tests/services/fabricv4/helpers/apis.py new file mode 100644 index 00000000..796512a5 --- /dev/null +++ b/tests/services/fabricv4/helpers/apis.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from equinix.services import fabricv4 + +from . import token_generator +from .users import UserName + + +class Apis: + def __init__(self, api_client: fabricv4.ApiClient) -> None: + self.api_client = api_client + self.cloud_routers = fabricv4.CloudRoutersApi(api_client) + self.connections = fabricv4.ConnectionsApi(api_client) + self.health = fabricv4.HealthApi(api_client) + self.metros = fabricv4.MetrosApi(api_client) + self.networks = fabricv4.NetworksApi(api_client) + self.ports = fabricv4.PortsApi(api_client) + self.precision_time = fabricv4.PrecisionTimeApi(api_client) + self.prices = fabricv4.PricesApi(api_client) + self.route_filters = fabricv4.RouteFiltersApi(api_client) + self.route_filter_rules = fabricv4.RouteFilterRulesApi(api_client) + self.routing_protocols = fabricv4.RoutingProtocolsApi(api_client) + self.service_profiles = fabricv4.ServiceProfilesApi(api_client) + self.service_tokens = fabricv4.ServiceTokensApi(api_client) + self.statistics = fabricv4.StatisticsApi(api_client) + self.internet_access_services = fabricv4.InternetAccessServicesApi(api_client) + self.ip_blocks = fabricv4.IpBlocksApi(api_client) + + +def for_user(user_name: UserName) -> Apis: + return Apis(token_generator.get_api_client(user_name)) diff --git a/tests/services/fabricv4/helpers/http_logging.py b/tests/services/fabricv4/helpers/http_logging.py new file mode 100644 index 00000000..7f7fd8bb --- /dev/null +++ b/tests/services/fabricv4/helpers/http_logging.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import logging +import os + +logger = logging.getLogger("fabric.http") + + +def _max_len() -> int: + try: + return int(os.getenv("FABRIC_LOG_MAXLEN", "2000")) + except ValueError: + return 2000 + + +def _to_text(value) -> str: + if value is None: + return "" + if isinstance(value, (bytes, bytearray)): + return value.decode("utf-8", "replace") + return str(value) + + +def _truncate(text: str) -> str: + limit = _max_len() + if limit and len(text) > limit: + return f"{text[:limit]}... [{len(text)} chars total]" + return text + + +def enable(api_client) -> None: + rest_client = api_client.rest_client + if getattr(rest_client, "_fabric_logging_wrapped", False): + return + original = rest_client.request + + def logged_request(method, url, headers=None, body=None, + post_params=None, _request_timeout=None): + logger.info("--> %s %s", method, url) + if body is not None: + logger.info(" request body: %s", _truncate(_to_text(body))) + if post_params: + logger.info(" form params: %s", _truncate(_to_text(post_params))) + response = original( + method, url, headers=headers, body=body, + post_params=post_params, _request_timeout=_request_timeout, + ) + try: + text = _to_text(response.read()) + except Exception: # noqa: BLE001 - logging must never break the request + text = "" + logger.info("<-- %s %s -> HTTP %s", method, url, response.status) + logger.info(" response body: %s", _truncate(text)) + return response + + rest_client.request = logged_request + rest_client._fabric_logging_wrapped = True diff --git a/tests/services/fabricv4/helpers/token_generator.py b/tests/services/fabricv4/helpers/token_generator.py new file mode 100644 index 00000000..406f2d67 --- /dev/null +++ b/tests/services/fabricv4/helpers/token_generator.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import Dict + +import requests + +# Import from submodules: the fabricv4 package root re-exports a model named +# Configuration that shadows the HTTP-client Configuration. +from equinix.services.fabricv4.api_client import ApiClient +from equinix.services.fabricv4.configuration import Configuration + +from . import http_logging, utils +from .users import UserName + +_clients: Dict[UserName, ApiClient] = {} + + +def _request_access_token(base_url: str, client_id: str, client_secret: str) -> str: + response = requests.post( + f"{base_url}/oauth2/v1/token", + json={ + "client_id": client_id, + "client_secret": client_secret, + "grant_type": "client_credentials", + }, + headers={"Content-Type": "application/json"}, + timeout=60, + ) + response.raise_for_status() + return response.json()["access_token"] + + +def generate(user_name: UserName) -> ApiClient: + base_url = utils.env_url() + user = utils.get_user_data(user_name) + + access_token = _request_access_token(base_url, user.client_id, user.client_secret) + + configuration = Configuration(host=base_url, access_token=access_token) + api_client = ApiClient(configuration) + http_logging.enable(api_client) + + _clients[user_name] = api_client + return api_client + + +def get_api_client(user_name: UserName) -> ApiClient: + if user_name in _clients: + return _clients[user_name] + return generate(user_name) diff --git a/tests/services/fabricv4/helpers/users.py b/tests/services/fabricv4/helpers/users.py new file mode 100644 index 00000000..e2c649e3 --- /dev/null +++ b/tests/services/fabricv4/helpers/users.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Dict, List, Optional + + +class UserName(str, Enum): + PANTHERS_FCR = "fcr" + PANTHERS_FNV = "fnv" + + +@dataclass +class Resources: + ports: List[str] = field(default_factory=list) + connections: List[str] = field(default_factory=list) + raw: Dict[str, Any] = field(default_factory=dict) + + @classmethod + def from_json(cls, data: Optional[Dict[str, Any]]) -> "Resources": + data = data or {} + return cls( + ports=list(data.get("ports") or []), + connections=list(data.get("connections") or []), + raw=data, + ) + + def get(self, name: str) -> list: + return list(self.raw.get(name) or []) + + +@dataclass +class UserData: + name: str + client_id: str + client_secret: str + project_id: Optional[str] = None + account_number: Optional[str] = None + account_number_eia: Optional[str] = None + ia_profile_uuid: Optional[str] = None + resources: Resources = field(default_factory=Resources) + + @classmethod + def from_json(cls, data: Dict[str, Any], name: Optional[str] = None) -> "UserData": + def pick(*keys, default=None): + for key in keys: + if key in data: + return data[key] + return default + + return cls( + name=name or pick("name"), + client_id=pick("client_id", "clientId"), + client_secret=pick("client_secret", "clientSecret"), + project_id=pick("projectId", "project_id"), + account_number=pick("accountNumber", "account_number"), + account_number_eia=pick("accountNumberEIA", "account_number_eia"), + ia_profile_uuid=pick("iaProfileUuid", "ia_profile_uuid"), + resources=Resources.from_json(pick("resources")), + ) diff --git a/tests/services/fabricv4/helpers/utils.py b/tests/services/fabricv4/helpers/utils.py new file mode 100644 index 00000000..c37f7db9 --- /dev/null +++ b/tests/services/fabricv4/helpers/utils.py @@ -0,0 +1,113 @@ +from __future__ import annotations + +import json +import os +import random +from functools import lru_cache +from pathlib import Path +from typing import Dict, Optional + +from .users import UserData, UserName + +TEST_DATA_ENV_VAR = "TEST_DATA_UAT_USERS" +TEST_DATA_FILE_VAR = "TEST_DATA_FILE" +ENV_URL_VAR = "ENV_URL" +DEFAULT_DATA_FILES = ("env.json", "file.json") + +_REPO_ROOT = Path(__file__).resolve().parents[4] + + +def data_file_path() -> Optional[Path]: + explicit = os.getenv(TEST_DATA_FILE_VAR) + if explicit: + path = Path(explicit) + return path if path.is_file() else None + for base in (Path.cwd(), _REPO_ROOT): + for name in DEFAULT_DATA_FILES: + candidate = base / name + if candidate.is_file(): + return candidate + return None + + +@lru_cache(maxsize=1) +def _load_document() -> dict: + raw = os.getenv(TEST_DATA_ENV_VAR) + if raw: + return json.loads(raw) + path = data_file_path() + if path is not None: + return json.loads(path.read_text()) + raise RuntimeError( + f"No test data found. Set {TEST_DATA_ENV_VAR}, or provide a JSON file " + f"(one of {DEFAULT_DATA_FILES} in the repo root, or {TEST_DATA_FILE_VAR})." + ) + + +def test_data_available() -> bool: + return bool(os.getenv(TEST_DATA_ENV_VAR)) or data_file_path() is not None + + +def env_url() -> str: + url = os.getenv(ENV_URL_VAR) + if not url and test_data_available(): + doc = _load_document() + url = doc.get("envUrl") or doc.get("env_url") or doc.get(ENV_URL_VAR) + if not url: + raise RuntimeError( + f"No base URL set. Provide {ENV_URL_VAR} (e.g. https://api.equinix.com) " + f"or an 'envUrl' key in the test-data document." + ) + return url.rstrip("/") + + +def env_url_available() -> bool: + try: + return bool(env_url()) + except RuntimeError: + return False + + +_META_KEYS = {ENV_URL_VAR, "envUrl", "env_url"} + + +def _as_dict(value): + if isinstance(value, str): + value = json.loads(value) + return value + + +@lru_cache(maxsize=1) +def _load_users() -> Dict[str, UserData]: + document = _load_document() + + if isinstance(document, list) or "users" in document: + users = document.get("users", document) if isinstance(document, dict) else document + users = _as_dict(users) + if isinstance(users, dict): + users = users.get("users", []) + return {u["name"]: UserData.from_json(u) for u in users} + + result: Dict[str, UserData] = {} + for name, value in document.items(): + if name in _META_KEYS: + continue + entry = _as_dict(value) + if isinstance(entry, dict): + result[name] = UserData.from_json(entry, name=name) + return result + + +def get_user_data(user_name: UserName) -> UserData: + users = _load_users() + try: + return users[user_name.value] + except KeyError as exc: + raise LookupError( + f"User '{user_name.value}' not found in test data " + f"(available: {sorted(users)})" + ) from exc + + +def get_random_vlan_number() -> int: + return random.randint(1, 3999) diff --git a/tests/services/fabricv4/test_internet_access_api.py b/tests/services/fabricv4/test_internet_access_api.py new file mode 100644 index 00000000..51363c5b --- /dev/null +++ b/tests/services/fabricv4/test_internet_access_api.py @@ -0,0 +1,436 @@ +from __future__ import annotations + +import time + +import pytest + +from equinix.services import fabricv4 +from equinix.services.fabricv4.exceptions import ApiException +from .helpers import utils +from .helpers.apis import Apis, for_user +from .helpers.users import UserName + +USER_NAME = UserName.PANTHERS_FNV +IA_METRO = "SV" +DELETE_RETRY_INTERVAL_S = 10 +DELETE_MAX_ATTEMPTS = 6 +STATE_RETRY_INTERVAL_S = 5 +STATE_MAX_ATTEMPTS = 5 +CONNECTION_RETRY_INTERVAL_S = 20 +CONNECTION_MAX_ATTEMPTS = 15 +CONNECTION_CREATE_MAX_ATTEMPTS = 5 +DUPLICATE_VLAN_ERROR_CODE = "EQ-3142570" +CONNECTION_ALREADY_DELETED_ERROR_CODE = "EQ-3142509" +INITIAL_BANDWIDTH = 50 +BANDWIDTH_CANDIDATES = (100, 200, 500, 1000) + + +def _value(enum_or_str): + return getattr(enum_or_str, "value", enum_or_str) + + +@pytest.mark.integration +class TestInternetAccessApi: + apis: Apis + test_data = None + created_service_uuid = None + created_ip_block_uuid = None + created_connection_uuids: list = [] + + @classmethod + def setup_class(cls) -> None: + cls.test_data = utils.get_user_data(USER_NAME) + cls.apis = for_user(USER_NAME) + cls.created_service_uuid = None + cls.created_ip_block_uuid = None + cls.created_connection_uuids = [] + + @classmethod + def teardown_class(cls) -> None: + service_deleted = cls._wait_for_service_deleted() + cls._delete_created_connections() + cls._delete_created_ip_block() + assert service_deleted, ( + f"Internet Access Service {cls.created_service_uuid} was not deleted" + ) + + def test_1_create_eia_service(self) -> None: + service = self._create_internet_access_service() + type(self).created_service_uuid = service.uuid + assert self.created_service_uuid is not None, ( + "Created Internet Access Service has no UUID" + ) + + def test_2_get_eia_service(self) -> None: + assert self.created_service_uuid is not None, "No Internet Access Service was created" + response = self.apis.internet_access_services.get_eia_service_with_http_info( + self.created_service_uuid + ) + assert response.status_code == 200 + assert response.data.uuid == self.created_service_uuid + + def test_3_search_eia_services(self) -> None: + assert self.created_service_uuid is not None, "No Internet Access Service was created" + response = self._search_services() + services = response.data + assert response.status_code == 200 + assert services.data, "Search returned no services" + assert all( + s.state == fabricv4.InternetAccessServiceState.PROVISIONED for s in services.data + ), "Search returned services that are not PROVISIONED" + found = any(s.uuid == self.created_service_uuid for s in services.data) + assert found, ( + f"Created Internet Access Service {self.created_service_uuid} " + f"not found in search results" + ) + + def test_4_patch_eia_service(self) -> None: + assert self.created_service_uuid is not None, "No Internet Access Service was created" + + for bandwidth in BANDWIDTH_CANDIDATES: + operation = fabricv4.InternetAccessPatchOperationUpdate( + op=fabricv4.InternetAccessPatchOperationUpdateAllowedOp.REPLACE, + path="/bandwidth", + value=bandwidth, + ) + try: + response = self.apis.internet_access_services.patch_eia_service_with_http_info( + self.created_service_uuid, [operation] + ) + assert response.status_code == 202 + return + except ApiException as e: + body = str(e.body) + if "EQ-7100076" in body: + continue + if "EQ-7100077" in body: + time.sleep(STATE_RETRY_INTERVAL_S) + continue + raise + pytest.fail( + f"Could not patch bandwidth to a new value for service {self.created_service_uuid}" + ) + + def test_5_delete_eia_service(self) -> None: + assert self.created_service_uuid is not None, "No Internet Access Service was created" + assert self._wait_for_service_is_in_state( + self.created_service_uuid, fabricv4.InternetAccessServiceState.PROVISIONED + ), "Internet Access Service was not PROVISIONED before delete" + assert self._delete_eia_service(self.created_service_uuid), ( + "Internet Access Service was not deleted in time" + ) + + @classmethod + def _wait_for_service_deleted(cls) -> bool: + if cls.created_service_uuid is None: + return True + for attempt in range(1, DELETE_MAX_ATTEMPTS + 1): + try: + state = cls.apis.internet_access_services.get_eia_service( + cls.created_service_uuid + ).state + if state == fabricv4.InternetAccessServiceState.DEPROVISIONED: + return True + except ApiException as e: + if e.status == 404: + return True + print(f"Service delete check attempt {attempt} " + f"for {cls.created_service_uuid}: {e}") + time.sleep(DELETE_RETRY_INTERVAL_S) + print(f"Internet Access Service {cls.created_service_uuid} " + f"was not deprovisioned within the timeout") + return False + + @classmethod + def _delete_created_ip_block(cls) -> None: + if cls.created_ip_block_uuid is None: + return + for attempt in range(1, DELETE_MAX_ATTEMPTS + 1): + try: + response = cls.apis.ip_blocks.delete_ip_block_by_id_with_http_info( + cls.created_ip_block_uuid + ) + if response.status_code // 100 == 2: + return + except ApiException as e: + print(f"Ip block delete attempt {attempt} " + f"for {cls.created_ip_block_uuid} not ready: {e}") + time.sleep(DELETE_RETRY_INTERVAL_S) + print(f"Ip block {cls.created_ip_block_uuid} could not be deleted within the timeout") + + @classmethod + def _delete_created_connections(cls) -> None: + for connection_uuid in list(cls.created_connection_uuids): + deleted = False + for attempt in range(1, DELETE_MAX_ATTEMPTS + 1): + try: + response = cls.apis.connections.delete_connection_by_uuid_with_http_info( + connection_uuid + ) + if response.status_code // 100 == 2: + deleted = True + break + except ApiException as e: + if e.status == 404 or CONNECTION_ALREADY_DELETED_ERROR_CODE in str(e.body): + deleted = True + break + print(f"Connection delete attempt {attempt} " + f"for {connection_uuid} not ready: {e}") + time.sleep(DELETE_RETRY_INTERVAL_S) + if not deleted: + print(f"Connection {connection_uuid} could not be deleted within the timeout") + + def _search_services(self): + request_body = fabricv4.InternetAccessSearchRequest( + filter=fabricv4.SearchExpression( + var_and=[ + fabricv4.SearchExpression( + var_property="/project/projectId", + operator="=", + values=[self.test_data.project_id], + ), + fabricv4.SearchExpression( + var_property="/state", + operator="=", + values=[ + _value(fabricv4.InternetAccessServiceState.PROVISIONED) + ], + ), + ] + ), + pagination=fabricv4.PaginationRequest(offset=0, limit=100), + ) + return self.apis.internet_access_services.search_eia_services_with_http_info(request_body) + + @classmethod + def _wait_for_service_is_in_state(cls, uuid, expected_state) -> bool: + current_state = None + for _ in range(STATE_MAX_ATTEMPTS): + try: + service = cls.apis.internet_access_services.get_eia_service(uuid) + current_state = service.state + except ApiException as e: + print(f"Service state check for {uuid} not ready: {e}") + time.sleep(STATE_RETRY_INTERVAL_S) + continue + if current_state == expected_state: + return True + time.sleep(STATE_RETRY_INTERVAL_S) + print(f"Internet Access Service {uuid} did not reach {expected_state} state " + f"(current: {current_state})") + return False + + def _create_ia_connection(self) -> str: + port = self._select_dot1q_port() + + connection = None + for _ in range(CONNECTION_CREATE_MAX_ATTEMPTS): + connection_post_request = fabricv4.ConnectionPostRequest( + name=f"panthers-eia-con-{utils.get_random_vlan_number()}", + type=fabricv4.ConnectionType.IA_VC, + bandwidth=INITIAL_BANDWIDTH, + project=fabricv4.Project(project_id=self.test_data.project_id), + notifications=[ + fabricv4.SimplifiedNotification( + type="ALL", emails=["panthers_auto@equinix.com"] + ) + ], + a_side=fabricv4.ConnectionSide( + access_point=fabricv4.AccessPoint( + type=fabricv4.AccessPointType.COLO, + port=fabricv4.SimplifiedPort(uuid=port.uuid), + link_protocol=fabricv4.SimplifiedLinkProtocol( + type=fabricv4.LinkProtocolType.DOT1Q, + vlan_tag=utils.get_random_vlan_number(), + ), + ) + ), + z_side=fabricv4.ConnectionSide( + access_point=fabricv4.AccessPoint( + type=fabricv4.AccessPointType.SP, + profile=fabricv4.SimplifiedServiceProfile( + type=fabricv4.ServiceProfileTypeEnum.IA_PROFILE, + uuid=self.test_data.ia_profile_uuid, + ), + location=fabricv4.SimplifiedLocation(metro_code=IA_METRO), + ) + ), + ) + try: + response = self.apis.connections.create_connection_with_http_info( + connection_post_request, dry_run=False + ) + except ApiException as e: + # Duplicate VLAN on the port — pick a new VLAN and retry. + if e.status == 400 and DUPLICATE_VLAN_ERROR_CODE in str(e.body): + print(f"Connection create hit duplicate VLAN, retrying: {e.body}") + continue + raise + if response.status_code == 201: + connection = response.data + break + + assert connection is not None, ( + "Could not create IA connection (VLAN conflicts or non-201 response)" + ) + type(self).created_connection_uuids.append(connection.uuid) + assert self._wait_for_connection_is_in_state( + connection.uuid, fabricv4.EquinixStatus.PROVISIONED + ), f"Connection {connection.uuid} did not reach PROVISIONED state" + return connection.uuid + + def _wait_for_connection_is_in_state(self, connection_uuid, *connection_state) -> bool: + result = False + current_state = None + for _ in range(CONNECTION_MAX_ATTEMPTS): + try: + connection = self.apis.connections.get_connection_by_uuid(connection_uuid) + current_state = connection.operation.equinix_status + except ApiException as e: + print(f"Connection state check for {connection_uuid} not ready: {e}") + time.sleep(CONNECTION_RETRY_INTERVAL_S) + continue + if current_state in connection_state: + result = True + break + time.sleep(CONNECTION_RETRY_INTERVAL_S) + if not result: + print(f"Connection has not reached the expected state: " + f"{_value(connection_state[0])} current state: {_value(current_state)}") + return result + + def _select_dot1q_port(self): + ports = [ + p for p in self._get_ports().data + if p.location is not None and p.location.metro_code == IA_METRO + and p.encapsulation is not None and _value(p.encapsulation.type) == "DOT1Q" + ] + assert ports, f"No DOT1Q port available in metro {IA_METRO}" + return ports[0] + + def _get_ports(self): + port_search_request = fabricv4.PortV4SearchRequest( + filter=fabricv4.PortExpression( + var_or=[ + fabricv4.PortExpression( + var_and=[ + fabricv4.PortExpression( + operator="=", + var_property=fabricv4.PortSearchFieldName.SLASH_STATE, + values=["ACTIVE"], + ), + fabricv4.PortExpression( + operator="=", + var_property=( + fabricv4.PortSearchFieldName.SLASH_PROJECT_SLASH_PROJECT_ID + ), + values=[self.test_data.project_id], + ), + fabricv4.PortExpression( + operator="=", + var_property=( + fabricv4.PortSearchFieldName.SLASH_LOCATION_SLASH_METRO_CODE + ), + values=[IA_METRO], + ), + ] + ) + ] + ), + pagination=fabricv4.PaginationRequest(offset=0, limit=100), + sort=[ + fabricv4.PortSortCriteria( + var_property=fabricv4.PortSortBy.SLASH_DEVICE_SLASH_NAME, + direction=fabricv4.PortSortDirection.DESC, + ) + ], + ) + return self.apis.ports.search_ports(port_search_request) + + def _create_customer_ipv4_block(self): + prefix = f"67.223.{1 + (utils.get_random_vlan_number() % 254)}.0/24" + request_body = fabricv4.SubmitIpBlockRequestBody( + type=fabricv4.TypeOfIpBlockProduct.IPV4_IP_BLOCK, + prefix=prefix, + project=fabricv4.IpBlockProjectRequest(project_id=self.test_data.project_id), + ) + response = self.apis.ip_blocks.submit_ip_block_with_http_info(request_body) + assert response.status_code == 202 + ip_block = response.data + type(self).created_ip_block_uuid = ip_block.uuid + return ip_block + + @staticmethod + def _equinix_peer_ip_for(ip_block) -> str: + octets = ip_block.prefix.split("/")[0].split(".") + return f"{octets[0]}.{octets[1]}.{octets[2]}.5" + + @classmethod + def _delete_eia_service(cls, uuid) -> bool: + for attempt in range(1, DELETE_MAX_ATTEMPTS + 1): + try: + state = cls.apis.internet_access_services.get_eia_service(uuid).state + if state in ( + fabricv4.InternetAccessServiceState.DEPROVISIONED, + fabricv4.InternetAccessServiceState.DEPROVISIONING, + ): + return True + response = ( + cls.apis.internet_access_services.delete_eia_service_with_http_info(uuid) + ) + if response.status_code // 100 == 2: + return True + except ApiException as e: + print(f"Delete attempt {attempt} for {uuid} not ready: {e}") + time.sleep(DELETE_RETRY_INTERVAL_S) + print(f"Internet Access Service {uuid} could not be deleted within the timeout") + return False + + def _create_internet_access_service(self): + connection_uuid = self._create_ia_connection() + ip_block = self._create_customer_ipv4_block() + equinix_peer_ip = self._equinix_peer_ip_for(ip_block) + + routing_protocol = fabricv4.InternetAccessRoutingProtocolDirectRequest( + type=fabricv4.InternetAccessRoutingProtocolType.DIRECT, + connections=[ + fabricv4.InternetAccessConnectionDirectRequest( + uuid=connection_uuid, + peering_ipv4=fabricv4.InternetAccessPeeringIpv4Request( + equinix_peer_ip=equinix_peer_ip + ), + ) + ], + customer_routes=[ + fabricv4.InternetAccessCustomerRouteRequest( + ip_block=fabricv4.InternetAccessIpBlockRequest(uuid=ip_block.uuid) + ) + ], + ) + + request_body = fabricv4.InternetAccessPostRequest( + type=fabricv4.InternetAccessServiceType.SINGLE_IA, + name=f"panthers_eia_{utils.get_random_vlan_number()}", + bandwidth=INITIAL_BANDWIDTH, + routing_protocol=routing_protocol, + billing=fabricv4.InternetAccessPostRequestBilling( + type=fabricv4.InternetAccessBillingType.FIXED + ), + project=fabricv4.Project(project_id=self.test_data.project_id), + account=fabricv4.InternetAccessAccount( + account_number=self.test_data.account_number_eia + ), + ) + + response = self.apis.internet_access_services.create_eia_service_with_http_info( + request_body + ) + service = response.data + assert response.status_code == 201 + + type(self).created_service_uuid = service.uuid + + assert self._wait_for_service_is_in_state( + service.uuid, fabricv4.InternetAccessServiceState.PROVISIONED + ), "Internet Access Service did not reach PROVISIONED state" + return service From 167fb7db865046096f49debfb420463442b1d54c Mon Sep 17 00:00:00 2001 From: "tomasz.tutka" Date: Wed, 1 Jul 2026 07:54:31 +0200 Subject: [PATCH 2/4] corrected flow --- .github/workflows/fabric-integration-tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/fabric-integration-tests.yaml b/.github/workflows/fabric-integration-tests.yaml index d004386b..a3d4df63 100644 --- a/.github/workflows/fabric-integration-tests.yaml +++ b/.github/workflows/fabric-integration-tests.yaml @@ -18,7 +18,8 @@ jobs: if: >- ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' - && github.event.label.name == 'run-fabric-tests') }} + && github.event.label.name == 'run-fabric-tests' + && startsWith(github.head_ref, 'sync/gh')) }} runs-on: ubuntu-latest steps: - name: Checkout code From ecbc04f1b1c93ad56be2310c06df53df4985f3af Mon Sep 17 00:00:00 2001 From: "tomasz.tutka" Date: Wed, 1 Jul 2026 07:56:50 +0200 Subject: [PATCH 3/4] removed patch --- ...oud_router_search_sort_criteria_enum.patch | 286 ------------------ 1 file changed, 286 deletions(-) delete mode 100644 patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch diff --git a/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch b/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch deleted file mode 100644 index 0dcf12df..00000000 --- a/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch +++ /dev/null @@ -1,286 +0,0 @@ -diff --git a/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py -+++ b/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py -@@ -25,7 +25,7 @@ class CloudRouterActionsSearchSortCriteria(BaseModel): - CloudRouterActionsSearchSortCriteria - """ # noqa: E501 - direction: Optional[CloudRouterActionsSearchSortDirection] = CloudRouterActionsSearchSortDirection.DESC -- var_property: Optional[CloudRouterActionsSearchSortBy] = Field(default=CloudRouterActionsSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[CloudRouterActionsSearchSortBy] = Field(default=CloudRouterActionsSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class CloudRouterActionsSearchSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterActionsSearchSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else CloudRouterActionsSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else CloudRouterActionsSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py -+++ b/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py -@@ -25,7 +25,7 @@ class CloudRouterCommandSearchSortCriteria(BaseModel): - CloudRouterCommandSearchSortCriteria - """ # noqa: E501 - direction: Optional[CloudRouterCommandSearchSortDirection] = CloudRouterCommandSearchSortDirection.DESC -- var_property: Optional[CloudRouterCommandSearchSortBy] = Field(default=CloudRouterCommandSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[CloudRouterCommandSearchSortBy] = Field(default=CloudRouterCommandSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class CloudRouterCommandSearchSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterCommandSearchSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else CloudRouterCommandSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else CloudRouterCommandSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/cloud_router_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/cloud_router_sort_criteria.py -+++ b/equinix/services/fabricv4/models/cloud_router_sort_criteria.py -@@ -25,7 +25,7 @@ class CloudRouterSortCriteria(BaseModel): - CloudRouterSortCriteria - """ # noqa: E501 - direction: Optional[CloudRouterSortDirection] = CloudRouterSortDirection.DESC -- var_property: Optional[CloudRouterSortBy] = Field(default=CloudRouterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[CloudRouterSortBy] = Field(default=CloudRouterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class CloudRouterSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else CloudRouterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else CloudRouterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/connection_route_sort_criteria.py b/equinix/services/fabricv4/models/connection_route_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/connection_route_sort_criteria.py -+++ b/equinix/services/fabricv4/models/connection_route_sort_criteria.py -@@ -25,7 +25,7 @@ class ConnectionRouteSortCriteria(BaseModel): - ConnectionRouteSortCriteria - """ # noqa: E501 - direction: Optional[ConnectionRouteEntrySortDirection] = ConnectionRouteEntrySortDirection.DESC -- var_property: Optional[ConnectionRouteEntrySortBy] = Field(default=ConnectionRouteEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[ConnectionRouteEntrySortBy] = Field(default=ConnectionRouteEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class ConnectionRouteSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else ConnectionRouteEntrySortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else ConnectionRouteEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else ConnectionRouteEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/network_sort_criteria_response.py b/equinix/services/fabricv4/models/network_sort_criteria_response.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/network_sort_criteria_response.py -+++ b/equinix/services/fabricv4/models/network_sort_criteria_response.py -@@ -25,7 +25,7 @@ class NetworkSortCriteriaResponse(BaseModel): - NetworkSortCriteriaResponse - """ # noqa: E501 - direction: Optional[NetworkSortDirectionResponse] = NetworkSortDirectionResponse.DESC -- var_property: Optional[NetworkSortByResponse] = Field(default=NetworkSortByResponse.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[NetworkSortByResponse] = Field(default=NetworkSortByResponse.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class NetworkSortCriteriaResponse(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else NetworkSortDirectionResponse.DESC, -- "property": obj.get("property") if obj.get("property") is not None else NetworkSortByResponse.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else NetworkSortByResponse.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/network_sort_criteria.py b/equinix/services/fabricv4/models/network_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/network_sort_criteria.py -+++ b/equinix/services/fabricv4/models/network_sort_criteria.py -@@ -25,7 +25,7 @@ class NetworkSortCriteria(BaseModel): - NetworkSortCriteria - """ # noqa: E501 - direction: Optional[NetworkSortDirection] = NetworkSortDirection.DESC -- var_property: Optional[NetworkSortBy] = Field(default=NetworkSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[NetworkSortBy] = Field(default=NetworkSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class NetworkSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else NetworkSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else NetworkSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else NetworkSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/route_aggregation_sort_item.py b/equinix/services/fabricv4/models/route_aggregation_sort_item.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/route_aggregation_sort_item.py -+++ b/equinix/services/fabricv4/models/route_aggregation_sort_item.py -@@ -24,7 +24,7 @@ class RouteAggregationSortItem(BaseModel): - """ - RouteAggregationSortItem - """ # noqa: E501 -- var_property: Optional[RouteAggregationSortItemProperty] = Field(default=RouteAggregationSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[RouteAggregationSortItemProperty] = Field(default=RouteAggregationSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - direction: Optional[SortItemDirection] = SortItemDirection.DESC - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["property", "direction"] -@@ -87,7 +87,7 @@ class RouteAggregationSortItem(BaseModel): - return cls.model_validate(obj) - - _obj = cls.model_validate({ -- "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, -+ "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, - "direction": obj.get("direction") if obj.get("direction") is not None else SortItemDirection.DESC - }) - # store additional fields in additional_properties -diff --git a/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py b/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py -+++ b/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py -@@ -25,7 +25,7 @@ class RouteTableEntrySortCriteria(BaseModel): - RouteTableEntrySortCriteria - """ # noqa: E501 - direction: Optional[RouteTableEntrySortDirection] = RouteTableEntrySortDirection.DESC -- var_property: Optional[RouteTableEntrySortBy] = Field(default=RouteTableEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[RouteTableEntrySortBy] = Field(default=RouteTableEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class RouteTableEntrySortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else RouteTableEntrySortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else RouteTableEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else RouteTableEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/service_profile_sort_criteria.py b/equinix/services/fabricv4/models/service_profile_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/service_profile_sort_criteria.py -+++ b/equinix/services/fabricv4/models/service_profile_sort_criteria.py -@@ -25,7 +25,7 @@ class ServiceProfileSortCriteria(BaseModel): - ServiceProfileSortCriteria - """ # noqa: E501 - direction: Optional[ServiceProfileSortDirection] = ServiceProfileSortDirection.DESC -- var_property: Optional[ServiceProfileSortBy] = Field(default=ServiceProfileSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[ServiceProfileSortBy] = Field(default=ServiceProfileSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class ServiceProfileSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else ServiceProfileSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else ServiceProfileSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else ServiceProfileSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/sort_criteria_response.py b/equinix/services/fabricv4/models/sort_criteria_response.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/sort_criteria_response.py -+++ b/equinix/services/fabricv4/models/sort_criteria_response.py -@@ -25,7 +25,7 @@ class SortCriteriaResponse(BaseModel): - SortCriteriaResponse - """ # noqa: E501 - direction: Optional[SortDirection] = SortDirection.DESC -- var_property: Optional[SortBy] = Field(default=SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[SortBy] = Field(default=SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class SortCriteriaResponse(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else SortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/sort_criteria.py b/equinix/services/fabricv4/models/sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/sort_criteria.py -+++ b/equinix/services/fabricv4/models/sort_criteria.py -@@ -25,7 +25,7 @@ class SortCriteria(BaseModel): - SortCriteria - """ # noqa: E501 - direction: Optional[SortDirection] = SortDirection.DESC -- var_property: Optional[SortBy] = Field(default=SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[SortBy] = Field(default=SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class SortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else SortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/time_service_sort_criteria.py b/equinix/services/fabricv4/models/time_service_sort_criteria.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/time_service_sort_criteria.py -+++ b/equinix/services/fabricv4/models/time_service_sort_criteria.py -@@ -25,7 +25,7 @@ class TimeServiceSortCriteria(BaseModel): - TimeServiceSortCriteria - """ # noqa: E501 - direction: Optional[TimeServiceSortDirection] = TimeServiceSortDirection.DESC -- var_property: Optional[TimeServiceSortBy] = Field(default=TimeServiceSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[TimeServiceSortBy] = Field(default=TimeServiceSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["direction", "property"] - -@@ -88,7 +88,7 @@ class TimeServiceSortCriteria(BaseModel): - - _obj = cls.model_validate({ - "direction": obj.get("direction") if obj.get("direction") is not None else TimeServiceSortDirection.DESC, -- "property": obj.get("property") if obj.get("property") is not None else TimeServiceSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME -+ "property": obj.get("property") if obj.get("property") is not None else TimeServiceSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME - }) - # store additional fields in additional_properties - for _key in obj.keys(): -diff --git a/equinix/services/fabricv4/models/sort_item.py b/equinix/services/fabricv4/models/sort_item.py -index 5e00735..d7e5f1a 100644 ---- a/equinix/services/fabricv4/models/sort_item.py -+++ b/equinix/services/fabricv4/models/sort_item.py -@@ -24,7 +24,7 @@ class SortItem(BaseModel): - """ - SortItem - """ # noqa: E501 -- var_property: Optional[SortItemProperty] = Field(default=SortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") -+ var_property: Optional[SortItemProperty] = Field(default=SortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") - direction: Optional[SortItemDirection] = SortItemDirection.DESC - additional_properties: Dict[str, Any] = {} - __properties: ClassVar[List[str]] = ["property", "direction"] -@@ -87,7 +87,7 @@ class SortItem(BaseModel): - return cls.model_validate(obj) - - _obj = cls.model_validate({ -- "property": obj.get("property") if obj.get("property") is not None else SortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, -+ "property": obj.get("property") if obj.get("property") is not None else SortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, - "direction": obj.get("direction") if obj.get("direction") is not None else SortItemDirection.DESC - }) - # store additional fields in additional_properties From 1e30763380409b111adf10000f4f24d9abe54ced Mon Sep 17 00:00:00 2001 From: "tomasz.tutka" Date: Mon, 6 Jul 2026 19:24:14 +0200 Subject: [PATCH 4/4] revert: restore 20260128 cloud router sort-criteria patch to fix codegen build Removing this patch broke the test-codegen check: the 11 committed sort-criteria models rely on its enum-default fix, so regeneration without it produced output differing from the committed code. Restore it to keep the build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...oud_router_search_sort_criteria_enum.patch | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch diff --git a/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch b/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch new file mode 100644 index 00000000..0dcf12df --- /dev/null +++ b/patches/services/fabricv4/20260128_fix_cloud_router_search_sort_criteria_enum.patch @@ -0,0 +1,286 @@ +diff --git a/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py ++++ b/equinix/services/fabricv4/models/cloud_router_actions_search_sort_criteria.py +@@ -25,7 +25,7 @@ class CloudRouterActionsSearchSortCriteria(BaseModel): + CloudRouterActionsSearchSortCriteria + """ # noqa: E501 + direction: Optional[CloudRouterActionsSearchSortDirection] = CloudRouterActionsSearchSortDirection.DESC +- var_property: Optional[CloudRouterActionsSearchSortBy] = Field(default=CloudRouterActionsSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[CloudRouterActionsSearchSortBy] = Field(default=CloudRouterActionsSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class CloudRouterActionsSearchSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterActionsSearchSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else CloudRouterActionsSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else CloudRouterActionsSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py ++++ b/equinix/services/fabricv4/models/cloud_router_command_search_sort_criteria.py +@@ -25,7 +25,7 @@ class CloudRouterCommandSearchSortCriteria(BaseModel): + CloudRouterCommandSearchSortCriteria + """ # noqa: E501 + direction: Optional[CloudRouterCommandSearchSortDirection] = CloudRouterCommandSearchSortDirection.DESC +- var_property: Optional[CloudRouterCommandSearchSortBy] = Field(default=CloudRouterCommandSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[CloudRouterCommandSearchSortBy] = Field(default=CloudRouterCommandSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class CloudRouterCommandSearchSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterCommandSearchSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else CloudRouterCommandSearchSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else CloudRouterCommandSearchSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/cloud_router_sort_criteria.py b/equinix/services/fabricv4/models/cloud_router_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/cloud_router_sort_criteria.py ++++ b/equinix/services/fabricv4/models/cloud_router_sort_criteria.py +@@ -25,7 +25,7 @@ class CloudRouterSortCriteria(BaseModel): + CloudRouterSortCriteria + """ # noqa: E501 + direction: Optional[CloudRouterSortDirection] = CloudRouterSortDirection.DESC +- var_property: Optional[CloudRouterSortBy] = Field(default=CloudRouterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[CloudRouterSortBy] = Field(default=CloudRouterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class CloudRouterSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else CloudRouterSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else CloudRouterSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else CloudRouterSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/connection_route_sort_criteria.py b/equinix/services/fabricv4/models/connection_route_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/connection_route_sort_criteria.py ++++ b/equinix/services/fabricv4/models/connection_route_sort_criteria.py +@@ -25,7 +25,7 @@ class ConnectionRouteSortCriteria(BaseModel): + ConnectionRouteSortCriteria + """ # noqa: E501 + direction: Optional[ConnectionRouteEntrySortDirection] = ConnectionRouteEntrySortDirection.DESC +- var_property: Optional[ConnectionRouteEntrySortBy] = Field(default=ConnectionRouteEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[ConnectionRouteEntrySortBy] = Field(default=ConnectionRouteEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class ConnectionRouteSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else ConnectionRouteEntrySortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else ConnectionRouteEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else ConnectionRouteEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/network_sort_criteria_response.py b/equinix/services/fabricv4/models/network_sort_criteria_response.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/network_sort_criteria_response.py ++++ b/equinix/services/fabricv4/models/network_sort_criteria_response.py +@@ -25,7 +25,7 @@ class NetworkSortCriteriaResponse(BaseModel): + NetworkSortCriteriaResponse + """ # noqa: E501 + direction: Optional[NetworkSortDirectionResponse] = NetworkSortDirectionResponse.DESC +- var_property: Optional[NetworkSortByResponse] = Field(default=NetworkSortByResponse.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[NetworkSortByResponse] = Field(default=NetworkSortByResponse.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class NetworkSortCriteriaResponse(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else NetworkSortDirectionResponse.DESC, +- "property": obj.get("property") if obj.get("property") is not None else NetworkSortByResponse.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else NetworkSortByResponse.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/network_sort_criteria.py b/equinix/services/fabricv4/models/network_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/network_sort_criteria.py ++++ b/equinix/services/fabricv4/models/network_sort_criteria.py +@@ -25,7 +25,7 @@ class NetworkSortCriteria(BaseModel): + NetworkSortCriteria + """ # noqa: E501 + direction: Optional[NetworkSortDirection] = NetworkSortDirection.DESC +- var_property: Optional[NetworkSortBy] = Field(default=NetworkSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[NetworkSortBy] = Field(default=NetworkSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class NetworkSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else NetworkSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else NetworkSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else NetworkSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/route_aggregation_sort_item.py b/equinix/services/fabricv4/models/route_aggregation_sort_item.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/route_aggregation_sort_item.py ++++ b/equinix/services/fabricv4/models/route_aggregation_sort_item.py +@@ -24,7 +24,7 @@ class RouteAggregationSortItem(BaseModel): + """ + RouteAggregationSortItem + """ # noqa: E501 +- var_property: Optional[RouteAggregationSortItemProperty] = Field(default=RouteAggregationSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteAggregationSortItemProperty] = Field(default=RouteAggregationSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + direction: Optional[SortItemDirection] = SortItemDirection.DESC + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["property", "direction"] +@@ -87,7 +87,7 @@ class RouteAggregationSortItem(BaseModel): + return cls.model_validate(obj) + + _obj = cls.model_validate({ +- "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, ++ "property": obj.get("property") if obj.get("property") is not None else RouteAggregationSortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "direction": obj.get("direction") if obj.get("direction") is not None else SortItemDirection.DESC + }) + # store additional fields in additional_properties +diff --git a/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py b/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py ++++ b/equinix/services/fabricv4/models/route_table_entry_sort_criteria.py +@@ -25,7 +25,7 @@ class RouteTableEntrySortCriteria(BaseModel): + RouteTableEntrySortCriteria + """ # noqa: E501 + direction: Optional[RouteTableEntrySortDirection] = RouteTableEntrySortDirection.DESC +- var_property: Optional[RouteTableEntrySortBy] = Field(default=RouteTableEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[RouteTableEntrySortBy] = Field(default=RouteTableEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class RouteTableEntrySortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else RouteTableEntrySortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else RouteTableEntrySortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else RouteTableEntrySortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/service_profile_sort_criteria.py b/equinix/services/fabricv4/models/service_profile_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/service_profile_sort_criteria.py ++++ b/equinix/services/fabricv4/models/service_profile_sort_criteria.py +@@ -25,7 +25,7 @@ class ServiceProfileSortCriteria(BaseModel): + ServiceProfileSortCriteria + """ # noqa: E501 + direction: Optional[ServiceProfileSortDirection] = ServiceProfileSortDirection.DESC +- var_property: Optional[ServiceProfileSortBy] = Field(default=ServiceProfileSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[ServiceProfileSortBy] = Field(default=ServiceProfileSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class ServiceProfileSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else ServiceProfileSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else ServiceProfileSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else ServiceProfileSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/sort_criteria_response.py b/equinix/services/fabricv4/models/sort_criteria_response.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/sort_criteria_response.py ++++ b/equinix/services/fabricv4/models/sort_criteria_response.py +@@ -25,7 +25,7 @@ class SortCriteriaResponse(BaseModel): + SortCriteriaResponse + """ # noqa: E501 + direction: Optional[SortDirection] = SortDirection.DESC +- var_property: Optional[SortBy] = Field(default=SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[SortBy] = Field(default=SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class SortCriteriaResponse(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else SortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/sort_criteria.py b/equinix/services/fabricv4/models/sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/sort_criteria.py ++++ b/equinix/services/fabricv4/models/sort_criteria.py +@@ -25,7 +25,7 @@ class SortCriteria(BaseModel): + SortCriteria + """ # noqa: E501 + direction: Optional[SortDirection] = SortDirection.DESC +- var_property: Optional[SortBy] = Field(default=SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[SortBy] = Field(default=SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class SortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else SortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else SortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else SortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/time_service_sort_criteria.py b/equinix/services/fabricv4/models/time_service_sort_criteria.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/time_service_sort_criteria.py ++++ b/equinix/services/fabricv4/models/time_service_sort_criteria.py +@@ -25,7 +25,7 @@ class TimeServiceSortCriteria(BaseModel): + TimeServiceSortCriteria + """ # noqa: E501 + direction: Optional[TimeServiceSortDirection] = TimeServiceSortDirection.DESC +- var_property: Optional[TimeServiceSortBy] = Field(default=TimeServiceSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[TimeServiceSortBy] = Field(default=TimeServiceSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["direction", "property"] + +@@ -88,7 +88,7 @@ class TimeServiceSortCriteria(BaseModel): + + _obj = cls.model_validate({ + "direction": obj.get("direction") if obj.get("direction") is not None else TimeServiceSortDirection.DESC, +- "property": obj.get("property") if obj.get("property") is not None else TimeServiceSortBy.CHANGE_LOG_SLASH_UPDATED_DATE_TIME ++ "property": obj.get("property") if obj.get("property") is not None else TimeServiceSortBy.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME + }) + # store additional fields in additional_properties + for _key in obj.keys(): +diff --git a/equinix/services/fabricv4/models/sort_item.py b/equinix/services/fabricv4/models/sort_item.py +index 5e00735..d7e5f1a 100644 +--- a/equinix/services/fabricv4/models/sort_item.py ++++ b/equinix/services/fabricv4/models/sort_item.py +@@ -24,7 +24,7 @@ class SortItem(BaseModel): + """ + SortItem + """ # noqa: E501 +- var_property: Optional[SortItemProperty] = Field(default=SortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") ++ var_property: Optional[SortItemProperty] = Field(default=SortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, alias="property") + direction: Optional[SortItemDirection] = SortItemDirection.DESC + additional_properties: Dict[str, Any] = {} + __properties: ClassVar[List[str]] = ["property", "direction"] +@@ -87,7 +87,7 @@ class SortItem(BaseModel): + return cls.model_validate(obj) + + _obj = cls.model_validate({ +- "property": obj.get("property") if obj.get("property") is not None else SortItemProperty.CHANGE_LOG_SLASH_UPDATED_DATE_TIME, ++ "property": obj.get("property") if obj.get("property") is not None else SortItemProperty.SLASH_CHANGE_LOG_SLASH_UPDATED_DATE_TIME, + "direction": obj.get("direction") if obj.get("direction") is not None else SortItemDirection.DESC + }) + # store additional fields in additional_properties