hfortix-fortiztp 0.5.162
FortiZTP Cloud API Client - Device provisioning and management
License: MIT
Requires Python: >=3.9
Keywords: fortinet, fortiztp, api, client, provisioning, zero-touch, fortigate, fortiap, fortiswitch

# FortiZTP Package

Python SDK for FortiZTP Cloud API v2.0 - Device provisioning and management for FortiGate, FortiAP, FortiSwitch, and FortiExtender.

## Status

**✅ FUNCTIONAL - BETA RELEASE**

**Schema Completion:** 100% (18/18 endpoints)
- ✅ Device endpoints: 5/5 complete
- ✅ Script endpoints: 7/7 complete
- ✅ FortiManager endpoints: 5/5 complete
- ✅ System endpoint: 1/1 complete

**Code Generation:** ✅ Complete - All 18 endpoints auto-generated from schema

**Testing:** ✅ Live tests passing - GET /v2/devices validated with real API

See `dev/SCHEMA_GAPS_ANALYSIS.md` for details on missing information.

---

## Overview

This package will provide a fully typed Python SDK for FortiZTP Cloud API with:

- 🔒 **OAuth 2.0 Authentication** - Automatic token management
- 📝 **Full Type Safety** - Type hints with Literal types for autocomplete
- 🎯 **Specialized Responses** - Property access on response objects
- 🔄 **Smart Defaults** - Sensible defaults for optional parameters
- ⚡ **HTTP Metadata** - Status codes, response times, raw access
- 🛡️ **Error Handling** - Comprehensive exception handling

---

## Quick Start

### Installation

```bash
pip install hfortix-fortiztp
```

### Basic Usage

```python
from hfortix_fortiztp import FortiZTP

# Option 1: Auto-login with credentials
client = FortiZTP(
    api_id="your_api_id",
    password="your_password"
)

# Option 2: CloudSession (recommended for multi-service)
from hfortix_core.session import CloudSession

with CloudSession(api_id="...", password="...") as session:
    client = FortiZTP(session=session)  # Auto-uses "fortiztp" client_id
    devices = client.api.devices.list()
    # Token managed automatically

# Option 2b: CloudSession with global rate limiting (NEW)
from hfortix_core.session import CloudSession
from hfortix_fortiztp import FortiZTP
from hfortix_forticare import FortiCare

# Configure rate limiting once at session level
session = CloudSession(
    api_id="your_api_id",
    password="your_password",
    # Global rate limiting - inherited by all clients
    rate_limit=True,
    rate_limit_max_requests=100,      # 100 requests per hour
    rate_limit_window_seconds=3600.0, # 1 hour window
    circuit_breaker=True,
)

# Both clients inherit session's rate limiting
fz = FortiZTP(session=session)     # Inherits 100 req/hour
fc = FortiCare(session=session)    # Inherits 100 req/hour

# Or override for specific client
fz_fast = FortiZTP(
    session=session,
    rate_limit_max_requests=200,   # Override session's 100 req/hour
)

# Option 3: Pre-obtained OAuth token
client = FortiZTP(oauth_token="your_token")

# List all devices
devices = client.api.devices.list()
print(f"Found {len(devices.devices)} devices")

# List provisioned devices only
provisioned = client.api.devices.list(provision_status="provisioned")

# Get specific device details
device = client.api.devices.get(device_sn="FGT60FTK19000001")
print(f"Device: {device.deviceType} - Status: {device.provisionStatus}")

# Get system status
status = client.api.system.get()
print(f"System status: {status.serviceStatus}")
```

### Rate Limit Enforcement (NEW)

**NEW in 0.5.162+**: Enforce rate limits using token bucket algorithm with queue support:

```python
from hfortix_fortiztp import FortiZTP

# Option 1: Direct client with rate limiting
client = FortiZTP(
    api_id="your_api_id",
    password="your_password",
    rate_limit=True,
    rate_limit_max_requests=2000,     # FortiZTP: 2000/hour
    rate_limit_window_seconds=3600.0, # 1 hour window
    rate_limit_strategy="queue",      # Queue excess requests
    rate_limit_queue_size=100,        # Max 100 queued requests
)

# Requests automatically queued when limit exceeded
for i in range(2100):
    devices = client.api.devices.list()  # First 2000 immediate, next 100 queued

# Option 2: CloudSession with global rate limiting (recommended)
from hfortix_core.session import CloudSession

session = CloudSession(
    api_id="your_api_id",
    password="your_password",
    rate_limit=True,
    rate_limit_max_requests=2000,
    rate_limit_window_seconds=3600.0,
)

# FortiZTP inherits session's rate limiting
client = FortiZTP(session=session)
```

**Rate Limiting Parameters:**
- `rate_limit: bool = False` - Enable/disable rate limiting
- `rate_limit_strategy: str = "queue"` - Strategy: `queue`, `drop`, or `raise`
- `rate_limit_max_requests: int = 100` - Max requests per window
- `rate_limit_window_seconds: float = 60.0` - Time window in seconds
- `rate_limit_queue_size: int = 100` - Max queued requests
- `rate_limit_queue_timeout: float = 30.0` - Max wait time in queue
- `rate_limit_queue_overflow: str = "block"` - Overflow behavior: `block`, `drop`, or `raise`
- `circuit_breaker: bool = False` - Enable circuit breaker
- `circuit_breaker_threshold: int = 5` - Failures before opening
- `circuit_breaker_timeout: float = 60.0` - Seconds before retry
- `circuit_breaker_half_open_calls: int = 3` - Test calls in half-open state

### Rate Limit Tracking

FortiZTP API has a limit of **2000 calls per hour**. Track your usage:

```python
from hfortix_fortiztp import FortiZTP

# Configure custom limits (optional, defaults to None)
client = FortiZTP(
    api_id="...",
    password="...",
    rate_limit_calls_per_hour=2000  # FortiZTP: 2000/hour
)

# Make API calls
devices = client.api.devices.list()

# Check rate limit status
status = client.get_rate_limit_status()
print(f"Calls last min: {status['calls_last_min']}")
print(f"Calls last 5min: {status['calls_last_5min']}")
print(f"Calls last hour: {status['calls_last_hour']}/{status['limits']['calls_per_hour']}")
print(f"Within limits: {status['within_limits']}")
```

**Note**: Rate limiting counters are **informational only** and do not enforce limits.

---

## API Coverage

### Devices API (5 endpoints)
- ✅ `list()` - List devices with filters (status, type, SN)
- ✅ `bulk_provision()` - Provision/unprovision multiple devices
- ✅ `get()` - Get single device details
- ✅ `update()` - Provision/unprovision single device
- ✅ `firmware_profiles()` - Get firmware profiles by region

### Scripts API (7 endpoints)
- ✅ `scripts_list()` - List all scripts
- ✅ `scripts_post()` - Create new script
- ✅ `scripts_get()` - Get script metadata
- ✅ `scripts_put()` - Update script metadata
- ✅ `scripts_delete()` - Delete script
- ✅ `scripts_content_get()` - Download script content
- ✅ `scripts_content_put()` - Upload script content

### FortiManagers API (5 endpoints)
- ✅ `fortimanagers_list()` - List all FortiManager configs
- ✅ `fortimanagers_post()` - Create FortiManager config
- ✅ `fortimanagers_get()` - Get FortiManager details
- ✅ `fortimanagers_put()` - Update FortiManager config
- ✅ `fortimanagers_delete()` - Delete FortiManager config

### System API (1 endpoint)
- ✅ `get()` - Get system status
- Script association

### System (1 endpoint)
- Get system status

---

## Planned Usage

> **Note:** This is the intended API design. Code is not yet generated.

### Basic Usage

```python
from hfortix import FortiZTP

# Initialize client with OAuth credentials
client = FortiZTP(
    client_id="your_client_id",
    api_key="your_api_key",
    password="your_password"
)

# List all provisioned devices
response = client.v2.devices.list.get(provision_status="provisioned")

# Access response properties
print(f"Total devices: {response.total}")
print(f"Has cache: {response.hasCache}")

for device in response.data:
    print(f"{device['deviceSN']}: {device['provisionStatus']}")

# Get specific device
device = client.v2.devices.get.get(device_sn="FGT60D4615067214")
print(f"Device type: {device.deviceType}")
print(f"Status: {device.provisionStatus}")

# Provision a device to FortiManager
client.v2.devices.put.put(
    device_sn="FGT60D4615067214",
    request_body={
        "provisionStatus": "provisioned",
        "provisionTarget": "FortiManager",
        "provisionTargetOid": 123
    }
)
```

### Type Safety with Literals

```python
from typing import Literal

# IDE will autocomplete these values:
DeviceType = Literal["FortiGate", "FortiAP", "FortiSwitch", "FortiExtender"]
ProvisionStatus = Literal["provisioned", "unprovisioned", "hidden", "incomplete"]
ProvisionTarget = Literal["FortiManager", "FortiGateCloud", "FortiEdgeCloud", "ExternalController"]

# Strongly typed parameters
response = client.v2.devices.list.get(
    provision_status="provisioned",  # Autocomplete!
    device_type="FortiGate"           # Autocomplete!
)
```

### Script Management

```python
# List all scripts
scripts = client.v2.settings.scripts.list.get()
print(f"Total scripts: {scripts.total}")

# Create new script
new_script = client.v2.settings.scripts.post.post(
    request_body={
        "name": "Initial Configuration",
    }
)
print(f"Created script OID: {new_script.oid}")

# Upload script content
client.v2.settings.scripts.put_content.put(
    oid=new_script.oid,
    file_path="./scripts/initial_config.conf"
)

# Download script content
content = client.v2.settings.scripts.get_content.get(oid=new_script.oid)
print(content.raw)  # Plain text content
```

### FortiManager Configuration

```python
# Add FortiManager
fmg = client.v2.settings.fortimanagers.post.post(
    request_body={
        "sn": "FMG-VMTM23010656",
        "ip": "192.168.223.20",
        "scriptOid": 123  # Optional pre-run script
    }
)

# HA Setup (comma-separated values)
fmg_ha = client.v2.settings.fortimanagers.post.post(
    request_body={
        "sn": "FMG-VMTM23010656,FMG-VMTM23010657",
        "ip": "192.168.223.20,192.168.223.21",
        "scriptOid": 123
    }
)

# List all FortiManagers
all_fmgs = client.v2.settings.fortimanagers.list.get()
for fmg in all_fmgs.data:
    print(f"{fmg['sn']} at {fmg['ip']}")
```

### HTTP Metadata Access

```python
response = client.v2.devices.get.get(device_sn="FGT60D4615067214")

# HTTP metadata
print(f"Status code: {response.http_status_code}")
print(f"Response time: {response.response_time}s")

# Raw access
raw_dict = response.raw
copy_dict = response.dict()
```

### Error Handling

```python
from hfortix_core.exceptions import (
    AuthenticationError,
    PermissionDeniedError,
    ResourceNotFoundError,
    RateLimitError
)

try:
    device = client.v2.devices.get.get(device_sn="INVALID_SN")
except ResourceNotFoundError as e:
    print(f"Device not found: {e}")
except PermissionDeniedError as e:
    print(f"Access denied: {e}")
except RateLimitError as e:
    print(f"Rate limit exceeded: {e}")
    print(f"Retry after: {e.retry_after}s")
```

---

## Development Status

### Completed ✅
- [x] Generator architecture designed
- [x] Schema file structure created
- [x] Type definitions (5 Literal types)
- [x] Common data structures (5 structures)
- [x] All 18 endpoints documented
- [x] 6 endpoints with complete responses

### In Progress ⏳
- [ ] Complete schema (12 endpoints missing responses)
- [ ] Verify data structures
- [ ] Get firmware_profiles structure

### Not Started 📋
- [ ] Schema parser
- [ ] Endpoint generator adaptation
- [ ] Template creation
- [ ] Code generation
- [ ] Testing
- [ ] Documentation
- [ ] PyPI release

---

## Project Structure

```
packages/fortiztp/
├── README.md                          # This file
├── dev/
│   ├── GENERATOR_PLAN.md              # Implementation roadmap
│   ├── API_DOCUMENTATION.md           # API reference
│   ├── SCHEMA_GAPS_ANALYSIS.md        # Missing information checklist
│   ├── generator/
│   │   ├── generate.py                # Main generator script (to be created)
│   │   ├── schema/
│   │   │   └── fortiztp_schema.py     # ✅ Manual API schema
│   │   ├── parsers/
│   │   │   └── fortiztp_schema_parser.py  # Schema parser (to be created)
│   │   ├── generators/
│   │   │   └── endpoint_generator.py  # Code generator (to be adapted)
│   │   ├── templates/
│   │   │   ├── endpoint_template.py.tmpl   # (to be created)
│   │   │   └── endpoint_template.pyi.tmpl  # (to be created)
│   │   └── endpoint_config.py         # FortiZTP overrides (to be created)
│   └── tests/                         # Generator tests (to be created)
└── src/
    └── hfortix_fortiztp/              # Generated SDK code (not yet created)
        ├── __init__.py
        ├── models.py                  # Response classes
        ├── types.py                   # Literal type aliases
        ├── api/
        │   └── v2/
        │       ├── __init__.py
        │       ├── devices/
        │       ├── settings/
        │       │   ├── scripts/
        │       │   └── fortimanagers/
        │       └── system/
        └── py.typed
```

---

## Schema Status

### Type Definitions (5/5) ✅
- DeviceType
- ProvisionStatus
- ProvisionSubStatus
- ProvisionTarget
- ServiceStatus

### Data Structures (5/5) ✅
- DEVICE_V2_DATA (16 properties) - needs verification
- SCRIPT_META_DATA (3 properties) - needs verification
- FORTIMANAGER_META_DATA (5 properties)
- SYSTEM_DATA (4 properties)
- ERROR_RESPONSE (2 properties)

### Endpoints (6/18) ⚠️

**Complete:**
- ✅ GET /v2/setting/fortimanagers
- ✅ POST /v2/setting/fortimanagers
- ✅ GET /v2/setting/fortimanagers/{oid}
- ✅ PUT /v2/setting/fortimanagers/{oid}
- ✅ DELETE /v2/setting/fortimanagers/{oid}
- ✅ GET /v2/system

**Missing Response Definitions (12):**
- ❌ GET /v2/devices
- ❌ PUT /v2/devices
- ❌ GET /v2/devices/{deviceSN}
- ❌ PUT /v2/devices/{deviceSN}
- ❌ GET /v2/devices/{deviceSN}/regions/{region}/firmwareprofiles
- ❌ GET /v2/setting/scripts
- ❌ POST /v2/setting/scripts
- ❌ GET /v2/setting/scripts/{oid}
- ❌ PUT /v2/setting/scripts/{oid}
- ❌ DELETE /v2/setting/scripts/{oid}
- ❌ GET /v2/setting/scripts/{oid}/content
- ❌ PUT /v2/setting/scripts/{oid}/content

---

## Next Steps

### Critical (Blocking Code Generation):
1. Get response body examples for 12 missing endpoints
2. Verify DEVICE_V2_DATA structure (all 16 fields correct?)
3. Get firmware_profiles actual response structure
4. Confirm HTTP status codes (200, 201, 204)

### Implementation:
5. Create schema parser
6. Adapt endpoint generator from FortiCare
7. Create templates
8. Generate code
9. Test and validate

---

## Resources

- **API Base URL**: https://fortiztp.forticloud.com/public/api
- **Auth URL**: https://customerapiauth.fortinet.com
- **API Version**: v2.0
- **Rate Limit**: 2000 calls/hour
- **Authentication**: OAuth 2.0 (IAM API Users - Local type only)

---

## Contributing

This package is part of the hfortix SDK family:
- hfortix-core: Core HTTP client and utilities
- hfortix-fortios: FortiOS REST API
- hfortix-forticare: FortiCare Asset Management API
- hfortix-fortiztp: FortiZTP Cloud API (this package)
- hfortix: Meta package

See main repository for contribution guidelines.

---

## License

[Your License Here]

---

## Authors

[Your Name/Organization]

---

**Last Updated:** February 6, 2026  
**Schema Version:** 2.0 (33% complete)  
**Status:** Development (blocked - awaiting API documentation)