Initial commit: VMware vSphere API simulator scaffold.

Add the FastAPI app, PostgreSQL migrations, Docker/Helm packaging, API
contracts, docs, client examples, and the unit/integration/compatibility
test suite for local client and tooling labs without a real vCenter.
This commit is contained in:
2026-07-18 04:42:11 +03:00
commit f8d3cbdd59
422 changed files with 361335 additions and 0 deletions
+49
View File
@@ -0,0 +1,49 @@
**Language / Язык:** [English](README.md) | [Русский](README.ru.md)
# Runnable client cookbooks
Companion code for [docs/clients.md](../docs/clients.md) and
[docs/examples/](../docs/examples/overview.md).
## Prerequisites
```bash
make up
VSPHERE_PROFILE=demo-cluster make seed # or small / large
```
Default lab credentials: `administrator@vsphere.local` / `VMware1!`.
## Native vSphere smokes
```bash
python examples/python/vsphere_rest_smoke.py https://localhost
python examples/python/vsphere_soap_smoke.py https://localhost
python examples/python/vsphere_lifecycle.py # REST create/power/guest FS + SOAP CreateVM
python examples/python/requests_cookbook.py # raw requests, no SDK
ansible-playbook -i examples/ansible/inventory.ini examples/ansible/vsphere_playbook.yml
cd examples/terraform/vsphere && terraform init && terraform apply
cd examples/pulumi && pulumi up
cd examples/go && go run .
cd examples/java && javac Cookbook.java && java Cookbook
cd examples/perl && cpanm --installdeps . && perl cookbook.pl
make vsphere-surface
```
## Layout
| Path | Stack |
|---|---|
| `python/vsphere_*.py` | Native REST + SOAP smokes / lifecycle |
| `python/requests_cookbook.py` | Raw `requests` REST cookbook (session, create/power/delete) |
| `ansible/vsphere_playbook.yml` | Ansible REST lifecycle |
| `terraform/vsphere/` | Terraform `hashicorp/vsphere` (SOAP-backed provider) |
| `pulumi/` | Pulumi REST `ComponentResource` |
| `go/`, `java/`, `perl/` | Minimal REST cookbooks in other languages (stdlib HTTP clients, Basic-auth session) |
An independent `pulumi-vsphere` lab suite lives under
[`pulumi-tests/`](../pulumi-tests/README.md) — run `make pulumi-tests` from the
repo root (or `make test-pulumi` inside that directory).
Guides: [docs/examples/](../docs/examples/overview.md). Coverage matrix:
[docs/api-coverage.md](../docs/api-coverage.md).
+49
View File
@@ -0,0 +1,49 @@
**Language / Язык:** [English](README.md) | [Русский](README.ru.md)
# Исполняемые client cookbooks
Сопровождающий код к [docs/ru/clients.md](../docs/ru/clients.md) и
[docs/ru/examples/](../docs/ru/examples/overview.md).
## Требования
```bash
make up
VSPHERE_PROFILE=demo-cluster make seed # or small / large
```
Учётные данные лаборатории по умолчанию: `administrator@vsphere.local` / `VMware1!`.
## Нативные smoke vSphere
```bash
python examples/python/vsphere_rest_smoke.py https://localhost
python examples/python/vsphere_soap_smoke.py https://localhost
python examples/python/vsphere_lifecycle.py # REST create/power/guest FS + SOAP CreateVM
python examples/python/requests_cookbook.py # raw requests, no SDK
ansible-playbook -i examples/ansible/inventory.ini examples/ansible/vsphere_playbook.yml
cd examples/terraform/vsphere && terraform init && terraform apply
cd examples/pulumi && pulumi up
cd examples/go && go run .
cd examples/java && javac Cookbook.java && java Cookbook
cd examples/perl && cpanm --installdeps . && perl cookbook.pl
make vsphere-surface
```
## Структура
| Путь | Стек |
|---|---|
| `python/vsphere_*.py` | Нативные REST + SOAP smokes / lifecycle |
| `python/requests_cookbook.py` | Raw `requests` REST cookbook (session, create/power/delete) |
| `ansible/vsphere_playbook.yml` | Ansible REST lifecycle |
| `terraform/vsphere/` | Terraform `hashicorp/vsphere` (SOAP-backed provider) |
| `pulumi/` | Pulumi REST `ComponentResource` |
| `go/`, `java/`, `perl/` | Минимальные REST cookbook на других языках (stdlib HTTP-клиенты, сессия Basic-auth) |
Независимый lab-набор на `pulumi-vsphere` живёт в
[`pulumi-tests/`](../pulumi-tests/README.ru.md) — из корня репозитория:
`make pulumi-tests` (или `make test-pulumi` внутри этой директории).
Гайды: [docs/ru/examples/](../docs/ru/examples/overview.md). Матрица покрытия:
[docs/ru/api-coverage.md](../docs/ru/api-coverage.md).
+2
View File
@@ -0,0 +1,2 @@
[simulator]
localhost ansible_connection=local
+123
View File
@@ -0,0 +1,123 @@
---
# vSphere Automation REST cookbook against the API simulator.
# ansible-playbook -i inventory.ini vsphere_playbook.yml
#
# Requires: gateway https://localhost (or set vsphere_base).
- name: vSphere API simulator lifecycle
hosts: simulator
gather_facts: false
vars:
vsphere_base: "https://localhost"
vsphere_user: "administrator@vsphere.local"
vsphere_password: "VMware1!"
vm_name: "ansible-lab-01"
tasks:
- name: Create CIS session
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/session"
method: POST
url_username: "{{ vsphere_user }}"
url_password: "{{ vsphere_password }}"
force_basic_auth: true
validate_certs: false
status_code: [200, 201]
return_content: true
register: session
- name: Set session header
ansible.builtin.set_fact:
vsphere_headers:
vmware-api-session-id: "{{ session.json }}"
- name: List VMs
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm"
headers: "{{ vsphere_headers }}"
validate_certs: false
return_content: true
register: vms
- name: Show inventory size
ansible.builtin.debug:
msg: "VMs in inventory: {{ vms.json | length }}"
- name: Create VM
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm"
method: POST
headers: "{{ vsphere_headers }}"
body_format: json
body:
name: "{{ vm_name }}"
guest_OS: OTHER_GUEST_64
placement:
folder: group-v23
host: host-11
datastore: datastore-31
resource_pool: resgroup-22
cpu:
count: 1
memory:
size_MiB: 512
validate_certs: false
status_code: [200, 201]
return_content: true
register: created
- name: Power on
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm/{{ created.json }}/power?action=start"
method: POST
headers: "{{ vsphere_headers }}"
validate_certs: false
status_code: [200, 204]
return_content: true
register: power_on
- name: Poll CIS task when returned
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/cis/tasks/{{ power_on.json.task }}"
headers: "{{ vsphere_headers }}"
validate_certs: false
return_content: true
register: task
when: power_on.json is mapping and power_on.json.task is defined
until: task.json.status in ['SUCCEEDED', 'FAILED']
retries: 30
delay: 1
- name: Write guest file (lab virtual FS)
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm/{{ created.json }}/guest/filesystem?path=/tmp/ansible-marker"
method: PUT
headers: "{{ vsphere_headers }}"
body_format: json
body:
content: "ansible-ok"
validate_certs: false
status_code: [200, 204]
- name: Power off
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm/{{ created.json }}/power?action=stop"
method: POST
headers: "{{ vsphere_headers }}"
validate_certs: false
status_code: [200, 204]
- name: Delete VM
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/vcenter/vm/{{ created.json }}"
method: DELETE
headers: "{{ vsphere_headers }}"
validate_certs: false
status_code: [200, 204]
- name: Delete session
ansible.builtin.uri:
url: "{{ vsphere_base }}/api/session"
method: DELETE
headers: "{{ vsphere_headers }}"
validate_certs: false
status_code: [200, 204]
+3
View File
@@ -0,0 +1,3 @@
module example.com/vmware-api-simulator-cookbook
go 1.22
+121
View File
@@ -0,0 +1,121 @@
package main
import (
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func env(k, def string) string {
if v := os.Getenv(k); v != "" {
return v
}
return def
}
var client = &http.Client{
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}, // lab self-signed cert only
}
func main() {
base := strings.TrimRight(env("VSPHERE_BASE", "https://localhost"), "/")
user := env("VSPHERE_USER", "administrator@vsphere.local")
password := env("VSPHERE_PASSWORD", "VMware1!")
vmName := env("VSPHERE_VM_NAME", "go-lab-01")
auth := "Basic " + base64.StdEncoding.EncodeToString([]byte(user+":"+password))
var sessionID string
call(base+"/api/session", "POST", map[string]string{"Authorization": auth}, nil, &sessionID)
fmt.Println("session:", sessionID)
headers := map[string]string{"vmware-api-session-id": sessionID}
var vms []map[string]any
call(base+"/api/vcenter/vm", "GET", headers, nil, &vms)
fmt.Printf("vms before: %d\n", len(vms))
body, _ := json.Marshal(map[string]any{
"name": vmName,
"guest_OS": "OTHER_GUEST_64",
"placement": map[string]string{
"folder": "group-v23",
"host": "host-11",
"datastore": "datastore-31",
"resource_pool": "resgroup-22",
},
"cpu": map[string]int{"count": 1},
"memory": map[string]int{"size_MiB": 512},
})
var moid string
call(base+"/api/vcenter/vm", "POST", headers, body, &moid)
fmt.Println("created:", moid)
var power map[string]string
call(base+"/api/vcenter/vm/"+moid+"/power?action=start", "POST", headers, nil, &power)
waitTask(base, headers, power["task"])
var detail map[string]any
call(base+"/api/vcenter/vm/"+moid, "GET", headers, nil, &detail)
fmt.Println("power_state:", detail["power_state"])
call(base+"/api/vcenter/vm/"+moid+"/power?action=stop", "POST", headers, nil, &power)
waitTask(base, headers, power["task"])
call(base+"/api/vcenter/vm/"+moid, "DELETE", headers, nil, nil)
call(base+"/api/session", "DELETE", headers, nil, nil)
fmt.Println("ok")
}
func waitTask(base string, headers map[string]string, task string) {
if task == "" {
return
}
deadline := time.Now().Add(2 * time.Minute)
for time.Now().Before(deadline) {
var status map[string]any
call(base+"/api/cis/tasks/"+task, "GET", headers, nil, &status)
if s, _ := status["status"].(string); s == "SUCCEEDED" || s == "FAILED" {
return
}
time.Sleep(300 * time.Millisecond)
}
panic("timeout waiting for task " + task)
}
func call(url, method string, headers map[string]string, body []byte, out any) {
var reqBody io.Reader
if body != nil {
reqBody = strings.NewReader(string(body))
}
req, err := http.NewRequest(method, url, reqBody)
if err != nil {
panic(err)
}
for k, v := range headers {
req.Header.Set(k, v)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
panic(fmt.Sprintf("%s %s: %d %s", method, url, resp.StatusCode, raw))
}
if out == nil || len(raw) == 0 {
return
}
if err := json.Unmarshal(raw, out); err != nil {
panic(err)
}
}
+170
View File
@@ -0,0 +1,170 @@
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Base64;
/**
* Minimal Java 11+ cookbook against the vSphere REST gateway using a Basic-auth
* session (vmware-api-session-id).
*
* javac Cookbook.java && java Cookbook
*/
public final class Cookbook {
private static final HttpClient CLIENT =
HttpClient.newBuilder().sslContext(insecureSslContext()).build();
public static void main(String[] args) throws Exception {
String base = env("VSPHERE_BASE", "https://localhost");
String user = env("VSPHERE_USER", "administrator@vsphere.local");
String password = env("VSPHERE_PASSWORD", "VMware1!");
String vmName = env("VSPHERE_VM_NAME", "java-lab-01");
String auth =
"Basic " + Base64.getEncoder().encodeToString((user + ":" + password).getBytes(StandardCharsets.UTF_8));
String sessionId = unquote(send(post(base + "/api/session", auth, null)));
System.out.println("session: " + sessionId);
String vms = send(get(base + "/api/vcenter/vm", sessionId));
System.out.println("vms before: " + vms.length() + " bytes");
String createBody =
"{\"name\":\""
+ vmName
+ "\",\"guest_OS\":\"OTHER_GUEST_64\","
+ "\"placement\":{\"folder\":\"group-v23\",\"host\":\"host-11\","
+ "\"datastore\":\"datastore-31\",\"resource_pool\":\"resgroup-22\"},"
+ "\"cpu\":{\"count\":1},\"memory\":{\"size_MiB\":512}}";
String moid = unquote(send(postJson(base + "/api/vcenter/vm", sessionId, createBody)));
System.out.println("created: " + moid);
String power = send(post(base + "/api/vcenter/vm/" + moid + "/power?action=start", sessionId, null));
waitTask(base, sessionId, extractField(power, "task"));
String detail = send(get(base + "/api/vcenter/vm/" + moid, sessionId));
System.out.println("power_state: " + extractField(detail, "power_state"));
power = send(post(base + "/api/vcenter/vm/" + moid + "/power?action=stop", sessionId, null));
waitTask(base, sessionId, extractField(power, "task"));
send(delete(base + "/api/vcenter/vm/" + moid, sessionId));
send(delete(base + "/api/session", sessionId));
System.out.println("ok");
}
private static void waitTask(String base, String sessionId, String task) throws Exception {
if (task == null || task.isBlank()) {
return;
}
long deadline = System.currentTimeMillis() + 120_000;
while (System.currentTimeMillis() < deadline) {
String body = send(get(base + "/api/cis/tasks/" + task, sessionId));
String status = extractField(body, "status");
if ("SUCCEEDED".equals(status) || "FAILED".equals(status)) {
return;
}
Thread.sleep(300);
}
throw new IllegalStateException("timeout waiting for task " + task);
}
private static String env(String key, String def) {
String value = System.getenv(key);
return value == null || value.isBlank() ? def : value;
}
private static HttpRequest get(String uri, String sessionId) {
return HttpRequest.newBuilder(URI.create(uri))
.timeout(Duration.ofSeconds(60))
.header("vmware-api-session-id", sessionId)
.GET()
.build();
}
private static HttpRequest post(String uri, String auth, String body) {
HttpRequest.Builder builder =
HttpRequest.newBuilder(URI.create(uri)).timeout(Duration.ofSeconds(60));
if (auth.startsWith("Basic ")) {
builder.header("Authorization", auth);
} else {
builder.header("vmware-api-session-id", auth);
}
return builder.POST(body == null ? BodyPublishers.noBody() : BodyPublishers.ofString(body)).build();
}
private static HttpRequest postJson(String uri, String sessionId, String body) {
return HttpRequest.newBuilder(URI.create(uri))
.timeout(Duration.ofSeconds(60))
.header("vmware-api-session-id", sessionId)
.header("Content-Type", "application/json")
.POST(BodyPublishers.ofString(body))
.build();
}
private static HttpRequest delete(String uri, String sessionId) {
return HttpRequest.newBuilder(URI.create(uri))
.timeout(Duration.ofSeconds(60))
.header("vmware-api-session-id", sessionId)
.DELETE()
.build();
}
private static String send(HttpRequest request) throws Exception {
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 300) {
throw new RuntimeException(response.statusCode() + ": " + response.body());
}
return response.body();
}
private static String unquote(String value) {
String trimmed = value.trim();
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) {
return trimmed.substring(1, trimmed.length() - 1);
}
return trimmed;
}
/** Tiny string-based field extraction — good enough for a lab smoke, no JSON library required. */
private static String extractField(String body, String field) {
String marker = "\"" + field + "\":";
int start = body.indexOf(marker);
if (start < 0) {
return "";
}
start += marker.length();
while (start < body.length() && (body.charAt(start) == ' ' || body.charAt(start) == '"')) {
start++;
}
int end = start;
while (end < body.length() && body.charAt(end) != '"' && body.charAt(end) != ',' && body.charAt(end) != '}') {
end++;
}
return body.substring(start, end);
}
private static javax.net.ssl.SSLContext insecureSslContext() {
try {
javax.net.ssl.TrustManager[] trustAll =
new javax.net.ssl.TrustManager[] {
new javax.net.ssl.X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {}
}
};
javax.net.ssl.SSLContext context = javax.net.ssl.SSLContext.getInstance("TLS");
context.init(null, trustAll, new java.security.SecureRandom());
return context;
} catch (Exception error) {
throw new RuntimeException(error);
}
}
}
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env perl
use strict;
use warnings;
use HTTP::Tiny;
use JSON qw(decode_json encode_json);
use MIME::Base64 qw(encode_base64);
use Time::HiRes qw(sleep);
my $base = $ENV{VSPHERE_BASE} // 'https://localhost';
my $user = $ENV{VSPHERE_USER} // 'administrator@vsphere.local';
my $password = $ENV{VSPHERE_PASSWORD} // 'VMware1!';
my $vm_name = $ENV{VSPHERE_VM_NAME} // 'perl-lab-01';
# Lab self-signed certificate only — do not disable verification against a real vCenter.
my $http = HTTP::Tiny->new(timeout => 60, verify_SSL => 0);
sub api {
my ($method, $path, %opts) = @_;
my $res = $http->request($method, "$base$path", \%opts);
die "$method $path failed: $res->{status} $res->{content}\n" unless $res->{success};
return $res->{content};
}
sub json_field {
my ($body, $field) = @_;
my $data = decode_json($body);
return $data->{$field};
}
sub wait_task {
my ($task, $headers) = @_;
return unless $task;
my $deadline = time + 120;
while (time < $deadline) {
my $body = api('GET', "/api/cis/tasks/$task", headers => $headers);
my $status = json_field($body, 'status');
return if $status eq 'SUCCEEDED' || $status eq 'FAILED';
sleep(0.3);
}
die "timeout waiting for task $task\n";
}
my $auth = 'Basic ' . encode_base64("$user:$password", '');
my $session_body = api('POST', '/api/session', headers => { Authorization => $auth });
(my $session_id = $session_body) =~ s/^"(.*)"$/$1/;
print "session: $session_id\n";
my %headers = ('vmware-api-session-id' => $session_id);
my $vms_before = api('GET', '/api/vcenter/vm', headers => \%headers);
print 'vms before: ', scalar(@{ decode_json($vms_before) }), "\n";
my $create_body = encode_json({
name => $vm_name,
guest_OS => 'OTHER_GUEST_64',
placement => {
folder => 'group-v23',
host => 'host-11',
datastore => 'datastore-31',
resource_pool => 'resgroup-22',
},
cpu => { count => 1 },
memory => { size_MiB => 512 },
});
my $created = api(
'POST', '/api/vcenter/vm',
headers => { %headers, 'Content-Type' => 'application/json' },
content => $create_body,
);
(my $moid = $created) =~ s/^"(.*)"$/$1/;
print "created: $moid\n";
my $power = api('POST', "/api/vcenter/vm/$moid/power?action=start", headers => \%headers);
wait_task(json_field($power, 'task'), \%headers);
my $detail = api('GET', "/api/vcenter/vm/$moid", headers => \%headers);
print 'power_state: ', json_field($detail, 'power_state'), "\n";
$power = api('POST', "/api/vcenter/vm/$moid/power?action=stop", headers => \%headers);
wait_task(json_field($power, 'task'), \%headers);
api('DELETE', "/api/vcenter/vm/$moid", headers => \%headers);
api('DELETE', '/api/session', headers => \%headers);
print "ok\n";
+3
View File
@@ -0,0 +1,3 @@
requires 'HTTP::Tiny';
requires 'JSON';
requires 'IO::Socket::SSL';
+3
View File
@@ -0,0 +1,3 @@
name: vmware-api-simulator
runtime: python
description: pulumi-vsphere cookbook against vmware-api-simulator
+69
View File
@@ -0,0 +1,69 @@
"""Pulumi cookbook using official pulumi-vsphere against the simulator."""
from __future__ import annotations
import os
import pulumi
import pulumi_vsphere as vsphere
config = pulumi.Config()
user = config.get("user") or os.environ.get("VSPHERE_USER", "administrator@vsphere.local")
password = config.get_secret("password") or os.environ.get("VSPHERE_PASSWORD", "VMware1!")
server = config.get("server") or os.environ.get("VSPHERE_SERVER", "localhost")
datacenter_name = config.get("datacenter") or "Datacenter"
datastore_name = config.get("datastore") or "datastore1"
cluster_name = config.get("cluster") or "Cluster"
network_name = config.get("network") or "VM Network"
vm_name = config.get("vm_name") or "pulumi-lab-01"
provider = vsphere.Provider(
"vsphere",
user=user,
password=password,
vsphere_server=server,
allow_unverified_ssl=True,
)
invoke_opts = pulumi.InvokeOptions(provider=provider)
res_opts = pulumi.ResourceOptions(provider=provider)
dc = vsphere.get_datacenter_output(name=datacenter_name, opts=invoke_opts)
ds = vsphere.get_datastore_output(
name=datastore_name,
datacenter_id=dc.id,
opts=invoke_opts,
)
cluster = vsphere.get_compute_cluster_output(
name=cluster_name,
datacenter_id=dc.id,
opts=invoke_opts,
)
network = vsphere.get_network_output(
name=network_name,
datacenter_id=dc.id,
opts=invoke_opts,
)
vm = vsphere.VirtualMachine(
"cookbook-vm",
name=vm_name,
resource_pool_id=cluster.resource_pool_id,
datastore_id=ds.id,
num_cpus=1,
memory=1024,
guest_id="otherGuest64",
wait_for_guest_net_timeout=0,
wait_for_guest_ip_timeout=0,
network_interfaces=[
vsphere.VirtualMachineNetworkInterfaceArgs(network_id=network.id),
],
disks=[
vsphere.VirtualMachineDiskArgs(label="disk0", size=16),
],
opts=res_opts,
)
pulumi.export("datacenter_id", dc.id)
pulumi.export("lab_vm_id", vm.id)
pulumi.export("lab_vm_name", vm.name)
pulumi.export("resource_pool_id", cluster.resource_pool_id)
+2
View File
@@ -0,0 +1,2 @@
pulumi>=3.0.0,<4
pulumi-vsphere==4.17.0
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Raw `requests` cookbook against the vSphere REST gateway (no vsphere-automation-sdk)."""
from __future__ import annotations
import os
import sys
import time
from typing import Any
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
BASE = os.environ.get("VSPHERE_BASE", "https://localhost").rstrip("/")
USER = os.environ.get("VSPHERE_USER", "administrator@vsphere.local")
PASSWORD = os.environ.get("VSPHERE_PASSWORD", "VMware1!")
VM_NAME = os.environ.get("VSPHERE_VM_NAME", "req-lab-01")
def api(
method: str,
path: str,
*,
headers: dict[str, str] | None = None,
json_body: dict[str, Any] | None = None,
) -> Any:
response = requests.request(
method,
f"{BASE}{path}",
headers=headers,
json=json_body,
verify=False, # local self-signed development certificate only
timeout=60,
)
response.raise_for_status()
if response.status_code == 204 or not response.content:
return None
return response.json()
def wait_task(headers: dict[str, str], task: str, timeout: float = 120.0) -> None:
deadline = time.time() + timeout
while time.time() < deadline:
status = api("GET", f"/api/cis/tasks/{task}", headers=headers)
if status.get("status") in {"SUCCEEDED", "FAILED"}:
return
time.sleep(0.3)
raise TimeoutError(task)
def session_headers() -> dict[str, str]:
response = requests.post(
f"{BASE}/api/session",
auth=(USER, PASSWORD),
verify=False,
timeout=60,
)
response.raise_for_status()
return {"vmware-api-session-id": response.json()}
def main() -> int:
headers = session_headers()
print("session:", headers["vmware-api-session-id"])
vms = api("GET", "/api/vcenter/vm", headers=headers)
print("vms before:", len(vms))
moid = api(
"POST",
"/api/vcenter/vm",
headers=headers,
json_body={
"name": VM_NAME,
"guest_OS": "OTHER_GUEST_64",
"placement": {
"folder": "group-v23",
"host": "host-11",
"datastore": "datastore-31",
"resource_pool": "resgroup-22",
},
"cpu": {"count": 1},
"memory": {"size_MiB": 512},
},
)
print("created:", moid)
power = api("POST", f"/api/vcenter/vm/{moid}/power?action=start", headers=headers)
wait_task(headers, power["task"])
detail = api("GET", f"/api/vcenter/vm/{moid}", headers=headers)
print("power_state:", detail["power_state"])
power = api("POST", f"/api/vcenter/vm/{moid}/power?action=stop", headers=headers)
wait_task(headers, power["task"])
api("DELETE", f"/api/vcenter/vm/{moid}", headers=headers)
api("DELETE", "/api/session", headers=headers)
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -0,0 +1 @@
requests>=2.31
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
"""End-to-end Python smoke: REST create/power/guest-file + SOAP CreateVM_Task."""
from __future__ import annotations
import os
import sys
import xml.etree.ElementTree as ET
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
BASE = os.environ.get("VSPHERE_BASE", "https://localhost").rstrip("/")
USER = os.environ.get("VSPHERE_USER", "administrator@vsphere.local")
PASSWORD = os.environ.get("VSPHERE_PASSWORD", "VMware1!")
def rest_session() -> dict[str, str]:
response = requests.post(f"{BASE}/api/session", auth=(USER, PASSWORD), verify=False, timeout=60)
response.raise_for_status()
return {"vmware-api-session-id": response.json()}
def rest_lifecycle(headers: dict[str, str]) -> str:
created = requests.post(
f"{BASE}/api/vcenter/vm",
headers=headers,
json={
"name": "py-rest-lab-01",
"guest_OS": "OTHER_GUEST_64",
"placement": {
"folder": "group-v23",
"host": "host-11",
"datastore": "datastore-31",
"resource_pool": "resgroup-22",
},
"cpu": {"count": 1},
"memory": {"size_MiB": 512},
},
verify=False,
timeout=60,
)
created.raise_for_status()
vm = created.json()
power = requests.post(
f"{BASE}/api/vcenter/vm/{vm}/power",
params={"action": "start"},
headers=headers,
verify=False,
timeout=60,
)
power.raise_for_status()
assert "task" in power.json()
put = requests.put(
f"{BASE}/api/vcenter/vm/{vm}/guest/filesystem",
params={"path": "/tmp/python-marker"},
headers=headers,
json={"content": "ok"},
verify=False,
timeout=60,
)
put.raise_for_status()
requests.post(
f"{BASE}/api/vcenter/vm/{vm}/power",
params={"action": "stop"},
headers=headers,
verify=False,
timeout=60,
).raise_for_status()
requests.delete(
f"{BASE}/api/vcenter/vm/{vm}", headers=headers, verify=False, timeout=60
).raise_for_status()
return vm
def soap_create_vm(session_id: str) -> str:
envelope = """<?xml version="1.0"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Body>
<CreateVM_Task xmlns="urn:vim25">
<_this type="Folder">group-v23</_this>
<config>
<name>py-soap-lab-01</name>
<guestId>otherGuest64</guestId>
<numCPUs>1</numCPUs>
<memoryMB>512</memoryMB>
<files><vmPathName>[datastore1]</vmPathName></files>
</config>
<pool type="ResourcePool">resgroup-22</pool>
<host type="HostSystem">host-11</host>
</CreateVM_Task>
</soapenv:Body>
</soapenv:Envelope>"""
response = requests.post(
f"{BASE}/sdk",
data=envelope,
headers={
"Content-Type": "text/xml",
"vmware-api-session-id": session_id,
"Cookie": f'vmware_soap_session="{session_id}"',
},
verify=False,
timeout=60,
)
response.raise_for_status()
root = ET.fromstring(response.text)
task = None
for node in root.iter():
if node.tag.endswith("returnval") and (node.text or "").startswith("task-"):
task = node.text
break
assert task, response.text
return task
def main() -> int:
headers = rest_session()
rest_vm = rest_lifecycle(headers)
soap_task = soap_create_vm(headers["vmware-api-session-id"])
print(f"REST VM lifecycle ok: {rest_vm}")
print(f"SOAP CreateVM_Task ok: {soap_task}")
return 0
if __name__ == "__main__":
sys.exit(main())
+58
View File
@@ -0,0 +1,58 @@
#!/usr/bin/env python3
"""Smoke the native vSphere REST surface against a running simulator."""
from __future__ import annotations
import json
import sys
import urllib.error
import urllib.request
from base64 import b64encode
BASE = sys.argv[1] if len(sys.argv) > 1 else "https://localhost"
USER = "administrator@vsphere.local"
PASSWORD = "VMware1!"
def _req(
method: str, path: str, *, headers: dict[str, str] | None = None, data: bytes | None = None
):
request = urllib.request.Request(
f"{BASE}{path}",
data=data,
method=method,
headers=headers or {},
)
ctx = None
if BASE.startswith("https://"):
import ssl
ctx = ssl._create_unverified_context() # noqa: S323 - lab self-signed
with urllib.request.urlopen(request, context=ctx) as response: # noqa: S310
body = response.read()
return response.status, dict(response.headers), body
def main() -> int:
basic = b64encode(f"{USER}:{PASSWORD}".encode()).decode()
status, headers, body = _req(
"POST",
"/api/session",
headers={"Authorization": f"Basic {basic}"},
)
print("session", status, body.decode())
session = json.loads(body.decode())
sess_headers = {"vmware-api-session-id": session}
status, _, vms = _req("GET", "/api/vcenter/vm", headers=sess_headers)
print("vms", status, vms.decode())
status, _, hosts = _req("GET", "/api/vcenter/host", headers=sess_headers)
print("hosts", status, hosts.decode())
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except urllib.error.URLError as error:
print(f"failed: {error}", file=sys.stderr)
raise SystemExit(1) from error
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Minimal SOAP /sdk smoke (RetrieveServiceContent + Login envelope)."""
from __future__ import annotations
import ssl
import sys
import urllib.error
import urllib.request
BASE = sys.argv[1] if len(sys.argv) > 1 else "https://localhost"
CONTENT = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:urn="urn:vim25">
<soapenv:Body>
<urn:RetrieveServiceContent>
<urn:_this type="ServiceInstance">ServiceInstance</urn:_this>
</urn:RetrieveServiceContent>
</soapenv:Body>
</soapenv:Envelope>
"""
LOGIN = """<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:urn="urn:vim25">
<soapenv:Body>
<urn:Login>
<urn:_this type="SessionManager">SessionManager</urn:_this>
<urn:userName>administrator@vsphere.local</urn:userName>
<urn:password>VMware1!</urn:password>
</urn:Login>
</soapenv:Body>
</soapenv:Envelope>
"""
def _post(path: str, body: str) -> tuple[int, str]:
req = urllib.request.Request(
f"{BASE}{path}",
data=body.encode(),
method="POST",
headers={"Content-Type": "text/xml; charset=utf-8", "SOAPAction": '""'},
)
ctx = ssl._create_unverified_context() if BASE.startswith("https://") else None # noqa: S323
with urllib.request.urlopen(req, context=ctx) as resp: # noqa: S310
return int(resp.status), resp.read().decode()
def main() -> int:
status, body = _post("/sdk", CONTENT)
print("RetrieveServiceContent", status, "ServiceContent" in body)
if "ServiceContent" not in body:
return 1
status, body = _post("/sdk", LOGIN)
login_ok = "LoginResponse" in body or "UserSession" in body or "key>" in body
print("Login", status, login_ok)
if not login_ok:
print(body[:400], file=sys.stderr)
return 1
wsdl = urllib.request.Request(f"{BASE}/sdk/vimService.wsdl")
ctx = ssl._create_unverified_context() if BASE.startswith("https://") else None # noqa: S323
with urllib.request.urlopen(wsdl, context=ctx) as resp: # noqa: S310
print("wsdl", resp.status, len(resp.read()))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except urllib.error.URLError as error:
print(f"failed: {error}", file=sys.stderr)
raise SystemExit(1) from error
+22
View File
@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/vsphere" {
version = "2.12.0"
constraints = "~> 2.8"
hashes = [
"h1:WqZLjrDFhrpS0Y0IdSrKGNE/iuJf3l38K8Wz+ZRlJLE=",
"zh:02eee1071636834570106f70df904807e4d5cff2cf378c9e1dcfbebbeef4411b",
"zh:0ea4ed072d6b9bd8517a1b5c1b06ec05fac6293376c94a9b4625a41ca0626117",
"zh:0fa82a384b25a58b65523e0ea4768fa1212b1f5cfc0c9379d31162454fedcc9d",
"zh:23cede3a4cb8080662dc0416f2ea6b43e6ca0907c3af0ddae544df923b878080",
"zh:2fd2e293115fe4a3ca5f3df051ef852315d5eada304ffa8caaa11d95d15f2ab0",
"zh:532577c8897109a23eb768d23479870ccc353987b9bf28fd6532c1adc2878743",
"zh:a575a9d5e0b08866fe78da9bf50afee29a3278a0f7e0c269f0626e4a4d25bba1",
"zh:c7461658ae2d81881e84a37b6e1c69b05fcee1110fac0b57ebf7ea1a0f5406f2",
"zh:d9af3f73391a02d8c1d0e7a0f7da72afff8d49445da8c98bf6e63ad799c5dbc9",
"zh:deeed7463f1f532bb3edf70c786c8e3eca0b255cb1c74bcf69a5978d4757ed2c",
"zh:ed21ac2b742326098818876125c3a8177b76ae01ce82a88238b786c8020ef151",
"zh:f4ff28277d68956d1adc94c5d6fbc109f4a389e8ebb707d24b99ab3113235428",
]
}
+69
View File
@@ -0,0 +1,69 @@
terraform {
required_providers {
vsphere = {
source = "hashicorp/vsphere"
version = "~> 2.8"
}
}
}
provider "vsphere" {
user = var.vsphere_user
password = var.vsphere_password
vsphere_server = var.vsphere_server
allow_unverified_ssl = true
}
data "vsphere_datacenter" "dc" {
name = var.datacenter
}
data "vsphere_datastore" "ds" {
name = var.datastore
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_compute_cluster" "cluster" {
name = var.cluster
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_network" "network" {
name = var.network
datacenter_id = data.vsphere_datacenter.dc.id
}
data "vsphere_virtual_machine" "web" {
name = var.vm_name
datacenter_id = data.vsphere_datacenter.dc.id
}
# Lab VM create (SOAP CreateVM_Task). Use a unique name per apply.
resource "vsphere_virtual_machine" "lab" {
count = var.create_lab_vm ? 1 : 0
name = var.lab_vm_name
resource_pool_id = data.vsphere_compute_cluster.cluster.resource_pool_id
datastore_id = data.vsphere_datastore.ds.id
num_cpus = 1
memory = 1024
guest_id = "otherGuest64"
wait_for_guest_net_timeout = 0
wait_for_guest_ip_timeout = 0
network_interface {
network_id = data.vsphere_network.network.id
}
disk {
label = "disk0"
size = 16
}
}
output "vm_id" {
value = data.vsphere_virtual_machine.web.id
}
output "lab_vm_id" {
value = try(vsphere_virtual_machine.lab[0].id, null)
}
+50
View File
@@ -0,0 +1,50 @@
variable "vsphere_server" {
type = string
default = "localhost"
}
variable "vsphere_user" {
type = string
default = "administrator@vsphere.local"
}
variable "vsphere_password" {
type = string
default = "VMware1!"
sensitive = true
}
variable "datacenter" {
type = string
default = "Datacenter"
}
variable "vm_name" {
type = string
default = "web-01"
}
variable "datastore" {
type = string
default = "datastore1"
}
variable "cluster" {
type = string
default = "Cluster"
}
variable "network" {
type = string
default = "VM Network"
}
variable "create_lab_vm" {
type = bool
default = false
}
variable "lab_vm_name" {
type = string
default = "tf-lab-01"
}