diff --git a/Makefile b/Makefile index 6c72f05..4aa7255 100644 --- a/Makefile +++ b/Makefile @@ -270,6 +270,16 @@ verify: ## Run Goss test suite against all hosts, commit TAP reports — exits n git diff --cached --quiet && echo "No new reports to commit." || \ git commit -m "chore: Goss verification reports $$(date -u +%Y-%m-%dT%H%M%SZ)" +observe-railiance01: ## Timestamped host capacity observation for resource-control + @mkdir -p docs/evidence/resource-hosteurope-railiance01/observations + @out=$$(mktemp); \ + python3 scripts/observe-host-capacity.py Railiance01 > $$out; \ + stamp=$$(python3 -c "import json; print(json.load(open('$$out'))['observed_at'].replace(':',''))"); \ + dest=docs/evidence/resource-hosteurope-railiance01/observations/$$stamp.json; \ + mv $$out $$dest; \ + ln -sfn $$stamp.json docs/evidence/resource-hosteurope-railiance01/observations/latest.json; \ + echo "wrote $$dest" + verify-host: ## Run Goss against one host: make verify-host HOST=Railiance01 @test -n "$(HOST)" || (echo "Usage: make verify-host HOST=Railiance01"; exit 1) @echo "Running Goss baseline assertions on $(HOST)..." diff --git a/SCOPE.md b/SCOPE.md index 6a4d122..5282db9 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -89,6 +89,9 @@ higher layer (Kubernetes, platform, etc.) can run. - **Firewall posture**: `RAIL-HO-WP-0009` finished. k3s API is tunnel-only (ADR-005); live `Railiance01` UFW matches the declaration. CoulombCore UFW stays unmanaged (`ufw_manage: false`) +- **Resource evidence**: `RAIL-HO-WP-0008` finished. Non-secret identity, + capacity observations, and host-ops labor live under + `docs/evidence/resource-hosteurope-railiance01/` --- diff --git a/docs/evidence/resource-hosteurope-railiance01/README.md b/docs/evidence/resource-hosteurope-railiance01/README.md new file mode 100644 index 0000000..21fff2f --- /dev/null +++ b/docs/evidence/resource-hosteurope-railiance01/README.md @@ -0,0 +1,63 @@ +# Evidence interface — `resource:hosteurope:railiance01` + +Stable, non-secret facts that `resource-control` should read for the Host +Europe virtual server `Railiance01`. This directory is the interface. +`resource-control` remains the portfolio record; `fin-hub` remains booked +cost. Do not copy invoices or credentials into either side. + +## Files + +| Path | Task | What it fills | +| --- | --- | --- | +| `identity-lifecycle.yaml` | T01 | provider, product, IDs, location, lifecycle, classified unknowns | +| `observations/*.json` | T02 | timestamped OS / provider / k3s capacity and utilization | +| `observations/latest.json` | T02 | symlink to the newest observation | +| `labor-and-exit.yaml` | T03 | recurring host-ops activities, estimated hours, exit path | + +Producer: `scripts/observe-host-capacity.py Railiance01` + +```bash +make observe-railiance01 +``` + +Recommended cadence: monthly, and after any provider resize. Traffic +allowance is still unknown (operator / panel). + +## How to fold into resource-control + +Map, do not duplicate operating strings into a shadow inventory: + +| Inventory field | Source | +| --- | --- | +| `provider.name` | Host Europe GmbH | +| `provider.product_ref` | `oh.hosting.c2.r4.d100` | +| `provider.provider_resource_id` | `c0e697d1-4c71-40f5-9746-0b4c4b9e5c17` | +| `provider.account_ref` | `openstack-project:f6491e4f796146d1a7ce30d569d5f122` | +| `location.country` | `FR` | +| `location.region` | `sxb1` | +| `location.residency` | `EU` | +| `lifecycle.commissioned_on` | `2026-03-08` | +| `lifecycle.ordered_on` / `renews_on` / `cancel_by` | remain `null` (unknown, operator) | +| `cost.*` booked price | remain unknown; owner `fin-hub` / `FIN-WP-0004` | +| `capacity[]` | latest observation, kinds `usable` / `provisioned` / `observed` | +| labor hours in forecasts | `labor-and-exit.yaml#monthly_host_ops_hours` (basis `estimated`) | + +Cluster utilization and PVC allocation stay `RAIL-BS-WP-0014`. + +## Layers in an observation + +1. **provider_declared** — instance type and UUIDs from named cloud-init keys. +2. **operating_system** — what the kernel sees (CPU, memory, root filesystem). +3. **kubernetes** — k3s capacity, allocatable, and `kubectl top` when present. + +Do not treat k3s allocatable as the provider grant. On 2026-08-15 they +happened to match for CPU and memory; ephemeral-storage allocatable is +smaller than the root filesystem. + +## What is still unknown + +- Host Europe order date, renewal date, cancellation deadline, contract term +- Billing customer number (restricted) +- Traffic allowance +- Booked recurring price and tax treatment (`fin-hub`) +- Measured operator hours (no time records; estimates only) diff --git a/docs/evidence/resource-hosteurope-railiance01/identity-lifecycle.yaml b/docs/evidence/resource-hosteurope-railiance01/identity-lifecycle.yaml new file mode 100644 index 0000000..3d582d8 --- /dev/null +++ b/docs/evidence/resource-hosteurope-railiance01/identity-lifecycle.yaml @@ -0,0 +1,139 @@ +# Non-secret identity and lifecycle for resource:hosteurope:railiance01 +# Consumer: resource-control inventory fields on that resource. +# Booked price, invoices, customer numbers, and payment instruments are +# out of scope (fin-hub / operator). Unknowns stay unknown. + +schema_version: "1.0" +resource_id: resource:hosteurope:railiance01 +inventory_name: Railiance01 +public_ipv4: 92.205.62.239 +status: active +management_model: provider_managed +resource_class: compute_instance +published_at: "2026-08-15" +authority: railiance-infra +workplan: RAIL-HO-WP-0008-T01 + +provider: + name: Host Europe GmbH + as_number: AS21499 + parent: GoDaddy + product_class: virtual server + product_family: vps4 + product_ref: + value: oh.hosting.c2.r4.d100 + basis: measured + source: cloud-init ds.ec2_metadata.instance-type + note: Provider instance-type string. Not a booked catalogue confirmation. + provider_resource_id: + value: c0e697d1-4c71-40f5-9746-0b4c4b9e5c17 + basis: measured + source: cloud-init ds.meta_data.uuid (OpenStack Nova) + provider_instance_id_ec2: + value: i-000197c7 + basis: measured + source: cloud-init ds.ec2_metadata.instance-id + instance_name: + value: 1fxw7h4zug + basis: measured + source: cloud-init ds.meta_data.name + account_ref: + value: openstack-project:f6491e4f796146d1a7ce30d569d5f122 + basis: measured + source: cloud-init ds.meta_data.project_id + classification: non-secret-join-key + note: Cloud tenant UUID for joins. Not the billing customer number. + billing_customer_number: + value: null + classification: restricted + owner: operator + source: Host Europe control panel + provisioning: + value: manual + basis: quoted + source: resource-control/docs/evidence/RESOURCE-WP-0002-host-europe-resolution-2026-08-15.md + note: Host Europe has no API to provision this class of resource. + +location: + country: + value: FR + basis: derived + sources: + - cloud-init local-hostname contains sxb1 (Strasbourg site code) + - ipinfo.io 2026-08-15 city=Strasbourg country=FR org=AS21499 Host Europe GmbH + - Host Europe Group operates a Strasbourg data centre + region: + value: sxb1 + basis: measured + source: cloud-init ds.ec2_metadata.local-hostname = 1fxw7h4zug.cloud.prd.sxb1.gdg + locality: + value: Strasbourg + basis: derived + source: sxb1 + ipinfo.io + residency: + value: EU + basis: derived + source: country FR + failure_domains: + - provider:host-europe + - site:sxb1 + - host:railiance01 + ptr: 239.62.205.92.host.secureserver.net + +lifecycle: + proposed_on: + value: null + classification: unknown + owner: operator + ordered_on: + value: null + classification: unknown + owner: operator + source: Host Europe control panel or invoice + commissioned_on: + value: "2026-03-08" + basis: measured + source: workplans/archived/260702-RAIL-HO-WP-0001-hosteurope-bootstrap.md and state-hub-inbox/2026-03-08-railiance01-bootstrap.yaml + note: Date this repo took the host into S1 management. Not the provider order date. + renews_on: + value: null + classification: unknown + owner: operator + source: Host Europe control panel or invoice + cancel_by: + value: null + classification: unknown + owner: operator + source: Host Europe contract / cancellation notice period + contract_term: + value: null + classification: unknown + owner: operator + retired_on: + value: null + classification: not_applicable + note: Resource is active. + +cost_boundary: + currency: EUR + booked_price: + value: null + classification: unknown + owner: fin-hub + workplan: FIN-WP-0004 + tax_treatment: + value: null + classification: unknown + owner: fin-hub + this_repo_publishes: identity, location, lifecycle dates we can prove, host observations, host-ops labor + this_repo_does_not_publish: + - invoices + - customer numbers + - payment instruments + - booked recurring price + +residual_credential: + finding: cloud-init metadata on the host still exposes an admin_pass field + action: rotate or disable that password via the Host Europe panel if it is still valid + recorded: false + note: The value was not copied into this repository. diff --git a/docs/evidence/resource-hosteurope-railiance01/labor-and-exit.yaml b/docs/evidence/resource-hosteurope-railiance01/labor-and-exit.yaml new file mode 100644 index 0000000..cceed33 --- /dev/null +++ b/docs/evidence/resource-hosteurope-railiance01/labor-and-exit.yaml @@ -0,0 +1,101 @@ +# Host-level operations labor and exit inputs for resource:hosteurope:railiance01 +# Consumer: resource-control forecasts that need to separate host infrastructure, +# recurring host-ops labor, and replacement/exit effort. +# Does not estimate workload, cluster, or platform labor. + +schema_version: "1.0" +resource_id: resource:hosteurope:railiance01 +published_at: "2026-08-15" +authority: railiance-infra +workplan: RAIL-HO-WP-0008-T03 + +scope: + includes: + - OS baseline convergence + - host firewall declared state + - host verification (Goss) + - SSH access and host identity + - host incident response + - replacement and cancellation of this virtual server + excludes: + - k3s / cluster operations (railiance-cluster) + - platform services (railiance-platform) + - application and tenant labor (those repos) + - booked Host Europe spend (fin-hub) + +recurring_activities: + - id: os-baseline-converge + description: Apply ansible/playbooks/bootstrap.yaml or a tagged subset + cadence: event-driven + typical_hours: 1.0 + basis: estimated + note: One tagged run. Full untagged converge is deliberately not routine. + - id: firewall-declared-state + description: Review and apply UFW from inventory (make converge-firewall) + cadence: event-driven + typical_hours: 0.5 + basis: estimated + note: Hourly Goss timer is unattended; this is operator time when it fails. + - id: host-verify + description: make verify-host HOST=Railiance01 and read TAP + cadence: monthly + typical_hours: 0.25 + basis: estimated + - id: ssh-and-identity + description: SSH CA, authorized_keys, ops-bridge key, inventory IP + cadence: event-driven + typical_hours: 0.5 + basis: estimated + - id: host-incident + description: Host-level outage, disk, SSH lockout, provider ticket + cadence: unscheduled + typical_hours: 2.0 + basis: estimated + note: Not every month. Use as a rare-event allowance, not a measured mean. + +monthly_host_ops_hours: + value: 2.0 + unit: hours + basis: estimated + method: Sum of typical_hours for monthly verify plus a share of event-driven work + uncertainty: No time records exist. This is a forecast placeholder, not a measurement. + owner: railiance-infra + +one_off_not_recurring: + - id: RAIL-HO-WP-0009 + description: Firewall declared-state integrity and k3s API tunnel-only cutover + completed: "2026-08-15" + hours: null + basis: unknown + note: Do not amortise into the monthly host-ops figure until a time record exists. + +exit: + path: + - Provision replacement compute (manual Host Europe panel or another provider) + - Restore or redeploy k3s and stateful services from their owning repos + - Switch DNS and ingress + - Verify workloads + - Cancel the Host Europe virtual server in the panel + - Confirm disk wipe / instance destroy + replacement_cutover_hours: + value: 8.0 + unit: hours + basis: estimated + uncertainty: Host-ops only. Cluster and workload cutover hours belong to those repos. + cancellation: + notice_period: + value: null + classification: unknown + owner: operator + source: Host Europe contract + panel: Host Europe / GoDaddy VPS control panel + provider_api: none + data_at_rest: + items: + - Ubuntu root filesystem (193 GiB provisioned class) + - local-path PVs for cluster workloads (failure domain host:railiance01) + note: PV contents are not S1-owned; list them only as correlated-exit risk. + +cadence_for_consumers: + republish_labor: when the activity catalog changes, or after a quarter of time records + republish_exit: when replacement procedure or cancel terms become known diff --git a/docs/evidence/resource-hosteurope-railiance01/observations/2026-08-15T170250Z.json b/docs/evidence/resource-hosteurope-railiance01/observations/2026-08-15T170250Z.json new file mode 100644 index 0000000..85a4efc --- /dev/null +++ b/docs/evidence/resource-hosteurope-railiance01/observations/2026-08-15T170250Z.json @@ -0,0 +1,92 @@ +{ + "schema_version": "1.0", + "record_type": "host_capacity_observation", + "resource_id": "resource:hosteurope:railiance01", + "inventory_name": "Railiance01", + "observed_at": "2026-08-15T17:02:50Z", + "observer": "railiance-infra/scripts/observe-host-capacity.py", + "source_commands": [ + "nproc / lscpu /proc/meminfo /proc/loadavg statvfs(/)", + "cloud-init query ", + "sudo k3s kubectl get node -o json", + "sudo k3s kubectl top node" + ], + "layers": { + "provider_declared": { + "instance_type": "oh.hosting.c2.r4.d100", + "instance_uuid": "c0e697d1-4c71-40f5-9746-0b4c4b9e5c17", + "instance_id_ec2": "i-000197c7", + "instance_name": "1fxw7h4zug", + "product_family": "vps4", + "product_role": "vps4_default", + "local_hostname": "1fxw7h4zug.cloud.prd.sxb1.gdg", + "project_id": "f6491e4f796146d1a7ce30d569d5f122", + "note": "Declared by the provider metadata service. Not a booked SKU confirmation." + }, + "operating_system": { + "hostname": "239.62.205.92.host.secureserver.net", + "os": "Ubuntu 24.04.4 LTS", + "cpu": { + "value": 4, + "unit": "vCPU", + "kind": "usable", + "model": "AMD EPYC Processor (with IBPB)", + "hypervisor": "KVM" + }, + "memory": { + "total_kib": 16377056, + "available_kib": 10577368, + "unit": "KiB", + "kind": "usable" + }, + "swap": { + "total_kib": 4194300, + "unit": "KiB", + "kind": "observed", + "note": "OS swapfile, not a provider disk grant" + }, + "root_filesystem": { + "total_bytes": 206900281344, + "used_bytes": 77003415552, + "avail_bytes": 129880088576, + "kind": "provisioned" + }, + "block_devices": "sda 200G disk QEMU HARDDISK\nsr0 838K rom QEMU DVD-ROM", + "loadavg": [ + "1.85", + "1.38", + "1.31" + ], + "uptime_seconds": 1738009.47 + }, + "kubernetes": { + "node_name": "239.62.205.92.host.secureserver.net", + "capacity": { + "cpu": "4", + "memory": "16377056Ki", + "ephemeral_storage": "202051056Ki", + "pods": "110" + }, + "allocatable": { + "cpu": "4", + "memory": "16377056Ki", + "ephemeral_storage": "196555267123", + "pods": "110" + }, + "observed": { + "cpu": "1715m", + "cpu_pct": "42%", + "memory": "6308Mi", + "memory_pct": "39%" + } + } + }, + "unknown": [ + { + "field": "traffic_allowance", + "classification": "unknown", + "owner": "operator", + "source": "Host Europe control panel or contract" + } + ] +} diff --git a/docs/evidence/resource-hosteurope-railiance01/observations/latest.json b/docs/evidence/resource-hosteurope-railiance01/observations/latest.json new file mode 120000 index 0000000..ecdedb8 --- /dev/null +++ b/docs/evidence/resource-hosteurope-railiance01/observations/latest.json @@ -0,0 +1 @@ +2026-08-15T170250Z.json \ No newline at end of file diff --git a/scripts/observe-host-capacity.py b/scripts/observe-host-capacity.py new file mode 100755 index 0000000..a43ac39 --- /dev/null +++ b/scripts/observe-host-capacity.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Collect a non-secret host capacity observation over SSH. + +Does not dump cloud-init metadata wholesale (that blob can contain +admin_pass). Only named keys are read. + +Usage: + scripts/observe-host-capacity.py Railiance01 + scripts/observe-host-capacity.py Railiance01 > docs/evidence/resource-hosteurope-railiance01/observations/....json +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from datetime import datetime, timezone + +REMOTE = r""" +set -euo pipefail +python3 - <<'PY' +import json, os, subprocess, shutil + +def sh(cmd): + return subprocess.check_output(cmd, shell=True, text=True).strip() + +def cloud(key): + if not shutil.which("cloud-init"): + return None + try: + out = subprocess.check_output( + ["cloud-init", "query", key], text=True, stderr=subprocess.DEVNULL + ).strip() + except subprocess.CalledProcessError: + return None + if not out or out.lower() in {"none", "null"}: + return None + return out + +mem = open("/proc/meminfo").read().split() +kv = {} +for i, tok in enumerate(mem): + if tok.endswith(":") and i + 1 < len(mem): + try: + kv[tok[:-1]] = int(mem[i + 1]) + except ValueError: + pass +st = os.statvfs("/") +k3s = {} +if shutil.which("k3s"): + raw = subprocess.check_output( + ["sudo", "k3s", "kubectl", "get", "node", "-o", "json"], text=True + ) + node = json.loads(raw)["items"][0] + cap = node["status"]["capacity"] + alloc = node["status"]["allocatable"] + k3s = { + "node_name": node["metadata"]["name"], + "capacity": { + "cpu": cap.get("cpu"), + "memory": cap.get("memory"), + "ephemeral_storage": cap.get("ephemeral-storage"), + "pods": cap.get("pods"), + }, + "allocatable": { + "cpu": alloc.get("cpu"), + "memory": alloc.get("memory"), + "ephemeral_storage": alloc.get("ephemeral-storage"), + "pods": alloc.get("pods"), + }, + } + try: + top = subprocess.check_output( + ["sudo", "k3s", "kubectl", "top", "node", "--no-headers"], + text=True, + stderr=subprocess.DEVNULL, + ).split() + # NAME CPU(cores) CPU% MEMORY(bytes) MEMORY% + if len(top) >= 5: + k3s["observed"] = { + "cpu": top[1], + "cpu_pct": top[2], + "memory": top[3], + "memory_pct": top[4], + } + except subprocess.CalledProcessError: + k3s["observed"] = None + +print(json.dumps({ + "hostname": sh("hostname -f"), + "os_pretty": sh("grep ^PRETTY_NAME= /etc/os-release").split("=", 1)[1].strip('"'), + "nproc": int(sh("nproc")), + "cpu_model": sh("lscpu | awk -F: '/Model name/ {print $2; exit}'").strip(), + "hypervisor": sh("lscpu | awk -F: '/Hypervisor vendor/ {print $2; exit}'").strip() or None, + "mem_total_kib": kv.get("MemTotal"), + "mem_available_kib": kv.get("MemAvailable"), + "swap_total_kib": kv.get("SwapTotal"), + "root_total_bytes": st.f_frsize * st.f_blocks, + "root_used_bytes": st.f_frsize * (st.f_blocks - st.f_bfree), + "root_avail_bytes": st.f_frsize * st.f_bavail, + "block_devices": sh("lsblk -dn -o NAME,SIZE,TYPE,MODEL"), + "loadavg": open("/proc/loadavg").read().split()[:3], + "uptime_seconds": float(open("/proc/uptime").read().split()[0]), + "instance_type": cloud("ds.ec2_metadata.instance-type"), + "instance_id_ec2": cloud("ds.ec2_metadata.instance-id"), + "instance_uuid": cloud("ds.meta_data.uuid"), + "instance_name": cloud("ds.meta_data.name"), + "product_family": cloud("ds.meta_data.meta.app"), + "product_role": cloud("ds.meta_data.meta.role"), + "local_hostname": cloud("ds.ec2_metadata.local-hostname"), + "project_id": cloud("ds.meta_data.project_id"), + "k3s": k3s or None, +})) +PY +""" + + +def inventory_ssh(name: str) -> tuple[str, str]: + import yaml + + path = os.path.join(os.path.dirname(__file__), "..", "inventory", "servers.yaml") + with open(path) as f: + servers = yaml.safe_load(f).get("servers") or [] + for s in servers: + if s.get("name") == name: + return s["ip"], s.get("ssh_user", "tegwick") + raise SystemExit(f"unknown inventory host: {name}") + + +def main() -> int: + if len(sys.argv) != 2: + print("Usage: observe-host-capacity.py ", file=sys.stderr) + return 2 + name = sys.argv[1] + ip, user = inventory_ssh(name) + observed_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + raw = subprocess.check_output( + ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", f"{user}@{ip}", REMOTE], + text=True, + ) + host = json.loads(raw) + rec = { + "schema_version": "1.0", + "record_type": "host_capacity_observation", + "resource_id": "resource:hosteurope:railiance01" if name == "Railiance01" else f"resource:host:{name.lower()}", + "inventory_name": name, + "observed_at": observed_at, + "observer": "railiance-infra/scripts/observe-host-capacity.py", + "source_commands": [ + "nproc / lscpu /proc/meminfo /proc/loadavg statvfs(/)", + "cloud-init query ", + "sudo k3s kubectl get node -o json", + "sudo k3s kubectl top node", + ], + "layers": { + "provider_declared": { + "instance_type": host.get("instance_type"), + "instance_uuid": host.get("instance_uuid"), + "instance_id_ec2": host.get("instance_id_ec2"), + "instance_name": host.get("instance_name"), + "product_family": host.get("product_family"), + "product_role": host.get("product_role"), + "local_hostname": host.get("local_hostname"), + "project_id": host.get("project_id"), + "note": "Declared by the provider metadata service. Not a booked SKU confirmation.", + }, + "operating_system": { + "hostname": host["hostname"], + "os": host["os_pretty"], + "cpu": { + "value": host["nproc"], + "unit": "vCPU", + "kind": "usable", + "model": host.get("cpu_model"), + "hypervisor": host.get("hypervisor"), + }, + "memory": { + "total_kib": host["mem_total_kib"], + "available_kib": host["mem_available_kib"], + "unit": "KiB", + "kind": "usable", + }, + "swap": { + "total_kib": host["swap_total_kib"], + "unit": "KiB", + "kind": "observed", + "note": "OS swapfile, not a provider disk grant", + }, + "root_filesystem": { + "total_bytes": host["root_total_bytes"], + "used_bytes": host["root_used_bytes"], + "avail_bytes": host["root_avail_bytes"], + "kind": "provisioned", + }, + "block_devices": host["block_devices"], + "loadavg": host["loadavg"], + "uptime_seconds": host["uptime_seconds"], + }, + "kubernetes": host.get("k3s"), + }, + "unknown": [ + { + "field": "traffic_allowance", + "classification": "unknown", + "owner": "operator", + "source": "Host Europe control panel or contract", + } + ], + } + json.dump(rec, sys.stdout, indent=2) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md b/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md index 9e3cc40..b8909c0 100644 --- a/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md +++ b/workplans/RAIL-HO-WP-0008-railiance01-resource-and-commercial-evidence.md @@ -4,11 +4,11 @@ type: workplan title: "Publish railiance01 resource and commercial evidence" domain: financials repo: railiance-infra -status: ready +status: finished owner: codex topic_slug: railiance created: "2026-08-11" -updated: "2026-08-11" +updated: "2026-08-15" related: - RESOURCE-WP-0003 state_hub_workstream_id: "7122657f-87c8-46b5-a725-a1af1ba0af12" @@ -27,7 +27,7 @@ Origin: `RESOURCE-WP-0003-T04` delegated evidence gap. ```task id: RAIL-HO-WP-0008-T01 -status: todo +status: done priority: high state_hub_task_id: "cde55584-229d-4e27-8fc8-0f54d78c71ed" ``` @@ -41,11 +41,23 @@ customer numbers, or payment instruments. Done when resource-control can update the provider and lifecycle fields with provenance, while unknown or restricted fields remain explicitly classified. +**Done 2026-08-15.** +`docs/evidence/resource-hosteurope-railiance01/identity-lifecycle.yaml` + +- Product class: virtual server, family `vps4`, type `oh.hosting.c2.r4.d100` +- Provider resource ID: OpenStack UUID `c0e697d1-4c71-40f5-9746-0b4c4b9e5c17` +- Join key: `openstack-project:f6491e4f796146d1a7ce30d569d5f122` (not the + billing customer number) +- Location: `sxb1` / Strasbourg / `FR` / EU +- Commissioned into S1 management: `2026-03-08` +- Order, renewal, cancel-by, contract term: unknown, owner operator +- Booked price: unknown, owner fin-hub (`FIN-WP-0004`) + ## T02 — Publish host capacity and utilization evidence ```task id: RAIL-HO-WP-0008-T02 -status: todo +status: done priority: high state_hub_task_id: "8b0e5547-23ae-4516-b6d7-9c196882d088" ``` @@ -58,11 +70,20 @@ Kubernetes observations. Done when at least one reproducible observation can be consumed by resource-control and its source and cadence are documented. +**Done 2026-08-15.** Producer +`scripts/observe-host-capacity.py`; first observation +`docs/evidence/resource-hosteurope-railiance01/observations/2026-08-15T170250Z.json` +(also `observations/latest.json`). Cadence: monthly, and after a resize. +Traffic allowance remains unknown. + +Sample at `2026-08-15T17:02:50Z`: 4 vCPU, 16377056 KiB RAM, 193 GiB root +(72 GiB used); k3s `kubectl top` 1715m CPU / 6308 MiB memory. + ## T03 — Record infrastructure operations labor and exit inputs ```task id: RAIL-HO-WP-0008-T03 -status: todo +status: done priority: medium state_hub_task_id: "8bdaa96f-400b-455f-87db-d5c11e287142" ``` @@ -74,8 +95,23 @@ Do not estimate another repository's workload labor. Done when resource-control can separate host infrastructure, recurring labor, and migration effort in forecasts and optimization cases. +**Done 2026-08-15.** +`docs/evidence/resource-hosteurope-railiance01/labor-and-exit.yaml` + +Recurring host-ops catalog plus `monthly_host_ops_hours: 2.0` (basis +`estimated`, no time records). Replacement cutover 8 host-ops hours +(estimated). Cancellation notice period unknown. Cluster and workload labor +excluded. + ## Acceptance -- [ ] Provider identity and lifecycle evidence is non-secret and attributable. -- [ ] Capacity and utilization observations have timestamps and provenance. -- [ ] Host labor and exit inputs can be consumed without duplicating booked cost. +- [x] Provider identity and lifecycle evidence is non-secret and attributable. +- [x] Capacity and utilization observations have timestamps and provenance. +- [x] Host labor and exit inputs can be consumed without duplicating booked cost. + +## Outcome + +Finished 2026-08-15. Interface: +`docs/evidence/resource-hosteurope-railiance01/`. resource-control can fold +the known fields and must leave booked price and contract dates unknown until +their owners publish them.