SKILL DETAIL
huawei-cloud-obs-website-host
huaweicloud/huaweicloud-skills/huawei-cloud-obs-website-host
This skill configures static website hosting on an existing Huawei Cloud OBS bucket using the Python SDK and a custom domain. It supports enabling or repairing website hosting, setting index and error documents, exposing the bucket for public website access via a custom domain, and connecting the domain through Huawei Cloud DNS when applicable. Per Huawei Cloud security compliance, the default OBS bucket domain is prohibited for online preview, so a custom domain is mandatory. The skill guides users through domain registration and ICP filing (if applicable), and ensures the custom domain is registered on the bucket and resolves correctly. It includes scripts for configuration and verification, and helps diagnose 403, 404, and DNS issues.
Installation
npx skills add https://github.com/huaweicloud/huaweicloud-skills --skill huawei-cloud-obs-website-host
スキルファイル
SKILL.md
最終同期 · 2026/08/29
references/cli-installation-guide.md›
# CLI Installation and Configuration Guide
Use this reference when you need to install or configure **KooCLI (`hcloud`)** or **`obsutil`**.
---
## Table of Contents
- [hcloud (KooCLI)](#hcloud-koocli)
- [Linux Installation](#linux-installation)
- [Windows Installation](#windows-installation)
- [Configure hcloud](#configure-hcloud)
- [obsutil](#obsutil)
- [Config File Location](#config-file-location)
- [Linux AMD64 (x86\_64)](#linux-amd64-x86_64)
- [Linux ARM64](#linux-arm64)
- [macOS (AMD64)](#macos-amd64)
- [Windows (AMD64)](#windows-amd64)
- [Generate Config File](#generate-config-file)
- [Secure Credential Check](#secure-credential-check)
- [Notes](#notes)
---
## hcloud (KooCLI)
### Linux Installation
```bash
curl -sSL https://ap-southeast-3-hwcloudcli.obs.ap-southeast-3.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh
bash ./hcloud_install.sh -y
```
Interactive install:
```bash
bash ./hcloud_install.sh
```
### Windows Installation
1. Download the package:
- `https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-windows-amd64.zip`
2. Unzip it and get `hcloud.exe`.
3. Add the folder containing `hcloud.exe` to `Path` if desired.
4. Verify:
```powershell
hcloud version
```
### Configure hcloud
Interactive init:
```bash
hcloud configure init
```
AK/SK mode:
```bash
hcloud configure set --cli-profile=default --cli-mode=AKSK --cli-region=<region> --cli-access-key=<ak> --cli-secret-key=<sk>
```
Verify current profile:
```bash
hcloud version
hcloud configure list
```
---
## obsutil
### Config File Location
`obsutil` auto-generates a config file named `.obsutilconfig` in user home directory:
- macOS/Linux: `~/.obsutilconfig`
- Windows: `C:\Users\<username>\.obsutilconfig`
### Linux AMD64 (x86_64)
```bash
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
tar -xzvf obsutil_linux_amd64.tar.gz
cd obsutil_linux_amd64_*
chmod 755 obsutil
./obsutil version
```
### Linux ARM64
```bash
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_arm64.tar.gz
tar -xzvf obsutil_linux_arm64.tar.gz
cd obsutil_linux_arm64_*
chmod 755 obsutil
./obsutil version
```
### macOS (AMD64)
```bash
curl -O https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_darwin_amd64.tar.gz
tar -xzvf obsutil_darwin_amd64.tar.gz
cd obsutil_darwin_amd64_*
chmod 755 obsutil
./obsutil version
```
### Windows (AMD64)
1. Download the package:
- `https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_windows_amd64.zip`
2. Unzip it.
3. Open `cmd` or PowerShell in the extracted directory.
4. Run:
```powershell
obsutil.exe version
```
### Generate Config File
```bash
# generate config file
./obsutil config
```
Windows:
```powershell
obsutil.exe config
```
### Secure Credential Check (No Value Output)
Do not print `ak`, `sk`, or `securitytoken` values to console.
Only print key presence status (`true` / `false`).
Never run:
- `cat ~/.obsutilconfig`
- `grep -E "ak|sk|token" ~/.obsutilconfig`
### Notes
- Internet connectivity is required when downloading packages.
- `chmod 755 obsutil` is required before running `obsutil` on Linux/macOS.
- If `./obsutil version` returns version information, installation is successful.
- For macOS, run `chmod 755 obsutil` in the extracted directory before first use.
references/hcloud-dns-obs-website.md›
# Huawei Cloud DNS Configuration for OBS Static Website
Use this reference when you need to configure DNS records for an OBS static website custom domain via Huawei Cloud DNS (hcloud CLI).
## Prerequisites
- `hcloud` CLI installed and configured with AK/SK credentials (see `references/hcloud-install-config.md`)
- The DNS zone for your domain already exists in Huawei Cloud DNS
## Workflow
### 1. Find the DNS Zone ID
List all public zones and locate the one matching your domain:
```bash
hcloud DNS ListPublicZones --cli-region=<region>
```
Look for the zone whose `name` matches your domain (e.g., `example.com.`). Note its `id`.
### 2. Check Existing Record Sets
Verify there is no conflicting record for your subdomain:
```bash
hcloud DNS ListRecordSets --zone_type=public --cli-region=<region>
```
Look for records with the name `<subdomain>.<domain>.` (e.g., `www.example.com.`).
### 3. Create a CNAME Record
Create a CNAME record pointing your custom domain to the OBS website endpoint:
```bash
hcloud DNS CreateRecordSet \
--zone_id="<zone_id>" \
--name="<custom_domain>." \
--type="CNAME" \
--records.1="<bucket_name>.obs.<region>.myhuaweicloud.com." \
--cli-region=<region> \
--ttl=300
```
**Parameters:**
| Parameter | Value | Description |
|-----------|-------|-------------|
| `--zone_id` | Zone UUID | The ID of your DNS zone from step 1 |
| `--name` | `custom_domain.` | Full domain name **with trailing dot** |
| `--type` | `CNAME` | Record type for domain alias |
| `--records.1` | OBS website endpoint | Target URL **with trailing dot**, e.g. `my-bucket.obs.cn-north-4.myhuaweicloud.com.` |
| `--cli-region` | Region | Region where the DNS API is called |
| `--ttl` | `300` (recommended) | Time-to-live in seconds |
**Example:**
```bash
hcloud DNS CreateRecordSet \
--zone_id="ff8080828fb6d17b018fbd5a2fac1d7f" \
--name="www.example.com." \
--type="CNAME" \
--records.1="my-bucket.obs.cn-north-4.myhuaweicloud.com." \
--cli-region=cn-north-4 \
--ttl=300
```
### 4. Verify DNS Resolution
Check that the CNAME record resolves correctly:
```bash
dig +short <custom_domain> CNAME
```
Expected output:
```
<bucket_name>.obs.<region>.myhuaweicloud.com.
```
## Important Notes
- **Trailing dot**: Both the `--name` and `--records.1` values must end with a `.` (period) — this is the fully qualified domain name (FQDN) format required by Huawei Cloud DNS API.
- **DNS propagation**: After creation, the record status may show `PENDING_CREATE`. Propagation typically completes within minutes.
- **OBS custom domain registration**: Creating a DNS CNAME record alone is NOT sufficient. You must also register the custom domain on the OBS bucket via `setBucketCustomDomain` (see `references/obs-python-sdk-website.md`). Use the `--custom-domain` flag of `scripts/set_obs_website_sdk.py`.
- **HTTPS**: The OBS website endpoint serves HTTP by default. For HTTPS, consider using CDN (Content Delivery Network) with an SSL certificate.
- **Bucket name with dots**: If the bucket name contains dots, HTTPS access may be problematic. A custom domain with CDN+SSL is recommended.
## Troubleshooting
| Symptom | Likely Cause | Solution |
|---------|-------------|----------|
| `dig` returns no result | DNS not propagated or record not created | Check `hcloud DNS ListRecordSets` to confirm record exists |
| Custom domain not reachable | Missing `setBucketCustomDomain` on OBS bucket | Re-run `set_obs_website_sdk.py` with `--custom-domain` flag |
references/iam-policies.md›
# IAM Policy - Huawei Cloud OBS Website Host
## Permission Usage
| API Action | Permission | Purpose |
|------------|------------|---------|
| obs:bucket:HeadBucket | Read bucket existence/access status | Verify bucket exists and caller can access it before configuration |
| obs:bucket:GetBucketLocation | Read bucket region | Verify bucket region matches expected deployment region |
| obs:bucket:GetBucketCustomDomainConfiguration | Read bucket custom domain configuration | Check whether a custom domain is already registered |
| obs:bucket:GetBucketWebsite | Read bucket website configuration | Check static website hosting settings |
| dns:recordset:list | List DNS recordsets | Check whether the CNAME record exists |
| dns:zone:get | Read DNS zone details | Confirm the target zone exists |
| dns:zone:list | List DNS zones | Find the target zone |
| obs:bucket:PutBucketCustomDomainConfiguration | Update bucket custom domain configuration | Register or update a custom domain |
| obs:bucket:PutBucketWebsite | Update bucket website configuration | Set index/error page |
| dns:recordset:create | Create DNS recordset | Create the CNAME record |
## Minimum Policy JSON
```json
{
"Version": "1.1",
"Statement": [
{
"Effect": "Allow",
"Action": [
"obs:bucket:HeadBucket",
"obs:bucket:GetBucketLocation",
"obs:bucket:GetBucketCustomDomainConfiguration",
"obs:bucket:GetBucketWebsite",
"obs:bucket:PutBucketCustomDomainConfiguration",
"obs:bucket:PutBucketWebsite",
"dns:recordset:create",
"dns:recordset:list",
"dns:zone:get",
"dns:zone:list"
],
"Resource": [
"*"
]
}
]
}
```
## Notes
- If you run `verify_obs_website.py` with `--bucket-name/--region`, `obs:bucket:HeadBucket` and `obs:bucket:GetBucketLocation` are required.
- If you only configure bucket-level static website hosting and do not use a custom domain, DNS permissions are optional.
- If you use a custom domain, both OBS and DNS permissions above are required.
references/obs-python-sdk-website.md›
# OBS Python SDK Website Configuration Notes
Use Huawei Cloud OBS Python SDK (`esdk-obs-python >= 3.x`) for static website hosting configuration.
## SDK package
- Install: `pip install esdk-obs-python`
- Required imports:
```python
from obs import ObsClient, WebsiteConfiguration, IndexDocument, ErrorDocument
```
## Required action
Use SDK method for bucket website configuration:
```python
website = WebsiteConfiguration(
indexDocument=IndexDocument(suffix='index.html'),
errorDocument=ErrorDocument(key='error.html') # optional
)
resp = client.setBucketWebsite('bucket-name', website)
```
**⚠️ Breaking Change:**
The older SDK style used keyword arguments such as `setBucketWebsite(bucketName, indexDocumentSuffix=..., errorDocument=...)`. That pattern is **deprecated in `esdk-obs-python >= 3.x`**. The new API requires a `WebsiteConfiguration` object. `indexDocument` must be an `IndexDocument(suffix='...')` object, and `errorDocument` must be an `ErrorDocument(key='...')` object.
### Custom domain registration
If you need a custom domain, you must register it on the OBS bucket in addition to creating the DNS CNAME record:
```python
# Register a custom domain on the bucket (HTTP mode)
resp = client.setBucketCustomDomain('bucket-name', 'www.example.com')
# For HTTPS, provide certificate information
cert_info = {
"name": "cert-name",
"certificate": "-----BEGIN CERTIFICATE-----\n...",
"privateKey": "-----BEGIN RSA PRIVATE KEY-----\n..."
}
resp = client.setBucketCustomDomain('bucket-name', 'www.example.com', certificateInfo=cert_info)
# Query registered custom domains
resp = client.getBucketCustomDomain('bucket-name')
# resp.body == {'domains': [{'domainName': 'www.example.com', 'createTime': '...'}]}
# Delete a custom domain
resp = client.deleteBucketCustomDomain('bucket-name', 'www.example.com')
```
## Minimal flow
1. Create `ObsClient` with AK/SK and OBS endpoint.
2. Create `WebsiteConfiguration` with `IndexDocument` (and optional `ErrorDocument`).
3. Call `client.setBucketWebsite(bucket_name, website)`.
4. If custom domain needed, call `client.setBucketCustomDomain(bucket_name, domain_name)`.
5. Check the HTTP status code (`2xx` expected).
6. Verify the website endpoint with an HTTP GET to the root path.
## Common failures
- `setBucketWebsite` with `unexpected keyword argument 'indexDocumentSuffix'` → use the new `WebsiteConfiguration` object style
- `403`: two common causes must both be considered and reported to the user: missing policy/ACL for anonymous public read, or insufficient AK/SK IAM permissions for website configuration / verification APIs.
- `404`: wrong index document key or file not uploaded.
- DNS mismatch: custom domain CNAME does not point to OBS website endpoint.
- Custom domain not reachable: `setBucketCustomDomain` not called on the bucket (DNS alone is insufficient).
references/verification-method.md›
# Validation Rules
- The website endpoint should follow `BucketName.obs.<region>.myhuaweicloud.com`.
- Public read must be enabled for website files, or the site will return access errors.
- A custom domain must point to the OBS website endpoint with a CNAME record.
- Treat DNS propagation as eventual; the setup is not complete until name resolution works.
- Root path verification and one missing-path check are mandatory.
scripts/set_obs_website_sdk.py›
#!/usr/bin/env python3
import argparse
import os
from pathlib import Path
import sys
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(
description="Set OBS static website hosting via Huawei OBS Python SDK"
)
p.add_argument("bucket_name", help="OBS bucket name")
p.add_argument("endpoint", help="OBS endpoint, e.g. https://obs.<region>.myhuaweicloud.com")
p.add_argument("--index-document", default="index.html", help="Index document key")
p.add_argument("--error-document", default="", help="Error document key (optional)")
p.add_argument(
"--custom-domain",
required=True,
help="Custom domain to register on the bucket (required)",
)
p.add_argument(
"--access-key",
default="",
help="AK (optional; defaults to HW_ACCESS_KEY or ~/.obsutilconfig)",
)
p.add_argument(
"--secret-key",
default="",
help="SK (optional; defaults to HW_SECRET_KEY or ~/.obsutilconfig)",
)
p.add_argument(
"--security-token",
default="",
help="Security token (optional; defaults to HW_SECURITY_TOKEN or ~/.obsutilconfig)",
)
p.add_argument(
"--obsutil-config",
default=str(Path.home() / ".obsutilconfig"),
help="obsutil config file path (default: ~/.obsutilconfig)",
)
return p
def read_obsutil_config(path: str) -> dict[str, str]:
cfg: dict[str, str] = {}
p = Path(path).expanduser()
if not p.exists():
return cfg
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
return cfg
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
key, value = s.split("=", 1)
cfg[key.strip().lower()] = value.strip()
return cfg
def pick_credential(cli_val: str, env_val: str, cfg: dict[str, str], cfg_keys: tuple[str, ...]) -> str:
if cli_val:
return cli_val
if env_val:
return env_val
for key in cfg_keys:
val = cfg.get(key, "")
if val:
return val
return ""
def main() -> int:
parser = build_parser()
args = parser.parse_args()
custom_domain = args.custom_domain.strip()
if not custom_domain:
parser.error("--custom-domain must not be blank")
cfg = read_obsutil_config(args.obsutil_config)
access_key = pick_credential(
args.access_key,
os.getenv("HW_ACCESS_KEY", ""),
cfg,
("ak", "access_key_id"),
)
secret_key = pick_credential(
args.secret_key,
os.getenv("HW_SECRET_KEY", ""),
cfg,
("sk", "secret_access_key"),
)
security_token = pick_credential(
args.security_token,
os.getenv("HW_SECURITY_TOKEN", ""),
cfg,
("securitytoken", "security_token", "token"),
)
missing = []
if not access_key:
missing.append("ak")
if not secret_key:
missing.append("sk")
if missing:
print(
"missing credentials: "
+ ", ".join(missing)
+ ". Fill them in ~/.obsutilconfig (or pass CLI args / env vars).",
file=sys.stderr,
)
print(
"checked sources: --access-key/--secret-key, HW_ACCESS_KEY/HW_SECRET_KEY, and obsutil config file.",
file=sys.stderr,
)
return 2
try:
from obs import ObsClient, WebsiteConfiguration, IndexDocument, ErrorDocument # type: ignore # noqa: E501
except Exception as exc: # noqa: BLE001
print(f"obs sdk not available: {exc}", file=sys.stderr)
print("install with: pip install esdk-obs-python", file=sys.stderr)
return 2
client = ObsClient(
access_key_id=access_key,
secret_access_key=secret_key,
security_token=security_token or None,
server=args.endpoint,
)
try:
# NOTE: esdk-obs-python >= 3.x requires WebsiteConfiguration model objects.
# setBucketWebsite(bucketName, website, extensionHeaders=None)
# where website is a WebsiteConfiguration with IndexDocument/ErrorDocument.
index_doc = IndexDocument(suffix=args.index_document)
if args.error_document:
error_doc = ErrorDocument(key=args.error_document)
website = WebsiteConfiguration(indexDocument=index_doc, errorDocument=error_doc)
else:
website = WebsiteConfiguration(indexDocument=index_doc)
website_resp = client.setBucketWebsite(args.bucket_name, website)
custom_domain_resp = client.setBucketCustomDomain(args.bucket_name, custom_domain)
except Exception as exc: # noqa: BLE001
print(f"OBS configuration failed: {exc}", file=sys.stderr)
return 1
finally:
client.close()
website_status = getattr(website_resp, "status", None)
if website_status is None or not (200 <= int(website_status) < 300):
print(f"setBucketWebsite unexpected status: {website_status}", file=sys.stderr)
return 1
custom_domain_status = getattr(custom_domain_resp, "status", None)
if custom_domain_status is None or not (200 <= int(custom_domain_status) < 300):
print(
f"setBucketCustomDomain unexpected status: {custom_domain_status}",
file=sys.stderr,
)
return 1
print("ok")
return 0
if __name__ == "__main__":
raise SystemExit(main())
scripts/verify_obs_website.py›
#!/usr/bin/env python3
import argparse
import json
import os
from pathlib import Path
import socket
import sys
from datetime import datetime, timezone
import urllib.error
import urllib.parse
import urllib.request
def fetch(url: str) -> tuple[int | None, str, str]:
req = urllib.request.Request(url, method="GET")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.getcode(), "", resp.geturl()
except urllib.error.HTTPError as exc:
return exc.code, f"HTTPError: {exc.reason}", exc.url or url
except Exception as exc: # noqa: BLE001
return None, str(exc), url
def resolve_domain(hostname: str) -> tuple[list[str], str]:
try:
_, _, ips = socket.gethostbyname_ex(hostname)
if not ips:
return [], "no A record resolved"
return sorted(set(ips)), ""
except Exception as exc: # noqa: BLE001
return [], str(exc)
def remediation_for_check(name: str, status: int | None, error: str) -> str:
error_l = (error or "").lower()
if "certificate_verify_failed" in error_l or "hostname mismatch" in error_l:
return (
"TLS certificate mismatch. Bind a valid certificate for this custom domain "
"or front OBS with CDN/ELB and terminate TLS there."
)
if status == 403:
return (
"HTTP 403 has two common causes in this workflow: "
"the bucket/object policy does not allow anonymous public read for website access, "
"or the AK/SK used for OBS SDK checks/configuration lacks required IAM permissions. "
"Verify both the website public-read policy/ACL and IAM actions such as "
"obs:bucket:HeadBucket, obs:bucket:GetBucketLocation, and obs:bucket:PutBucketWebsite."
)
if status == 404:
if name in {"root_path", "index_document"}:
return "Verify index document key/path and confirm website configuration points to the right index."
return "If a custom error page is configured, 404 can be expected; otherwise verify missing-path behavior."
if status == 301 or status == 302:
return "Check whether endpoint/domain is redirecting unexpectedly; use the OBS website endpoint."
if status is None:
return f"Network or DNS error: {error}. Verify domain resolution and endpoint reachability."
if status >= 500:
return "Server-side failure. Retry and check OBS service health/endpoint correctness."
return "Review endpoint, website config, and object paths."
def read_obsutil_config(path: str) -> dict[str, str]:
cfg: dict[str, str] = {}
p = Path(path).expanduser()
if not p.exists():
return cfg
try:
text = p.read_text(encoding="utf-8", errors="ignore")
except Exception:
return cfg
for line in text.splitlines():
s = line.strip()
if not s or s.startswith("#") or "=" not in s:
continue
key, value = s.split("=", 1)
cfg[key.strip().lower()] = value.strip()
return cfg
def pick_credential(
cli_val: str, env_val: str, cfg: dict[str, str], cfg_keys: tuple[str, ...]
) -> str:
if cli_val:
return cli_val
if env_val:
return env_val
for key in cfg_keys:
val = cfg.get(key, "")
if val:
return val
return ""
def run_bucket_region_check(
bucket_name: str,
obs_endpoint: str,
expected_region: str,
access_key: str,
secret_key: str,
security_token: str,
) -> dict[str, object]:
result: dict[str, object] = {
"enabled": True,
"bucket_name": bucket_name,
"obs_endpoint": obs_endpoint,
"expected_region": expected_region,
"actual_region": "",
"head_bucket_status": None,
"get_bucket_location_status": None,
"passed": False,
"error": "",
}
if not access_key or not secret_key:
result["error"] = "missing AK/SK. Set HW_ACCESS_KEY and HW_SECRET_KEY (or pass --access-key/--secret-key)."
return result
try:
from obs import ObsClient # type: ignore
except Exception as exc: # noqa: BLE001
result["error"] = f"OBS SDK not available: {exc}. Install with: pip install esdk-obs-python"
return result
client = ObsClient(
access_key_id=access_key,
secret_access_key=secret_key,
security_token=security_token or None,
server=obs_endpoint,
)
try:
head = client.headBucket(bucket_name)
head_status = int(getattr(head, "status", 0) or 0)
result["head_bucket_status"] = head_status
if not (200 <= head_status < 300):
result["error"] = f"headBucket returned non-2xx status: {head_status}"
return result
location = client.getBucketLocation(bucket_name)
location_status = int(getattr(location, "status", 0) or 0)
result["get_bucket_location_status"] = location_status
if not (200 <= location_status < 300):
result["error"] = f"getBucketLocation returned non-2xx status: {location_status}"
return result
actual_region = str(getattr(getattr(location, "body", None), "location", "") or "")
result["actual_region"] = actual_region
if expected_region and actual_region and actual_region != expected_region:
result["error"] = f"region mismatch: expected={expected_region}, actual={actual_region}"
return result
result["passed"] = True
return result
except Exception as exc: # noqa: BLE001
result["error"] = str(exc)
return result
finally:
client.close()
def main() -> int:
parser = argparse.ArgumentParser(
description="Verify Huawei OBS static website endpoint"
)
parser.add_argument(
"--bucket-name",
required=True,
help="OBS bucket name",
)
parser.add_argument(
"--region",
required=True,
help="OBS region, for example cn-north-4",
)
parser.add_argument(
"--domain",
default="",
help=(
"Custom domain to verify instead of the default OBS website endpoint "
"(optional; host or URL)"
),
)
parser.add_argument(
"--index-document",
default="index.html",
help="Index document key (default: index.html)",
)
parser.add_argument(
"--json",
action="store_true",
help="Output machine-readable JSON report",
)
parser.add_argument(
"--access-key",
default="",
help="AK for optional bucket SDK check (defaults to HW_ACCESS_KEY)",
)
parser.add_argument(
"--secret-key",
default="",
help="SK for optional bucket SDK check (defaults to HW_SECRET_KEY)",
)
parser.add_argument(
"--security-token",
default="",
help="Security token for optional bucket SDK check (defaults to HW_SECURITY_TOKEN or ~/.obsutilconfig)",
)
parser.add_argument(
"--obsutil-config",
default=str(Path.home() / ".obsutilconfig"),
help="obsutil config file path for optional bucket SDK check (default: ~/.obsutilconfig)",
)
args = parser.parse_args()
raw_domain = args.domain.strip()
if raw_domain:
if "://" in raw_domain:
parsed = urllib.parse.urlparse(raw_domain)
else:
parsed = urllib.parse.urlparse(f"http://{raw_domain}")
else:
parsed = urllib.parse.urlparse(
f"http://{args.bucket_name}.obs.{args.region}.myhuaweicloud.com"
)
if not parsed.netloc and parsed.path:
# Handle plain host input like "example.com" parsed into path.
parsed = urllib.parse.urlparse(f"http://{parsed.path}")
host_port = parsed.netloc
if not host_port:
print(f"invalid domain: {args.domain}", file=sys.stderr)
return 2
path_prefix = parsed.path.rstrip("/")
scheme_bases = [("http", f"http://{host_port}{path_prefix}")]
raw_site = scheme_bases[0][1]
checks: list[dict[str, object]] = []
verification_target = "custom_domain" if raw_domain else "default_obs_domain"
for scheme, base in scheme_bases:
checks.extend(
[
{
"scheme": scheme,
"name": "root_path",
"url": f"{base}/",
"expected": "HTTP 200",
"pass_statuses": {200},
},
{
"scheme": scheme,
"name": "index_document",
"url": f"{base}/{args.index_document}",
"expected": "HTTP 200",
"pass_statuses": {200},
},
{
"scheme": scheme,
"name": "missing_path",
"url": f"{base}/nonexistent-path",
"expected": "HTTP 404 or configured custom error page behavior",
"pass_statuses": {404, 200},
"advisory_only": True,
},
]
)
domain = parsed.hostname or ""
dns_ips, dns_error = ([], "")
if domain:
dns_ips, dns_error = resolve_domain(domain)
results: list[dict[str, object]] = []
dns_passed = bool(dns_ips) if domain else True
all_passed = dns_passed
remediation_steps: list[str] = []
bucket_check: dict[str, object] = {"enabled": False}
if not dns_passed:
remediation_steps.append(
f"DNS resolution failed for {domain}: {dns_error}. Verify A/CNAME record and propagation."
)
actions_performed = ["DNS resolution check for endpoint domain"]
obs_endpoint = f"https://obs.{args.region}.myhuaweicloud.com"
cfg = read_obsutil_config(args.obsutil_config)
access_key = pick_credential(
args.access_key,
os.getenv("HW_ACCESS_KEY", ""),
cfg,
("ak", "access_key_id"),
)
secret_key = pick_credential(
args.secret_key,
os.getenv("HW_SECRET_KEY", ""),
cfg,
("sk", "secret_access_key"),
)
security_token = pick_credential(
args.security_token,
os.getenv("HW_SECURITY_TOKEN", ""),
cfg,
("securitytoken", "security_token", "token"),
)
bucket_check = run_bucket_region_check(
bucket_name=args.bucket_name,
obs_endpoint=obs_endpoint,
expected_region=args.region,
access_key=access_key,
secret_key=secret_key,
security_token=security_token,
)
actions_performed.append("OBS SDK read-only check: headBucket + getBucketLocation")
if not bool(bucket_check.get("passed", False)):
all_passed = False
error = str(bucket_check.get("error", "") or "bucket/region check failed")
if "403" in error:
remediation = (
f"Bucket/region check failed: {error}. "
"When troubleshooting 403, tell the user both common possibilities: "
"the bucket/object is not public-read for website access, or the AK/SK lacks "
"required IAM permissions. Verify bucket public-read policy/ACL and IAM actions "
"such as obs:bucket:HeadBucket and obs:bucket:GetBucketLocation."
)
else:
remediation = (
f"Bucket/region check failed: {error}. "
"Verify bucket name, OBS endpoint, region, and IAM permissions."
)
remediation_steps.append(remediation)
for scheme, _base in scheme_bases:
actions_performed.extend(
[
f"HTTP GET root path over {scheme.upper()}",
f"HTTP GET index document over {scheme.upper()}",
f"HTTP GET missing path over {scheme.upper()}",
]
)
for check in checks:
status, error, final_url = fetch(check["url"])
passed = status in check["pass_statuses"]
advisory_only = bool(check.get("advisory_only", False))
if not passed and not advisory_only:
all_passed = False
remediation = remediation_for_check(check["name"], status, error)
if remediation not in remediation_steps:
remediation_steps.append(remediation)
else:
remediation = ""
results.append(
{
"name": check["name"],
"scheme": check["scheme"],
"url": check["url"],
"expected": check["expected"],
"status": status,
"final_url": final_url,
"passed": passed,
"advisory_only": advisory_only,
"error": error,
"remediation": remediation,
}
)
report = {
"input_summary": {
"site_url": raw_site,
"bucket_name": args.bucket_name,
"region": args.region,
"domain_override": raw_domain,
"verification_target": verification_target,
"target_host": host_port,
"checked_schemes": [scheme for scheme, _base in scheme_bases],
"index_document": args.index_document,
"checked_at_utc": datetime.now(timezone.utc).isoformat(),
"domain": domain,
},
"actions_performed": actions_performed,
"verification_results": {
"bucket_region": bucket_check,
"dns": {
"domain": domain,
"resolved_ips": dns_ips,
"passed": dns_passed,
"error": dns_error,
},
"http_checks": results,
"overall_passed": all_passed,
},
"remediation_steps": remediation_steps,
}
if args.json:
print(json.dumps(report, ensure_ascii=True, indent=2))
else:
print("Input summary:")
print(f"- site_url: {report['input_summary']['site_url']}")
print(f"- bucket_name: {report['input_summary']['bucket_name']}")
print(f"- region: {report['input_summary']['region']}")
print(f"- domain_override: {report['input_summary']['domain_override'] or 'none'}")
print(f"- verification_target: {report['input_summary']['verification_target']}")
print(f"- target_host: {report['input_summary']['target_host']}")
print(f"- checked_schemes: {','.join(report['input_summary']['checked_schemes'])}")
print(f"- index_document: {report['input_summary']['index_document']}")
print(f"- checked_at_utc: {report['input_summary']['checked_at_utc']}")
print(f"- domain: {report['input_summary']['domain']}")
print()
print("Actions performed:")
for action in report["actions_performed"]:
print(f"- {action}")
print()
print("Verification results:")
bucket_region_report = report["verification_results"]["bucket_region"]
if bucket_region_report.get("enabled"):
if bucket_region_report.get("passed"):
print(
"- bucket_region: PASS "
f"(bucket={bucket_region_report['bucket_name']}; "
f"expected_region={bucket_region_report['expected_region'] or 'n/a'}; "
f"actual_region={bucket_region_report['actual_region'] or 'unknown'})"
)
else:
print(
"- bucket_region: FAIL "
f"({bucket_region_report.get('error', 'unknown error')})"
)
dns_report = report["verification_results"]["dns"]
if dns_report["passed"]:
print(f"- dns: PASS (resolved_ips={','.join(dns_report['resolved_ips'])})")
else:
print(f"- dns: FAIL ({dns_report['error']})")
for item in report["verification_results"]["http_checks"]:
status_text = "PASS" if item["passed"] else "FAIL"
observed = item["status"] if item["status"] is not None else f"ERROR ({item['error']})"
print(
f"- {item['scheme']} {item['name']}: {status_text} "
f"(expected: {item['expected']}; observed: {observed}; url: {item['url']})"
)
print(f"- overall: {'PASS' if report['verification_results']['overall_passed'] else 'FAIL'}")
print()
if remediation_steps:
print("Remediation steps:")
for step in remediation_steps:
print(f"- {step}")
else:
print("Remediation steps:")
print("- none")
return 0 if all_passed else 1
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md›
---
name: huawei-cloud-obs-website-host
description: Configure Huawei Cloud OBS static website hosting with Python SDK and a custom domain. Use when the user needs to enable or repair OBS website hosting, set index or error pages, expose an existing bucket for public website access through a custom domain, or connect that domain through Huawei Cloud DNS when Huawei manages the zone. Triggers include OBS static website hosting, website endpoint, index page, error page, public-read bucket website access, custom domain CNAME, Huawei Cloud DNS, setBucketWebsite. 中文触发词包括:OBS 静态网站托管、网站托管、自定义域名解析。
---
# Huawei Cloud OBS Website Host
## Overview
Configure an existing Huawei Cloud OBS bucket for static website hosting with Huawei Cloud OBS Python SDK and register a custom domain for the site.
Use this skill when the user wants to:
- enable or repair OBS static website hosting
- set an index document or error document
- make the site reachable through a custom domain backed by the OBS static website endpoint
- add or repair a custom domain, including Huawei Cloud DNS when applicable
- diagnose 403, 404, or DNS issues on a hosted OBS site
## What Good Looks Like
- The bucket has static website hosting enabled.
- The bucket can serve `index.html` from the website endpoint.
- Anonymous users can read the website content.
- A missing path returns the configured error page or a clean 404.
- A custom domain is registered and resolves to the OBS website endpoint through DNS. A custom domain is **mandatory** — the default OBS bucket domain is not permitted for online object preview per security compliance requirements.
- The setup is not considered complete until the custom domain is registered on the bucket and resolves correctly.
- The OBS website endpoint is used, not the regular bucket API endpoint.
- A 403 usually has two common causes: anonymous/public read is not enabled on the bucket or objects, or the AK/SK used for OBS operations lacks required IAM permissions.
- A 404 usually means the index document name or upload path is wrong.
## Security Compliance: Custom Domain Requirement
Based on Huawei Cloud security compliance requirements, the OBS bucket default domain name (`<bucket_name>.obs.<region>.myhuaweicloud.com`) is **prohibited** from being used for online preview of objects within the bucket. A custom domain is therefore **mandatory** for static website hosting.
If the user does not have a custom domain prepared:
1. Direct the user to register a domain through the [Huawei Cloud Domain Registration Service](https://www.huaweicloud.com/product/domain.html), or other common domain registration sites.
2. For users in mainland China, the domain must also complete **ICP filing (网站备案)** before it can be used for website hosting.
3. Only after the domain is registered (and filed, if applicable) should the static website hosting configuration continue.
> **Important:** Do not proceed with static website hosting configuration until the custom domain prerequisite is confirmed. The default OBS domain is not a valid alternative for website access even in the testing environment.
## Required Inputs
Collect these before making changes:
- `region`
- `bucket_name`
- `custom_domain` (**required** — see Security Compliance section above)
- `index_document` (optional, default: `index.html`)
- `error_document` (optional)
- `dns_zone` or DNS account context (optional; required only if the user wants Huawei Cloud DNS changes in this run)
Assume static website files are already uploaded by the user.
## Dependencies
The skill depends on the following runtime/tooling components:
- Python 3.8+ (required for `scripts/set_obs_website_sdk.py` and `scripts/verify_obs_website.py`)
- Huawei OBS Python SDK package: `esdk-obs-python`
- `obsutil` (for generating and maintaining `.obsutilconfig` credential config)
- Huawei Cloud AK/SK credentials (from `.obsutilconfig`)
- Network access to OBS endpoint and website endpoint
- `hcloud` CLI (required only when this skill manages Huawei Cloud DNS record operations)
Install command:
```bash
pip install esdk-obs-python
```
## hcloud CLI Reference
Load `references/cli-installation-guide.md` when hcloud CLI or obsutil installation and configuration is needed.
Load `references/hcloud-dns-obs-website.md` when creating or managing DNS CNAME records for OBS static website custom domains (step-by-step guide with hcloud `DNS CreateRecordSet` commands).
Security note:
- Never hardcode AK/SK in scripts or checked-in files.
- Prefer environment variables for SDK scripts and secure local profile storage for CLI use.
## obsutil Config Dependency
Load `references/cli-installation-guide.md` when you need obsutil installation or `.obsutilconfig` setup guidance.
The Python SDK helper script (`scripts/set_obs_website_sdk.py`) reads credentials by default from:
1. CLI flags (`--access-key`, `--secret-key`, `--security-token`)
2. Environment variables (`HW_ACCESS_KEY`, `HW_SECRET_KEY`, `HW_SECURITY_TOKEN`)
3. `.obsutilconfig`
If `ak`/`sk` are empty across all sources, the script must stop and ask the user to fill missing keys in `.obsutilconfig` (or provide CLI/env credentials).
Credential check rule:
- Only report presence/absence of keys (`ak`, `sk`, `securitytoken`).
- Never print credential values during checks.
- Never print full lines from `.obsutilconfig` to console.
- Treat console output as model context; any leaked value is a security incident.
Safe check examples (status only, no secret values):
Linux/macOS:
```bash
CFG="${HOME}/.obsutilconfig"
if [ ! -f "$CFG" ]; then
echo "obsutilconfig_exists=false"
echo "ak_configured=false"
echo "sk_configured=false"
echo "securitytoken_configured=false"
else
awk -F= '
BEGIN { ak=0; sk=0; st=0 }
/^[[:space:]]*#/ { next }
/^[[:space:]]*(ak|access_key_id)[[:space:]]*=/ { if ($2 ~ /[^[:space:]]/) ak=1 }
/^[[:space:]]*(sk|secret_access_key)[[:space:]]*=/ { if ($2 ~ /[^[:space:]]/) sk=1 }
/^[[:space:]]*(securitytoken|security_token|token)[[:space:]]*=/ { if ($2 ~ /[^[:space:]]/) st=1 }
END {
print "obsutilconfig_exists=true"
print "ak_configured=" (ak ? "true" : "false")
print "sk_configured=" (sk ? "true" : "false")
print "securitytoken_configured=" (st ? "true" : "false")
}
' "$CFG"
fi
```
Windows (PowerShell):
```powershell
$cfg = Join-Path $HOME ".obsutilconfig"
if (-not (Test-Path $cfg)) {
"obsutilconfig_exists=false"
"ak_configured=false"
"sk_configured=false"
"securitytoken_configured=false"
} else {
$lines = Get-Content $cfg
$ak = $false; $sk = $false; $st = $false
foreach ($line in $lines) {
if ($line -match '^\s*#') { continue }
if ($line -match '^\s*(ak|access_key_id)\s*=\s*(\S.*)$') { $ak = $true }
if ($line -match '^\s*(sk|secret_access_key)\s*=\s*(\S.*)$') { $sk = $true }
if ($line -match '^\s*(securitytoken|security_token|token)\s*=\s*(\S.*)$') { $st = $true }
}
"obsutilconfig_exists=true"
"ak_configured=$ak"
"sk_configured=$sk"
"securitytoken_configured=$st"
}
```
Do not use:
- `cat ~/.obsutilconfig`
- `grep -E "ak|sk|token" ~/.obsutilconfig`
## Script Usage Intent
Use the bundled scripts by default for the tasks they were built for:
- `scripts/set_obs_website_sdk.py` applies or updates the bucket website configuration and registers the required custom domain. Use it whenever the task is to enable, repair, or change OBS static website hosting settings.
- `scripts/verify_obs_website.py` validates the published website endpoint. Use it after any website configuration change, and also when the user asks whether the site is reachable or when troubleshooting 403/404 behavior.
- Do not replace these scripts with ad hoc one-off code unless the script itself is broken and must be patched.
- Use the scripts to keep credential handling, SDK object construction, and verification behavior consistent across runs.
## Workflow
1. Verify Python runtime and OBS SDK are available (`pip install esdk-obs-python` if missing).
2. Verify the custom domain prerequisite (see **Security Compliance** section):
- Confirm `custom_domain` is provided by the user.
- If the user does not have a domain, guide them to register one at [Huawei Cloud Domain Registration](https://www.huaweicloud.com/product/domain.html) and complete **ICP filing (网站备案)** for mainland China regions. Stop here and wait for the user to complete this step.
- Check whether the user manages DNS in Huawei Cloud DNS or with an external provider.
- If Huawei Cloud DNS changes are part of this run, verify `hcloud` is installed and authenticated.
- If DNS is managed outside Huawei Cloud or outside this run, collect that constraint explicitly before proceeding.
3. Verify the bucket exists in the requested region (use **Bucket Existence and Region Check Method** below).
4. Check that the caller has permission to update bucket website settings.
5. Check that anonymous read is allowed for the website files (use the method in **Anonymous Read Check Method** below).
6. Do not upload or modify website content objects (`index.html`, assets, etc.). Assume content already exists in the bucket.
7. Configure static website hosting by running `scripts/set_obs_website_sdk.py` with `--custom-domain <domain>` (use `index.html` if `index_document` is not provided).
- The script exists to keep SDK object construction and credential lookup consistent.
- Use it instead of writing a one-off SDK call in the response.
8. Register the required custom domain on the bucket via the OBS SDK path used by the script:
- `client.setBucketCustomDomain(bucket_name, custom_domain)` — required even if DNS CNAME already exists.
- If DNS record changes are requested in this run, create a DNS CNAME record to the OBS website hostname and wait for propagation. (read `references/hcloud-dns-obs-website.md`)
- If DNS is managed outside Huawei Cloud or outside this run, provide the required CNAME target and explicitly instruct the user to create or update the CNAME record with their external DNS provider after OBS custom-domain registration is complete.
- For externally managed DNS, include the practical handoff details the user needs: record type `CNAME`, host/name, target/value, and a verification command such as `dig`.
9. Verify the published site by running `scripts/verify_obs_website.py --bucket-name <bucket_name> --region <region> [--domain <custom_domain>] [--index-document <name>]`.
- If the user provided a custom domain, final verification MUST use that custom domain via `--domain <custom_domain>`.
- Only use the default OBS hostname for interim checks or when no custom domain was provided.
10. Confirm the root path returns the homepage (HTTP 200).
11. Confirm a missing path returns the configured error behavior (HTTP 404 or configured error page).
12. Verify DNS resolution (`dig` / `nslookup`) and HTTP access through the user-provided custom domain. Do not treat the setup as complete based only on the default OBS hostname when a custom domain is part of the request.
## Bucket Existence and Region Check Method
Run a read-only SDK check with `verify_obs_website.py` before website configuration.
```bash
python scripts/verify_obs_website.py \
--bucket-name "<bucket_name>" \
--region "<region>" \
--index-document "<index_document>"
```
`obs endpoint` is auto-built as `https://obs.<region>.myhuaweicloud.com`.
Pass/Fail rules:
- `PASS`: `headBucket` is `2xx` and region matches (or region cannot be returned but bucket is reachable with `2xx`).
- `FAIL`: `headBucket` non-`2xx`, `getBucketLocation` non-`2xx`, or explicit region mismatch.
## Anonymous Read Check Method
Use anonymous HTTP requests against the OBS website endpoint (no AK/SK) as the source of truth.
1. The verifier auto-builds the default website URL:
- `http://<bucket_name>.obs.<region>.myhuaweicloud.com`
2. Run bundled verifier (preferred):
```bash
python scripts/verify_obs_website.py \
--bucket-name "<bucket_name>" \
--region "<region>" \
--domain "<custom_domain>" \
--index-document "<index_document>"
```
3. If no custom domain was provided by the user, verify the default OBS website endpoint instead:
```bash
python scripts/verify_obs_website.py \
--bucket-name "<bucket_name>" \
--region "<region>" \
--index-document "<index_document>"
```
4. If you need a quick single-file check, run:
```bash
site_url="http://<custom_domain>"
curl -s -o /dev/null -w "%{http_code}\n" "$site_url/<index_document>"
```
Pass/Fail rules:
- `200` on `root_path` and `index_document`: anonymous read is working.
- `403`: treat as two possible issues that must both be reported to the user: anonymous/public read is not enabled (ACL/policy issue), or the AK/SK used for SDK verification/configuration lacks required IAM permissions.
- `404`: object path/name issue (for example, `index.html` missing or key path mismatch), not an anonymous-permission success.
When `403` appears, treat setup as failed and tell the user both common possibilities:
- bucket/object is not public-read for website access
- AK/SK lacks required IAM permissions for OBS operations
Provide remediation via `references/iam-policies.md`.
## Response Shape
Always return:
1. Input summary
2. Actions performed
3. Verification results
4. Remediation steps if anything failed
When DNS is externally managed, also include a short DNS handoff section that tells the user exactly which CNAME record to configure with their provider.
## Safety Rules
- Never print secrets, AK/SK, or tokens.
- Do not claim success until the website endpoint is verified.
- If the user provided a custom domain, final success must be based on verification through that custom domain, not only the default OBS hostname.
- If permissions are missing, stop and report the missing capability.
- If DNS provider ownership is unspecified, ask whether the zone is managed in Huawei Cloud DNS or externally before assuming `hcloud` steps.
- If Huawei Cloud DNS changes are required for completion but the zone is unknown, ask for the zone instead of guessing.
- Do not use the regular bucket endpoint as the final website result.
- If the bucket name contains dots, warn that HTTPS access can be problematic.
- `obsutil` is allowed only for managing `~/.obsutilconfig`; do not use it to configure website hosting.
- Do not perform any object upload actions in this skill.
- Especially during verification, use read-only checks only; never upload test files.
- For externally managed DNS, do not stop at “DNS is external”; provide the user-facing CNAME handoff details needed to finish the setup.
## Permission Failure Handling (MUST)
When any command fails due to IAM permission errors:
1. Read `references/iam-policies.md`.
2. Show the required permission list and policy JSON to the user.
3. Guide the user to create a custom IAM policy and grant it in Huawei Cloud IAM console.
4. Pause execution and wait for user confirmation that permissions were granted.
## References
Load `references/obs-python-sdk-website.md` for SDK method usage for website hosting **and custom domain registration** (`setBucketCustomDomain`).
Load `references/iam-policies.md` for required IAM actions and policy JSON.
Load `references/hcloud-dns-obs-website.md` for step-by-step DNS CNAME configuration for custom domains via Huawei Cloud DNS (`hcloud` CLI), including zone lookup, record creation, and verification.
> **Known Pitfall:** The `setBucketWebsite` API in esdk-obs-python >= 3.x uses `WebsiteConfiguration` model objects, **not** keyword arguments like `indexDocumentSuffix`. Always import `WebsiteConfiguration`, `IndexDocument`, and `ErrorDocument` and construct them properly.
## Scripts
Use scripts only for repeatable checks and verification. Keep command output human-readable and focused on success/failure.
- `scripts/set_obs_website_sdk.py <bucket_name> <endpoint> --custom-domain <domain> [--index-document <name>] [--error-document <name>]` applies static website hosting settings through the OBS SDK, registers the required custom domain, and reads credentials from CLI args, env vars, or `~/.obsutilconfig`.
- `scripts/verify_obs_website.py --bucket-name <name> --region <region> [--domain <custom_domain>] [--index-document <name>] [--json]` verifies endpoint DNS/HTTP behavior and also performs a read-only bucket existence + region check (`headBucket` + `getBucketLocation`). If `--domain` is provided, that custom domain is the final verification target; otherwise it auto-builds the default website URL as `http://<bucket>.obs.<region>.myhuaweicloud.com`. The OBS API endpoint remains `https://obs.<region>.myhuaweicloud.com`. It prints structured sections (`Input summary`, `Actions performed`, `Verification results`, `Remediation steps`) so agent responses can directly reuse them.
## Validation Rules
Load `references/verification-method.md` for validation rules.