
Huawei Cloud Obs Website Host
- 93 installs
- 19 repo stars
- Updated July 31, 2026
- huaweicloud/huaweicloud-skills
Configure Huawei Cloud OBS static website hosting: enable hosting, set index/error pages, allow public read, and connect a custom domain via DNS.
About
Configures an existing Huawei Cloud OBS bucket for static website hosting using the OBS Python SDK, setting index/error documents, public-read access, and custom-domain CNAME via Huawei Cloud DNS. A developer uses it to serve a static site from OBS and diagnose 403/404/DNS issues.
- Enables hosting with index/error pages and public read
- Custom-domain DNS setup and 403/404 diagnosis
Huawei Cloud Obs Website Host by the numbers
- 93 all-time installs (skills.sh)
- +14 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #578 of 1,042 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/huaweicloud/huaweicloud-skills --skill huawei-cloud-obs-website-hostAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 93 |
|---|---|
| repo stars | ★ 19 |
| Last updated | July 31, 2026 |
| Repository | huaweicloud/huaweicloud-skills ↗ |
What it does
Configure Huawei Cloud OBS static website hosting: enable hosting, set index/error pages, allow public read, and connect a custom domain via DNS.
Files
Huawei Cloud OBS Website Host
Overview
Configure an existing Huawei Cloud OBS bucket for static website hosting with Huawei Cloud OBS Python SDK.
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 the OBS static website endpoint
- add a custom domain with Huawei Cloud DNS
- 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.htmlfrom 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 resolves to the OBS website endpoint through DNS.
- A custom domain is strongly recommended; using the default OBS domain in a browser may download web assets as attachments instead of rendering them inline.
- The OBS website endpoint is used, not the regular bucket API endpoint.
- A 403 usually means anonymous access or bucket policy is missing.
- A 404 usually means the index document name or upload path is wrong.
Required Inputs
Collect these before making changes:
regionbucket_nameindex_document(optional, default:index.html)error_document(optional)custom_domain(optional)dns_zoneor DNS account context (optional, only if the user wants DNS changes)
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.pyandscripts/verify_obs_website.py) - Huawei OBS Python SDK package:
esdk-obs-python obsutil(for generating and maintaining.obsutilconfigcredential config)- Huawei Cloud AK/SK credentials (from
.obsutilconfig) - Network access to OBS endpoint and website endpoint
hcloudCLI (optional for DNS helper steps, especially Huawei Cloud DNS record operations)
Install command:
pip install esdk-obs-pythonhcloud CLI Reference
Load references/hcloud-install-config.md when hcloud CLI installation or AK/SK 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/obsutil-install-config.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
.obsutilconfigto console. - Treat console output as model context; any leaked value is a security incident.
Safe check examples (status only, no secret values):
Linux/macOS:
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"
fiWindows (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 ~/.obsutilconfiggrep -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.pyapplies or updates the bucket website configuration. Use it whenever the task is to enable, repair, or change OBS static website hosting settings.scripts/verify_obs_website.pyvalidates 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. If the user requests a custom domain and DNS changes, verify hcloud is installed and authenticated. If no custom domain/DNS change is needed, do not treat hcloud as a blocker. 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 (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. If a custom domain is provided, register it 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 endpoint and wait for propagation. (read
references/hcloud-dns-obs-website.md) - If DNS is managed outside this run, treat DNS creation as an external prerequisite instead of failing website-hosting configuration steps.
9. Verify the published site by running scripts/verify_obs_website.py <site_url> [--index-document <name>]. 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. For a custom domain, verify DNS resolution (dig / nslookup) and HTTP access through the custom domain.
Bucket Existence and Region Check Method
Run a read-only SDK check with verify_obs_website.py before website configuration.
python scripts/verify_obs_website.py "<site_url>" \
--bucket-name "<bucket_name>" \
--expected-region "<region>" \
--index-document "<index_document>"obs endpoint is auto-built as https://obs.<region>.myhuaweicloud.com.
Pass/Fail rules:
PASS:headBucketis2xxand region matches (or region cannot be returned but bucket is reachable with2xx).FAIL:headBucketnon-2xx,getBucketLocationnon-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. Build website URL:
site_url="http://<bucket_name>.obs-website.<endpoint>"
2. Run bundled verifier (preferred):
python scripts/verify_obs_website.py "$site_url" --index-document "<index_document>"3. If you need a quick single-file check, run:
curl -s -o /dev/null -w "%{http_code}\n" "$site_url/<index_document>"Pass/Fail rules:
200onroot_pathandindex_document: anonymous read is working.403: anonymous read is not enabled (ACL/policy issue).404: object path/name issue (for example,index.htmlmissing or key path mismatch), not an anonymous-permission success.
When 403 appears, treat setup as failed and 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
Safety Rules
- Never print secrets, AK/SK, or tokens.
- Do not claim success until the website endpoint is verified.
- If permissions are missing, stop and report the missing capability.
- If DNS is requested 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.
obsutilis 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.
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: ThesetBucketWebsiteAPI in esdk-obs-python >= 3.x usesWebsiteConfigurationmodel objects, not keyword arguments likeindexDocumentSuffix. Always importWebsiteConfiguration,IndexDocument, andErrorDocumentand 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> [--index-document <name>] [--error-document <name>] [--custom-domain <domain>]applies static website hosting settings through the OBS SDK and reads credentials from CLI args, env vars, or~/.obsutilconfig.scripts/verify_obs_website.py <site_url> [--index-document <name>] [--json] [--bucket-name <name> --expected-region <region>]verifies endpoint DNS/HTTP behavior and can also perform a read-only bucket existence + region check (headBucket+getBucketLocation). It auto-builds OBS API endpoint ashttps://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
- The website endpoint should follow
BucketName.obs-website.Endpoint. - Public read must be enabled for website files, or the site will return access errors.
- A custom domain should 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.
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
hcloudCLI installed and configured with AK/SK credentials (seereferences/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:
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:
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:
hcloud DNS CreateRecordSet \
--zone_id="<zone_id>" \
--name="<custom_domain>." \
--type="CNAME" \
--records.1="<bucket_name>.obs-website.<region>.myhuaweicloud.com." \
--cli-region=<region> \
--ttl=300Parameters:
| 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-website.cn-north-4.myhuaweicloud.com. |
--cli-region | Region | Region where the DNS API is called |
--ttl | 300 (recommended) | Time-to-live in seconds |
Example:
hcloud DNS CreateRecordSet \
--zone_id="ff8080828fb6d17b018fbd5a2fac1d7f" \
--name="www.example.com." \
--type="CNAME" \
--records.1="my-bucket.obs-website.cn-north-4.myhuaweicloud.com." \
--cli-region=cn-north-4 \
--ttl=3004. Verify DNS Resolution
Check that the CNAME record resolves correctly:
dig +short <custom_domain> CNAMEExpected output:
<bucket_name>.obs-website.<region>.myhuaweicloud.com.Important Notes
- Trailing dot: Both the
--nameand--records.1values 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(seereferences/obs-python-sdk-website.md). Use the--custom-domainflag ofscripts/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 |
hcloud Install and Configure
Use this reference when you need to install or configure KooCLI (hcloud).
Linux
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 -yInteractive install:
bash ./hcloud_install.shWindows
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:
hcloud versionConfigure
Interactive init:
hcloud configure initAK/SK mode:
hcloud configure set --cli-profile=default --cli-mode=AKSK --cli-region=<region> --cli-access-key=<ak> --cli-secret-key=<sk>Verify current profile:
hcloud version
hcloud configure listIAM 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
{
"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.pywith--bucket-name/--obs-endpoint,obs:bucket:HeadBucketandobs:bucket:GetBucketLocationare 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.
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:
from obs import ObsClient, WebsiteConfiguration, IndexDocument, ErrorDocumentRequired action
Use SDK method for bucket website configuration:
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:
# 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
setBucketWebsitewithunexpected keyword argument 'indexDocumentSuffix'→ use the newWebsiteConfigurationobject style403: missing policy/ACL for anonymous read or missing permissions for website config.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:
setBucketCustomDomainnot called on the bucket (DNS alone is insufficient).
obsutil Install and Config
Use this reference when you need to install obsutil or prepare .obsutilconfig.
Config File Location
obsutil auto-generates a config file named .obsutilconfig in user home directory:
- macOS/Linux:
~/.obsutilconfig - Windows:
C:\\Users\\<username>\\.obsutilconfig
Install on Linux AMD64 (x86_64)
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 versionInstall on Linux ARM64
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 versionInstall on macOS (AMD64)
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 versionInstall on 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:
obsutil.exe versionGenerate config file
# generate config file
./obsutil configWindows:
obsutil.exe configSecure 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 ~/.obsutilconfiggrep -E "ak|sk|token" ~/.obsutilconfig
Notes
- Internet connectivity is required when downloading packages.
chmod 755 obsutilis required before runningobsutil.- If
./obsutil versionreturns version information, installation is successful. - For macOS, run
chmod 755 obsutilin the extracted directory before first use.
#!/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",
default="",
help="Custom domain to register on the bucket (optional)",
)
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()
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,
)
custom_domain = args.custom_domain.strip()
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 = None
if custom_domain:
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
if custom_domain:
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())
#!/usr/bin/env python3
import argparse
import json
import os
from pathlib import Path
import re
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 "Enable anonymous read for website objects and verify bucket policy/public ACL."
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 infer_region_from_site_url(site_url: str) -> str:
host = urllib.parse.urlparse(site_url if "://" in site_url else f"http://{site_url}").hostname or ""
# Typical OBS website endpoint: <bucket>.obs-website.<region>.myhuaweicloud.com
m = re.search(r"\.obs-website\.([a-z0-9-]+)\.myhuaweicloud\.com$", host)
if not m:
return ""
return m.group(1)
def main() -> int:
parser = argparse.ArgumentParser(
description="Verify Huawei OBS static website endpoint"
)
parser.add_argument("site_url", help="OBS website endpoint 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(
"--bucket-name",
default="",
help="OBS bucket name for read-only bucket existence/region check (optional)",
)
parser.add_argument(
"--expected-region",
default="",
help="Expected OBS region for bucket location check (optional)",
)
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()
if args.expected_region and not args.bucket_name:
parser.error("--expected-region requires --bucket-name")
raw_site = args.site_url.strip()
if "://" in raw_site:
parsed = urllib.parse.urlparse(raw_site)
else:
parsed = urllib.parse.urlparse(f"http://{raw_site}")
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 site_url: {args.site_url}", file=sys.stderr)
return 2
path_prefix = parsed.path.rstrip("/")
scheme_bases = [("http", f"http://{host_port}{path_prefix}")]
checks: list[dict[str, object]] = []
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"]
if args.bucket_name:
region = args.expected_region.strip()
if not region:
region = infer_region_from_site_url(raw_site)
if not region:
parser.error(
"bucket check requires --expected-region or a parseable OBS website site_url "
"(<bucket>.obs-website.<region>.myhuaweicloud.com)"
)
obs_endpoint = f"https://obs.{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=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")
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,
"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"- 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())