Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm packaging, API contract packs, and pytest/Pulumi coverage labs.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
**Language / Язык:** [English](README.md) | [Русский](README.ru.md)
|
||||
|
||||
# Runnable OpenStack examples
|
||||
|
||||
Companion code for [docs/clients.md](../docs/clients.md) and
|
||||
[docs/examples/overview.md](../docs/examples/overview.md).
|
||||
|
||||
This repo is an **OpenStack** API lab (not VMware). Use `openstack_*` Terraform
|
||||
resources / `pulumi_openstack` — not `vsphere_virtual_machine`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
make up
|
||||
make seed-demo # networks, images (cirros), flavors, …
|
||||
```
|
||||
|
||||
Default credentials: `admin` / `secret`, project `demo`, domain `Default`.
|
||||
Auth URL: `http://127.0.0.1:5000/v3`.
|
||||
|
||||
For local cookbooks, disable HTTP proxies (IDE sandboxes often inject one and
|
||||
break multi-port Keystone/Glance/Nova discovery):
|
||||
|
||||
```bash
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
|
||||
export NO_PROXY='*'
|
||||
```
|
||||
|
||||
## Full stack (Python + Ansible + Terraform + Pulumi)
|
||||
|
||||
```bash
|
||||
bash examples/run_iac_stack.sh
|
||||
```
|
||||
|
||||
| Step | Path | What it does |
|
||||
|---|---|---|
|
||||
| Python | `python/openstacksdk_cookbook.py` | net/subnet + server + volume via openstacksdk |
|
||||
| Ansible | `ansible/playbook.yml` | Keystone token + Nova/Neutron/Glance via `uri` |
|
||||
| Terraform | `terraform/main.tf` | `openstack_compute_instance_v2` + volume attach |
|
||||
| Pulumi | `pulumi/` | `pulumi_openstack` Instance + Network/Subnet |
|
||||
|
||||
## Other probes
|
||||
|
||||
| Path | Purpose |
|
||||
|---|---|
|
||||
| `python/openstack_smoke.py` | Multi-port GET smoke |
|
||||
| `python/openstack_conformance.py` | Write-path + UI contracts |
|
||||
| `python/openstack_surface_probe.py` | Full pack lifecycle probe |
|
||||
@@ -0,0 +1,48 @@
|
||||
**Language / Язык:** [English](README.md) | [Русский](README.ru.md)
|
||||
|
||||
# Исполняемые примеры OpenStack
|
||||
|
||||
Сопровождающий код к [docs/ru/clients.md](../docs/ru/clients.md) и
|
||||
[docs/ru/examples/overview.md](../docs/ru/examples/overview.md).
|
||||
|
||||
Этот репозиторий — **OpenStack** API lab (не VMware). Используйте ресурсы
|
||||
`openstack_*` Terraform / `pulumi_openstack` — не `vsphere_virtual_machine`.
|
||||
|
||||
## Требования
|
||||
|
||||
```bash
|
||||
make up
|
||||
make seed-demo # networks, images (cirros), flavors, …
|
||||
```
|
||||
|
||||
Учётные данные по умолчанию: `admin` / `secret`, проект `demo`, домен `Default`.
|
||||
Auth URL: `http://127.0.0.1:5000/v3`.
|
||||
|
||||
Для локальных cookbook'ов отключите HTTP-прокси (IDE-песочницы часто
|
||||
подставляют его и ломают multi-port discovery Keystone/Glance/Nova):
|
||||
|
||||
```bash
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
|
||||
export NO_PROXY='*'
|
||||
```
|
||||
|
||||
## Полный стек (Python + Ansible + Terraform + Pulumi)
|
||||
|
||||
```bash
|
||||
bash examples/run_iac_stack.sh
|
||||
```
|
||||
|
||||
| Шаг | Путь | Что делает |
|
||||
|---|---|---|
|
||||
| Python | `python/openstacksdk_cookbook.py` | net/subnet + server + volume через openstacksdk |
|
||||
| Ansible | `ansible/playbook.yml` | токен Keystone + Nova/Neutron/Glance через `uri` |
|
||||
| Terraform | `terraform/main.tf` | `openstack_compute_instance_v2` + volume attach |
|
||||
| Pulumi | `pulumi/` | `pulumi_openstack` Instance + Network/Subnet |
|
||||
|
||||
## Другие probe'ы
|
||||
|
||||
| Путь | Назначение |
|
||||
|---|---|
|
||||
| `python/openstack_smoke.py` | Multi-port GET smoke |
|
||||
| `python/openstack_conformance.py` | Write-path + UI contracts |
|
||||
| `python/openstack_surface_probe.py` | Полный lifecycle-probe пакета |
|
||||
@@ -0,0 +1,2 @@
|
||||
[local]
|
||||
localhost ansible_connection=local
|
||||
@@ -0,0 +1,176 @@
|
||||
---
|
||||
# OpenStack lab cookbook against openstack-api-simulator (Keystone :5000).
|
||||
# Uses ansible.builtin.uri so no galaxy collections are required.
|
||||
# ansible-playbook -i inventory.ini playbook.yml
|
||||
|
||||
- name: OpenStack API simulator cookbook
|
||||
hosts: local
|
||||
gather_facts: false
|
||||
vars:
|
||||
os_auth_url: "http://127.0.0.1:5000/v3"
|
||||
os_nova: "http://127.0.0.1:8774"
|
||||
os_neutron: "http://127.0.0.1:9696"
|
||||
os_glance: "http://127.0.0.1:9292"
|
||||
os_username: admin
|
||||
os_password: secret
|
||||
os_project: demo
|
||||
os_domain: Default
|
||||
server_name: "ansible-cookbook-vm"
|
||||
|
||||
tasks:
|
||||
- name: Authenticate to Keystone
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_auth_url }}/auth/tokens"
|
||||
method: POST
|
||||
body_format: json
|
||||
status_code: [201]
|
||||
body:
|
||||
auth:
|
||||
identity:
|
||||
methods: [password]
|
||||
password:
|
||||
user:
|
||||
name: "{{ os_username }}"
|
||||
domain: { name: "{{ os_domain }}" }
|
||||
password: "{{ os_password }}"
|
||||
scope:
|
||||
project:
|
||||
name: "{{ os_project }}"
|
||||
domain: { name: "{{ os_domain }}" }
|
||||
return_content: true
|
||||
register: auth
|
||||
|
||||
- name: Set token facts
|
||||
ansible.builtin.set_fact:
|
||||
os_token: "{{ auth.x_subject_token }}"
|
||||
os_project_id: "{{ auth.json.token.project.id }}"
|
||||
|
||||
- name: List images
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_glance }}/v2/images"
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
return_content: true
|
||||
register: images
|
||||
|
||||
- name: Pick boot image
|
||||
ansible.builtin.set_fact:
|
||||
image_id: "{{ (images.json.images | selectattr('name', 'search', 'cirros') | list | first).id }}"
|
||||
|
||||
- name: List networks
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_neutron }}/v2.0/networks"
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
return_content: true
|
||||
register: networks
|
||||
|
||||
- name: Pick demo network
|
||||
ansible.builtin.set_fact:
|
||||
network_id: "{{ (networks.json.networks | selectattr('name', 'equalto', 'demo-net') | list | first).id }}"
|
||||
|
||||
- name: Create network (ansible-managed)
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_neutron }}/v2.0/networks"
|
||||
method: POST
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
body_format: json
|
||||
status_code: [201, 200]
|
||||
body:
|
||||
network:
|
||||
name: "ansible-app-net"
|
||||
admin_state_up: true
|
||||
return_content: true
|
||||
register: net_create
|
||||
|
||||
- name: Create server
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers"
|
||||
method: POST
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
OpenStack-API-Version: "compute 2.79"
|
||||
body_format: json
|
||||
status_code: [202, 200, 201]
|
||||
body:
|
||||
server:
|
||||
name: "{{ server_name }}"
|
||||
flavorRef: "1"
|
||||
imageRef: "{{ image_id }}"
|
||||
networks:
|
||||
- uuid: "{{ network_id }}"
|
||||
metadata:
|
||||
managed_by: ansible
|
||||
return_content: true
|
||||
register: server_create
|
||||
|
||||
- name: Set server id
|
||||
ansible.builtin.set_fact:
|
||||
server_id: "{{ server_create.json.server.id }}"
|
||||
|
||||
- name: Show server
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers/{{ server_id }}"
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
return_content: true
|
||||
register: server_show
|
||||
|
||||
- name: Write server metadata
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers/{{ server_id }}/metadata"
|
||||
method: POST
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
body_format: json
|
||||
status_code: [200, 201]
|
||||
body:
|
||||
metadata:
|
||||
playbook: openstack-cookbook
|
||||
env: lab
|
||||
return_content: true
|
||||
|
||||
- name: Read metadata
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers/{{ server_id }}/metadata"
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
return_content: true
|
||||
register: meta
|
||||
|
||||
- name: Stop server
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers/{{ server_id }}/action"
|
||||
method: POST
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
body_format: json
|
||||
status_code: [202, 200, 204]
|
||||
body:
|
||||
"os-stop": null
|
||||
|
||||
- name: Delete server
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_nova }}/v2.1/servers/{{ server_id }}"
|
||||
method: DELETE
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
status_code: [204, 202, 200]
|
||||
|
||||
- name: Delete ansible network
|
||||
ansible.builtin.uri:
|
||||
url: "{{ os_neutron }}/v2.0/networks/{{ net_create.json.network.id }}"
|
||||
method: DELETE
|
||||
headers:
|
||||
X-Auth-Token: "{{ os_token }}"
|
||||
status_code: [204, 200]
|
||||
|
||||
- name: Summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
project_id: "{{ os_project_id }}"
|
||||
server_was: "{{ server_id }}"
|
||||
server_name: "{{ server_show.json.server.name }}"
|
||||
metadata: "{{ meta.json.metadata }}"
|
||||
images: "{{ images.json.images | length }}"
|
||||
@@ -0,0 +1,3 @@
|
||||
module example.com/proxmox-api-simulator-cookbook
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func env(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func main() {
|
||||
base := strings.TrimRight(env("PVE_BASE", "http://localhost:8006/api2/json"), "/")
|
||||
node := env("PVE_NODE", "pve01")
|
||||
vmid := env("PVE_VMID", "113")
|
||||
token := env("PVE_API_TOKEN", "root@pam!automation=automation-secret")
|
||||
auth := "PVEAPIToken=" + token
|
||||
|
||||
fmt.Printf("version: %v\n", call(base+"/version", "GET", auth, nil))
|
||||
upid := asString(call(base+"/nodes/"+node+"/qemu", "POST", auth, url.Values{
|
||||
"vmid": {vmid},
|
||||
"name": {"go-" + vmid},
|
||||
"cores": {"1"},
|
||||
"memory": {"512"},
|
||||
}))
|
||||
wait(base, node, auth, upid)
|
||||
upid = asString(call(base+"/nodes/"+node+"/qemu/"+vmid+"/status/start", "POST", auth, nil))
|
||||
wait(base, node, auth, upid)
|
||||
fmt.Printf("status: %v\n", call(base+"/nodes/"+node+"/qemu/"+vmid+"/status/current", "GET", auth, nil))
|
||||
upid = asString(call(base+"/nodes/"+node+"/qemu/"+vmid+"/status/stop", "POST", auth, nil))
|
||||
wait(base, node, auth, upid)
|
||||
upid = asString(call(base+"/nodes/"+node+"/qemu/"+vmid, "DELETE", auth, nil))
|
||||
wait(base, node, auth, upid)
|
||||
fmt.Println("ok")
|
||||
}
|
||||
|
||||
func wait(base, node, auth, upid string) {
|
||||
deadline := time.Now().Add(2 * time.Minute)
|
||||
for time.Now().Before(deadline) {
|
||||
status := call(base+"/nodes/"+node+"/tasks/"+url.PathEscape(upid)+"/status", "GET", auth, nil)
|
||||
if m, ok := status.(map[string]any); ok {
|
||||
if s, _ := m["status"].(string); s == "stopped" {
|
||||
return
|
||||
}
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
panic("timeout waiting for " + upid)
|
||||
}
|
||||
|
||||
func call(u, method, auth string, values url.Values) any {
|
||||
var body io.Reader
|
||||
if values != nil {
|
||||
body = strings.NewReader(values.Encode())
|
||||
}
|
||||
req, err := http.NewRequest(method, u, body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
req.Header.Set("Authorization", auth)
|
||||
if values != nil {
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
panic(fmt.Sprintf("%s %s: %s", method, u, b))
|
||||
}
|
||||
var envelope struct {
|
||||
Data any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &envelope); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return envelope.Data
|
||||
}
|
||||
|
||||
func asString(v any) string {
|
||||
s, ok := v.(string)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("expected string UPID, got %#v", v))
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Minimal Java 11+ cookbook against HTTP :8006 using an API token.
|
||||
*
|
||||
* javac Cookbook.java && java Cookbook
|
||||
*/
|
||||
public final class Cookbook {
|
||||
private static final HttpClient CLIENT = HttpClient.newHttpClient();
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
String base = env("PVE_BASE", "http://localhost:8006/api2/json");
|
||||
String node = env("PVE_NODE", "pve01");
|
||||
String vmid = env("PVE_VMID", "114");
|
||||
String token = env("PVE_API_TOKEN", "root@pam!automation=automation-secret");
|
||||
String auth = "PVEAPIToken=" + token;
|
||||
|
||||
System.out.println("version: " + data(get(base + "/version", auth)));
|
||||
String upid =
|
||||
data(
|
||||
form(
|
||||
base + "/nodes/" + node + "/qemu",
|
||||
auth,
|
||||
Map.of(
|
||||
"vmid", vmid,
|
||||
"name", "java-" + vmid,
|
||||
"cores", "1",
|
||||
"memory", "512")));
|
||||
waitTask(base, node, auth, upid);
|
||||
upid = data(form(base + "/nodes/" + node + "/qemu/" + vmid + "/status/start", auth, Map.of()));
|
||||
waitTask(base, node, auth, upid);
|
||||
System.out.println(
|
||||
"status: " + data(get(base + "/nodes/" + node + "/qemu/" + vmid + "/status/current", auth)));
|
||||
upid = data(form(base + "/nodes/" + node + "/qemu/" + vmid + "/status/stop", auth, Map.of()));
|
||||
waitTask(base, node, auth, upid);
|
||||
upid = data(delete(base + "/nodes/" + node + "/qemu/" + vmid, auth));
|
||||
waitTask(base, node, auth, upid);
|
||||
System.out.println("ok");
|
||||
}
|
||||
|
||||
private static void waitTask(String base, String node, String auth, String upid)
|
||||
throws Exception {
|
||||
long deadline = System.currentTimeMillis() + 120_000;
|
||||
String encoded = URLEncoder.encode(upid, StandardCharsets.UTF_8);
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
String body = get(base + "/nodes/" + node + "/tasks/" + encoded + "/status", auth);
|
||||
if (body.contains("\"status\":\"stopped\"") || body.contains("\"status\": \"stopped\"")) {
|
||||
return;
|
||||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
throw new IllegalStateException("timeout waiting for " + upid);
|
||||
}
|
||||
|
||||
private static String env(String key, String def) {
|
||||
String value = System.getenv(key);
|
||||
return value == null || value.isBlank() ? def : value;
|
||||
}
|
||||
|
||||
private static String get(String uri, String auth) throws IOException, InterruptedException {
|
||||
return send(
|
||||
HttpRequest.newBuilder(URI.create(uri))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.header("Authorization", auth)
|
||||
.GET()
|
||||
.build());
|
||||
}
|
||||
|
||||
private static String delete(String uri, String auth) throws IOException, InterruptedException {
|
||||
return send(
|
||||
HttpRequest.newBuilder(URI.create(uri))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.header("Authorization", auth)
|
||||
.DELETE()
|
||||
.build());
|
||||
}
|
||||
|
||||
private static String form(String uri, String auth, Map<String, String> fields)
|
||||
throws IOException, InterruptedException {
|
||||
String body =
|
||||
fields.entrySet().stream()
|
||||
.map(
|
||||
e ->
|
||||
URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
|
||||
+ "="
|
||||
+ URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
|
||||
.collect(Collectors.joining("&"));
|
||||
return send(
|
||||
HttpRequest.newBuilder(URI.create(uri))
|
||||
.timeout(Duration.ofSeconds(60))
|
||||
.header("Authorization", auth)
|
||||
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(body))
|
||||
.build());
|
||||
}
|
||||
|
||||
private static String send(HttpRequest request) throws IOException, InterruptedException {
|
||||
HttpResponse<String> response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (response.statusCode() >= 300) {
|
||||
throw new IOException(response.statusCode() + ": " + response.body());
|
||||
}
|
||||
return response.body();
|
||||
}
|
||||
|
||||
/** Extract Proxmox envelope data when it is a JSON string UPID. */
|
||||
private static String data(String body) {
|
||||
String marker = "\"data\":\"";
|
||||
int start = body.indexOf(marker);
|
||||
if (start >= 0) {
|
||||
start += marker.length();
|
||||
int end = body.indexOf('"', start);
|
||||
if (end > start) {
|
||||
return body.substring(start, end);
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env perl
|
||||
use strict;
|
||||
use warnings;
|
||||
use HTTP::Tiny;
|
||||
use JSON qw(decode_json encode_json);
|
||||
|
||||
sub uri_escape {
|
||||
my ($value) = @_;
|
||||
$value =~ s/([^A-Za-z0-9\-\._~])/sprintf('%%%02X', ord($1))/eg;
|
||||
return $value;
|
||||
}
|
||||
|
||||
my $base = $ENV{PVE_BASE} // 'http://localhost:8006/api2/json';
|
||||
my $node = $ENV{PVE_NODE} // 'pve01';
|
||||
my $vmid = $ENV{PVE_VMID} // '115';
|
||||
my $token = $ENV{PVE_API_TOKEN} // 'root@pam!automation=automation-secret';
|
||||
my $auth = "PVEAPIToken=$token";
|
||||
my $http = HTTP::Tiny->new(timeout => 60);
|
||||
|
||||
sub api {
|
||||
my ($method, $path, $body) = @_;
|
||||
my %opts = (headers => { Authorization => $auth });
|
||||
if (defined $body) {
|
||||
$opts{headers}{'Content-Type'} = 'application/x-www-form-urlencoded';
|
||||
$opts{content} = $body;
|
||||
}
|
||||
my $res = $http->request($method, "$base$path", \%opts);
|
||||
die "$method $path failed: $res->{status} $res->{content}\n" unless $res->{success};
|
||||
my $json = decode_json($res->{content});
|
||||
return $json->{data};
|
||||
}
|
||||
|
||||
sub wait_task {
|
||||
my ($upid) = @_;
|
||||
my $deadline = time + 120;
|
||||
while (time < $deadline) {
|
||||
my $status = api('GET', "/nodes/$node/tasks/" . uri_escape($upid) . '/status');
|
||||
return if ref $status eq 'HASH' && ($status->{status} // '') eq 'stopped';
|
||||
select(undef, undef, undef, 0.5);
|
||||
}
|
||||
die "timeout waiting for $upid\n";
|
||||
}
|
||||
|
||||
print "version: ", encode_json(api('GET', '/version')), "\n";
|
||||
my $upid = api('POST', "/nodes/$node/qemu", "vmid=$vmid&name=perl-$vmid&cores=1&memory=512");
|
||||
wait_task($upid);
|
||||
$upid = api('POST', "/nodes/$node/qemu/$vmid/status/start");
|
||||
wait_task($upid);
|
||||
print "status: ", encode_json(api('GET', "/nodes/$node/qemu/$vmid/status/current")), "\n";
|
||||
$upid = api('POST', "/nodes/$node/qemu/$vmid/status/stop");
|
||||
wait_task($upid);
|
||||
$upid = api('DELETE', "/nodes/$node/qemu/$vmid");
|
||||
wait_task($upid);
|
||||
print "ok\n";
|
||||
@@ -0,0 +1,2 @@
|
||||
requires 'HTTP::Tiny';
|
||||
requires 'JSON';
|
||||
@@ -0,0 +1,8 @@
|
||||
config:
|
||||
openstack:authUrl: http://127.0.0.1:5000/v3
|
||||
openstack:userName: admin
|
||||
openstack:password:
|
||||
secure: AAABANNOlcqFB+nL7EdsJpTICXXoUIhfYk6vimDAk+KJcI5V8UM=
|
||||
openstack:tenantName: demo
|
||||
openstack:domainName: Default
|
||||
openstack:region: RegionOne
|
||||
@@ -0,0 +1,3 @@
|
||||
name: openstack-api-simulator
|
||||
runtime: python
|
||||
description: Lab cookbook against openstack-api-simulator (OpenStack, not VMware/vSphere)
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Pulumi cookbook: OpenStack network + compute instance on the simulator.
|
||||
|
||||
Uses pulumi_openstack (not vsphere). Defaults target local Compose gateway.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pulumi
|
||||
from pulumi_openstack import compute, images, networking
|
||||
|
||||
config = pulumi.Config()
|
||||
# Provider picks up OS_* env vars; also set via Pulumi.yaml / pulumi config.
|
||||
|
||||
image = images.get_image(name="cirros", most_recent=True)
|
||||
demo_net = networking.get_network(name="demo-net")
|
||||
|
||||
app_net = networking.Network("pulumi-app-net", name="pulumi-app-net", admin_state_up=True)
|
||||
app_subnet = networking.Subnet(
|
||||
"pulumi-app-subnet",
|
||||
name="pulumi-app-subnet",
|
||||
network_id=app_net.id,
|
||||
cidr="10.77.0.0/24",
|
||||
ip_version=4,
|
||||
)
|
||||
|
||||
instance = compute.Instance(
|
||||
"pulumi-cookbook-vm",
|
||||
name="pulumi-cookbook-vm",
|
||||
flavor_id="1",
|
||||
image_id=image.id,
|
||||
networks=[compute.InstanceNetworkArgs(uuid=demo_net.id)],
|
||||
metadata={
|
||||
"managed_by": "pulumi",
|
||||
"stack": "openstack-api-simulator",
|
||||
},
|
||||
)
|
||||
|
||||
pulumi.export("image_id", image.id)
|
||||
pulumi.export("server_id", instance.id)
|
||||
pulumi.export("server_name", instance.name)
|
||||
pulumi.export("app_network_id", app_net.id)
|
||||
pulumi.export("app_subnet_id", app_subnet.id)
|
||||
@@ -0,0 +1,3 @@
|
||||
pulumi>=3.0
|
||||
pulumi-openstack>=5.0
|
||||
requests>=2.28
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write-path conformance sample: create → show → delete across core services."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from uuid import uuid4
|
||||
|
||||
HOST = os.environ.get("OS_HOST", "127.0.0.1")
|
||||
KEYSTONE = sys.argv[1] if len(sys.argv) > 1 else f"http://{HOST}:5000"
|
||||
|
||||
|
||||
def _u(port: int, path: str) -> str:
|
||||
return f"http://{HOST}:{port}{path}"
|
||||
|
||||
|
||||
def request(method: str, url: str, *, data: dict | None = None, token: str | None = None):
|
||||
body = None if data is None else json.dumps(data).encode()
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["X-Auth-Token"] = token
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as res:
|
||||
raw = res.read().decode()
|
||||
return res.status, dict(res.headers), json.loads(raw) if raw else None
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read().decode()
|
||||
try:
|
||||
parsed = json.loads(raw) if raw else None
|
||||
except json.JSONDecodeError:
|
||||
parsed = raw
|
||||
return exc.code, dict(exc.headers), parsed
|
||||
except urllib.error.URLError as exc:
|
||||
return 0, {}, {"error": str(exc.reason)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
# Allow full URL host override via argv keystone URL.
|
||||
global HOST
|
||||
if KEYSTONE.startswith("http"):
|
||||
# http://api-gateway:5000 → api-gateway
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(KEYSTONE)
|
||||
if parsed.hostname:
|
||||
HOST = parsed.hostname
|
||||
|
||||
auth = {
|
||||
"auth": {
|
||||
"identity": {
|
||||
"methods": ["password"],
|
||||
"password": {
|
||||
"user": {"name": "admin", "domain": {"name": "Default"}, "password": "secret"}
|
||||
},
|
||||
},
|
||||
"scope": {"project": {"name": "demo", "domain": {"name": "Default"}}},
|
||||
}
|
||||
}
|
||||
status, headers, body = request("POST", f"{KEYSTONE.rstrip('/')}/v3/auth/tokens", data=auth)
|
||||
token = headers.get("X-Subject-Token") or headers.get("x-subject-token")
|
||||
if not token and isinstance(body, dict):
|
||||
token = (body.get("token") or {}).get("id")
|
||||
if status != 201 or not token:
|
||||
print("auth failed", status, body)
|
||||
return 1
|
||||
project_id = (body or {}).get("token", {}).get("project", {}).get("id")
|
||||
failed = 0
|
||||
|
||||
name = f"conf-{uuid4().hex[:8]}"
|
||||
st, _, created = request(
|
||||
"POST",
|
||||
_u(9311, "/v1/secrets"),
|
||||
token=token,
|
||||
data={"secret": {"name": name, "payload_content_type": "text/plain"}},
|
||||
)
|
||||
print("barbican.create", st)
|
||||
sid = ((created or {}).get("secret") or {}).get("id")
|
||||
if st >= 400 or not sid:
|
||||
failed += 1
|
||||
else:
|
||||
st, _, _ = request("GET", _u(9311, f"/v1/secrets/{sid}"), token=token)
|
||||
print("barbican.show", st)
|
||||
if st >= 400:
|
||||
failed += 1
|
||||
st, _, _ = request("DELETE", _u(9311, f"/v1/secrets/{sid}"), token=token)
|
||||
print("barbican.delete", st)
|
||||
if st >= 400 and st != 204:
|
||||
failed += 1
|
||||
|
||||
st, _, sgs = request("GET", _u(9696, "/v2.0/security-groups"), token=token)
|
||||
sg_id = ((sgs or {}).get("security_groups") or [{}])[0].get("id")
|
||||
if sg_id:
|
||||
st, _, rule = request(
|
||||
"POST",
|
||||
_u(9696, "/v2.0/security-group-rules"),
|
||||
token=token,
|
||||
data={
|
||||
"security_group_rule": {
|
||||
"security_group_id": sg_id,
|
||||
"direction": "ingress",
|
||||
"protocol": "tcp",
|
||||
"port_range_min": 8080,
|
||||
"port_range_max": 8080,
|
||||
"ethertype": "IPv4",
|
||||
"remote_ip_prefix": "0.0.0.0/0",
|
||||
}
|
||||
},
|
||||
)
|
||||
print(
|
||||
"neutron.sg_rule.create", st, ((rule or {}).get("security_group_rule") or {}).get("id")
|
||||
)
|
||||
if st >= 400:
|
||||
failed += 1
|
||||
|
||||
st, _, servers = request("GET", _u(8774, "/v2.1/servers"), token=token)
|
||||
server_id = ((servers or {}).get("servers") or [{}])[0].get("id")
|
||||
if server_id:
|
||||
req = urllib.request.Request(
|
||||
_u(8774, f"/v2.1/servers/{server_id}/action"),
|
||||
data=json.dumps({"os-getConsoleOutput": {"length": 20}}).encode(),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"X-Auth-Token": token,
|
||||
"OpenStack-API-Version": "compute 2.79",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as res:
|
||||
print("nova.console", res.status)
|
||||
except urllib.error.HTTPError as exc:
|
||||
print("nova.console", exc.code)
|
||||
failed += 1
|
||||
|
||||
if project_id:
|
||||
st, _, stacks = request("GET", _u(8004, f"/v1/{project_id}/stacks"), token=token)
|
||||
print("heat.stacks", st, len((stacks or {}).get("stacks") or []))
|
||||
if st >= 400:
|
||||
failed += 1
|
||||
|
||||
st, _, contracts = request("GET", _u(5000, "/ui/api/openstack/contracts"))
|
||||
print("ui.contracts", st, (contracts or {}).get("active", {}).get("operation_count"))
|
||||
if st != 200 or not (contracts or {}).get("active", {}).get("operation_count"):
|
||||
failed += 1
|
||||
|
||||
if failed:
|
||||
print(f"FAILED checks={failed}")
|
||||
return 1
|
||||
print("OK conformance write-paths")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Full-surface smoke: Keystone token → every default-port OpenStack service."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
HOST = os.environ.get("OS_HOST", "127.0.0.1")
|
||||
KEYSTONE = sys.argv[1] if len(sys.argv) > 1 else f"http://{HOST}:5000"
|
||||
|
||||
|
||||
def _u(port: int, path: str) -> str:
|
||||
return f"http://{HOST}:{port}{path}"
|
||||
|
||||
|
||||
# (label, url, expected_json_key or None for version-only)
|
||||
CHECKS: list[tuple[str, str, str | None]] = [
|
||||
("nova.servers", _u(8774, "/v2.1/servers/detail"), "servers"),
|
||||
("nova.flavors", _u(8774, "/v2.1/flavors"), "flavors"),
|
||||
("nova.keypairs", _u(8774, "/v2.1/os-keypairs"), "keypairs"),
|
||||
("nova.az", _u(8774, "/v2.1/os-availability-zone"), "availabilityZoneInfo"),
|
||||
("nova.hypervisors", _u(8774, "/v2.1/os-hypervisors"), "hypervisors"),
|
||||
("neutron.networks", _u(9696, "/v2.0/networks"), "networks"),
|
||||
("neutron.routers", _u(9696, "/v2.0/routers"), "routers"),
|
||||
("neutron.sg", _u(9696, "/v2.0/security-groups"), "security_groups"),
|
||||
("neutron.fips", _u(9696, "/v2.0/floatingips"), "floatingips"),
|
||||
("glance.images", _u(9292, "/v2/images"), "images"),
|
||||
("cinder.volumes", _u(8776, "/v3/volumes/detail"), "volumes"),
|
||||
("placement.rp", _u(8003, "/resource_providers"), "resource_providers"),
|
||||
("heat.stacks", _u(8004, "/v1/demo/stacks"), "stacks"),
|
||||
("swift.info", _u(8080, "/info"), None),
|
||||
("ironic.nodes", _u(6385, "/v1/nodes"), "nodes"),
|
||||
("octavia.lbs", _u(9876, "/v2/lbaas/loadbalancers"), "loadbalancers"),
|
||||
("barbican.secrets", _u(9311, "/v1/secrets"), "secrets"),
|
||||
("manila.shares", _u(8786, "/v2/shares"), "shares"),
|
||||
("designate.zones", _u(9001, "/v2/zones"), "zones"),
|
||||
("magnum.clusters", _u(9511, "/v1/clusters"), "clusters"),
|
||||
("zun.containers", _u(9517, "/v1/containers"), "containers"),
|
||||
("trove.instances", _u(8779, "/v1.0/instances"), "instances"),
|
||||
("mistral.workflows", _u(8989, "/v2/workflows"), "workflows"),
|
||||
("aodh.alarms", _u(8042, "/v2/alarms"), "alarms"),
|
||||
("freezer.jobs", _u(9090, "/v2/jobs"), "jobs"),
|
||||
("blazar.leases", _u(1234, "/leases"), "leases"),
|
||||
("vitrage.alarms", _u(8999, "/v1/alarm"), "alarms"),
|
||||
("masakari.segments", _u(15868, "/v1/segments"), "segments"),
|
||||
("tacker.vnfs", _u(9890, "/v1.0/vnfs"), "vnfs"),
|
||||
("adjutant.tasks", _u(5050, "/v1/tasks"), "tasks"),
|
||||
("cloudkitty.services", _u(8889, "/v1/rating/module_config/hashmap/services"), "services"),
|
||||
("heat-cfn.stacks", _u(8000, "/stacks"), "Stacks"),
|
||||
]
|
||||
|
||||
|
||||
def request(
|
||||
method: str,
|
||||
url: str,
|
||||
*,
|
||||
data: dict | None = None,
|
||||
token: str | None = None,
|
||||
extra_headers: dict[str, str] | None = None,
|
||||
):
|
||||
body = None if data is None else json.dumps(data).encode()
|
||||
headers = {"Accept": "application/json"}
|
||||
if data is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
if token:
|
||||
headers["X-Auth-Token"] = token
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
req = urllib.request.Request(url, data=body, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as res:
|
||||
raw = res.read().decode()
|
||||
return res.status, dict(res.headers), json.loads(raw) if raw else None
|
||||
except urllib.error.HTTPError as exc:
|
||||
raw = exc.read().decode()
|
||||
try:
|
||||
parsed = json.loads(raw) if raw else None
|
||||
except json.JSONDecodeError:
|
||||
parsed = raw
|
||||
return exc.code, dict(exc.headers), parsed
|
||||
except urllib.error.URLError as exc:
|
||||
return 0, {}, {"error": str(exc.reason)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
auth = {
|
||||
"auth": {
|
||||
"identity": {
|
||||
"methods": ["password"],
|
||||
"password": {
|
||||
"user": {
|
||||
"name": "admin",
|
||||
"domain": {"name": "Default"},
|
||||
"password": "secret",
|
||||
}
|
||||
},
|
||||
},
|
||||
"scope": {"project": {"name": "demo", "domain": {"name": "Default"}}},
|
||||
}
|
||||
}
|
||||
status, headers, body = request("POST", f"{KEYSTONE}/v3/auth/tokens", data=auth)
|
||||
token = headers.get("X-Subject-Token") or headers.get("x-subject-token")
|
||||
print("auth", status, "token", bool(token))
|
||||
if status != 201 or not token:
|
||||
print(body)
|
||||
return 1
|
||||
catalog = (body or {}).get("token", {}).get("catalog", [])
|
||||
print("catalog_services", len(catalog), sorted(s.get("name") for s in catalog))
|
||||
|
||||
# Microversion header round-trip on Nova
|
||||
st, hdrs, _ = request(
|
||||
"GET",
|
||||
_u(8774, "/v2.1/servers"),
|
||||
token=token,
|
||||
extra_headers={"OpenStack-API-Version": "compute 2.79"},
|
||||
)
|
||||
mv = hdrs.get("OpenStack-API-Version") or hdrs.get("openstack-api-version")
|
||||
print("nova.microversion", st, mv)
|
||||
if st >= 400:
|
||||
return 1
|
||||
|
||||
failed = 0
|
||||
for label, url, key in CHECKS:
|
||||
# Heat needs project id in path — fetch from token
|
||||
if label == "heat.stacks":
|
||||
project_id = (body or {}).get("token", {}).get("project", {}).get("id")
|
||||
if project_id:
|
||||
url = _u(8004, f"/v1/{project_id}/stacks")
|
||||
st, _, payload = request("GET", url, token=token)
|
||||
if key is None:
|
||||
print(label, st)
|
||||
else:
|
||||
items = (payload or {}).get(key)
|
||||
count = (
|
||||
len(items)
|
||||
if isinstance(items, list)
|
||||
else ("ok" if items is not None else "missing")
|
||||
)
|
||||
print(label, st, "count", count)
|
||||
if st == 0 or st >= 400:
|
||||
print(" FAIL", payload)
|
||||
failed += 1
|
||||
|
||||
# Root discovery per port
|
||||
for port, name in [(5000, "keystone"), (8774, "nova"), (6385, "ironic"), (8080, "swift")]:
|
||||
st, _, payload = request("GET", _u(port, "/"))
|
||||
print(f"root.{name}", st, list((payload or {}).keys())[:3])
|
||||
|
||||
if failed:
|
||||
print(f"FAILED {failed}/{len(CHECKS)}")
|
||||
return 1
|
||||
print("OK", len(CHECKS), "service checks")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Probe every pack operation for Yoga → Dalmatian against the live gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Allow `python examples/python/openstack_surface_probe.py` from repo / container.
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
from app.openstack.surface_probe import format_report, probe_series # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--host", default=os.environ.get("OS_HOST", "http://127.0.0.1:5000"))
|
||||
parser.add_argument(
|
||||
"--series",
|
||||
action="append",
|
||||
help="Limit to series (repeatable). Default: all four.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--collections-only",
|
||||
action="store_true",
|
||||
help="Only GET endpoints without path parameters (faster smoke).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-lifecycle",
|
||||
action="store_true",
|
||||
help="Random-UUID shallow probe (accepts 404) instead of create→CRUD lifecycle.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--methods",
|
||||
default="",
|
||||
help="Comma-separated methods filter (e.g. GET,POST)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
series_list = args.series or ["yoga", "antelope", "caracal", "dalmatian"]
|
||||
methods = frozenset(m.strip().upper() for m in args.methods.split(",") if m.strip()) or None
|
||||
failed = 0
|
||||
for series in series_list:
|
||||
report = probe_series(
|
||||
series,
|
||||
host=args.host,
|
||||
methods=methods,
|
||||
collections_only=args.collections_only,
|
||||
lifecycle=not args.no_lifecycle and not args.collections_only,
|
||||
)
|
||||
print(format_report(report))
|
||||
failed += len(report.failures)
|
||||
if failed:
|
||||
print(f"FAILED total={failed}")
|
||||
return 1
|
||||
print("OK all probed operations returned acceptable statuses")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
"""OpenStack SDK cookbook against openstack-api-simulator.
|
||||
|
||||
Creates network + server + volume, updates metadata, cleans up.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import openstack
|
||||
|
||||
|
||||
def main() -> int:
|
||||
conn = openstack.connect(
|
||||
auth_url="http://127.0.0.1:5000/v3",
|
||||
project_name="demo",
|
||||
username="admin",
|
||||
password="secret",
|
||||
user_domain_name="Default",
|
||||
project_domain_name="Default",
|
||||
region_name="RegionOne",
|
||||
)
|
||||
|
||||
print("identity ok:", conn.identity.get_project(conn.current_project_id).name)
|
||||
|
||||
image = conn.image.find_image("cirros", ignore_missing=False)
|
||||
network = conn.network.find_network("demo-net", ignore_missing=False)
|
||||
print("boot image:", image.id, image.name)
|
||||
print("network:", network.id, network.name)
|
||||
|
||||
app_net = conn.network.create_network(name="sdk-app-net", admin_state_up=True)
|
||||
app_subnet = conn.network.create_subnet(
|
||||
name="sdk-app-subnet",
|
||||
network_id=app_net.id,
|
||||
ip_version=4,
|
||||
cidr="10.88.0.0/24",
|
||||
)
|
||||
print("created net/subnet:", app_net.id, app_subnet.id)
|
||||
|
||||
server = conn.compute.create_server(
|
||||
name="sdk-cookbook-vm",
|
||||
flavor_id="1",
|
||||
image_id=image.id,
|
||||
networks=[{"uuid": network.id}],
|
||||
metadata={"managed_by": "openstacksdk"},
|
||||
)
|
||||
server = conn.compute.wait_for_server(server, status="ACTIVE", failures=["ERROR"], wait=60)
|
||||
print("server ACTIVE:", server.id, server.name, server.status)
|
||||
|
||||
conn.compute.set_server_metadata(server, playbook="sdk", env="lab")
|
||||
server = conn.compute.get_server(server.id)
|
||||
print("metadata:", dict(server.metadata or {}))
|
||||
|
||||
volume = conn.block_storage.create_volume(name="sdk-cookbook-vol", size=5)
|
||||
volume = conn.block_storage.wait_for_status(volume, status="available", wait=60)
|
||||
print("volume:", volume.id, volume.status)
|
||||
|
||||
conn.compute.delete_server(server, ignore_missing=True)
|
||||
print("server deleted")
|
||||
|
||||
conn.block_storage.delete_volume(volume, ignore_missing=True)
|
||||
print("volume deleted")
|
||||
|
||||
conn.network.delete_subnet(app_subnet, ignore_missing=True)
|
||||
conn.network.delete_network(app_net, ignore_missing=True)
|
||||
print("network cleaned")
|
||||
print("OPENSTACKSDK_COOKBOOK_OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print("OPENSTACKSDK_COOKBOOK_FAIL:", exc, file=sys.stderr)
|
||||
raise
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""proxmoxer cookbook against the local HTTPS gateway."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from proxmoxer import ProxmoxAPI
|
||||
|
||||
|
||||
def wait_task(proxmox: ProxmoxAPI, node: str, upid: str, timeout: float = 120.0) -> None:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
status = proxmox.nodes(node).tasks(upid).status.get()
|
||||
if status.get("status") == "stopped":
|
||||
exitstatus = status.get("exitstatus", "")
|
||||
if exitstatus not in ("OK", "ok", None, ""):
|
||||
# Proxmox uses exitstatus "OK" on success; accept empty for lab.
|
||||
if str(exitstatus).upper() != "OK":
|
||||
raise RuntimeError(f"task failed: {status}")
|
||||
return
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError(upid)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
host = os.environ.get("PVE_HOST", "localhost")
|
||||
port = int(os.environ.get("PVE_PORT", "8007"))
|
||||
user = os.environ.get("PVE_USER", "root@pam")
|
||||
node = os.environ.get("PVE_NODE", "pve01")
|
||||
vmid = int(os.environ.get("PVE_VMID", "110"))
|
||||
|
||||
token_name = os.environ.get("PVE_TOKEN_NAME")
|
||||
token_value = os.environ.get("PVE_TOKEN_VALUE")
|
||||
if token_name and token_value:
|
||||
proxmox = ProxmoxAPI(
|
||||
host,
|
||||
user=user,
|
||||
token_name=token_name,
|
||||
token_value=token_value,
|
||||
port=port,
|
||||
verify_ssl=False,
|
||||
)
|
||||
else:
|
||||
proxmox = ProxmoxAPI(
|
||||
host,
|
||||
user=user,
|
||||
password=os.environ.get("PVE_PASSWORD", "secret"),
|
||||
port=port,
|
||||
verify_ssl=False,
|
||||
)
|
||||
|
||||
print("version:", proxmox.version.get())
|
||||
print("nodes:", proxmox.nodes.get())
|
||||
print("qemu:", proxmox.nodes(node).qemu.get())
|
||||
|
||||
upid = proxmox.nodes(node).qemu.post(
|
||||
vmid=vmid,
|
||||
name=f"cookbook-{vmid}",
|
||||
cores=1,
|
||||
memory=512,
|
||||
)
|
||||
print("create:", upid)
|
||||
wait_task(proxmox, node, upid)
|
||||
|
||||
upid = proxmox.nodes(node).qemu(vmid).status.start.post()
|
||||
print("start:", upid)
|
||||
wait_task(proxmox, node, upid)
|
||||
print("status:", proxmox.nodes(node).qemu(vmid).status.current.get())
|
||||
|
||||
upid = proxmox.nodes(node).qemu(vmid).status.stop.post()
|
||||
print("stop:", upid)
|
||||
wait_task(proxmox, node, upid)
|
||||
|
||||
upid = proxmox.nodes(node).qemu(vmid).delete()
|
||||
print("delete:", upid)
|
||||
wait_task(proxmox, node, upid)
|
||||
print("ok")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Raw requests cookbook against HTTP :8006."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
|
||||
BASE = os.environ.get("PVE_BASE", "http://localhost:8006/api2/json")
|
||||
NODE = os.environ.get("PVE_NODE", "pve01")
|
||||
VMID = int(os.environ.get("PVE_VMID", "111"))
|
||||
TOKEN = os.environ.get(
|
||||
"PVE_API_TOKEN",
|
||||
"root@pam!automation=automation-secret",
|
||||
)
|
||||
|
||||
|
||||
def api(
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
headers: dict[str, str] | None = None,
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
response = requests.request(
|
||||
method,
|
||||
f"{BASE}{path}",
|
||||
headers=headers,
|
||||
data=data,
|
||||
timeout=60,
|
||||
)
|
||||
response.raise_for_status()
|
||||
body = response.json()
|
||||
return body.get("data", body)
|
||||
|
||||
|
||||
def wait_task(headers: dict[str, str], upid: str, timeout: float = 120.0) -> None:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
status = api("GET", f"/nodes/{NODE}/tasks/{upid}/status", headers=headers)
|
||||
if status.get("status") == "stopped":
|
||||
return
|
||||
time.sleep(0.5)
|
||||
raise TimeoutError(upid)
|
||||
|
||||
|
||||
def with_token() -> dict[str, str]:
|
||||
return {"Authorization": f"PVEAPIToken={TOKEN}"}
|
||||
|
||||
|
||||
def with_ticket() -> dict[str, str]:
|
||||
data = api(
|
||||
"POST",
|
||||
"/access/ticket",
|
||||
data={
|
||||
"username": os.environ.get("PVE_USER", "root@pam"),
|
||||
"password": os.environ.get("PVE_PASSWORD", "secret"),
|
||||
},
|
||||
)
|
||||
return {
|
||||
"Cookie": f"PVEAuthCookie={data['ticket']}",
|
||||
"CSRFPreventionToken": data["CSRFPreventionToken"],
|
||||
}
|
||||
|
||||
|
||||
def cookbook(headers: dict[str, str], label: str) -> None:
|
||||
print(label, "version:", api("GET", "/version", headers=headers))
|
||||
print(label, "qemu:", api("GET", f"/nodes/{NODE}/qemu", headers=headers))
|
||||
upid = api(
|
||||
"POST",
|
||||
f"/nodes/{NODE}/qemu",
|
||||
headers=headers,
|
||||
data={"vmid": VMID, "name": f"req-{VMID}", "cores": 1, "memory": 512},
|
||||
)
|
||||
wait_task(headers, upid)
|
||||
upid = api("POST", f"/nodes/{NODE}/qemu/{VMID}/status/start", headers=headers)
|
||||
wait_task(headers, upid)
|
||||
print(
|
||||
label, "status:", api("GET", f"/nodes/{NODE}/qemu/{VMID}/status/current", headers=headers)
|
||||
)
|
||||
upid = api("POST", f"/nodes/{NODE}/qemu/{VMID}/status/stop", headers=headers)
|
||||
wait_task(headers, upid)
|
||||
upid = api("DELETE", f"/nodes/{NODE}/qemu/{VMID}", headers=headers)
|
||||
wait_task(headers, upid)
|
||||
print(label, "ok")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cookbook(with_token(), "token")
|
||||
# second VMID for ticket path
|
||||
global VMID
|
||||
VMID = int(os.environ.get("PVE_VMID_TICKET", "112"))
|
||||
cookbook(with_ticket(), "ticket")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
proxmoxer>=2.3,<3
|
||||
requests>=2.31
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run Python + Ansible + Terraform + Pulumi cookbooks against local simulator.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# Prefer CLT/system python (user site-packages) over Homebrew for cookbooks.
|
||||
export PATH="${HOME}/.local/bin:${HOME}/Library/Python/3.9/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:${PATH}"
|
||||
# Local lab must not go through IDE/sandbox HTTP proxies (breaks multi-port discovery).
|
||||
unset HTTP_PROXY HTTPS_PROXY ALL_PROXY http_proxy https_proxy all_proxy
|
||||
export NO_PROXY="*"
|
||||
export no_proxy="*"
|
||||
export OS_AUTH_URL="${OS_AUTH_URL:-http://127.0.0.1:5000/v3}"
|
||||
export OS_USERNAME="${OS_USERNAME:-admin}"
|
||||
export OS_PASSWORD="${OS_PASSWORD:-secret}"
|
||||
export OS_PROJECT_NAME="${OS_PROJECT_NAME:-demo}"
|
||||
export OS_USER_DOMAIN_NAME="${OS_USER_DOMAIN_NAME:-Default}"
|
||||
export OS_PROJECT_DOMAIN_NAME="${OS_PROJECT_DOMAIN_NAME:-Default}"
|
||||
export OS_IDENTITY_API_VERSION=3
|
||||
export OS_REGION_NAME="${OS_REGION_NAME:-RegionOne}"
|
||||
PYTHON="${PYTHON:-/usr/bin/python3}"
|
||||
if ! command -v "$PYTHON" >/dev/null 2>&1; then
|
||||
PYTHON=python3
|
||||
fi
|
||||
|
||||
cd "$ROOT"
|
||||
echo "== health =="
|
||||
curl -sf "$OS_AUTH_URL/../health/ready" >/dev/null || curl -sf "http://127.0.0.1:5000/health/ready"
|
||||
|
||||
echo "== ensure demo seed (networks/images) =="
|
||||
docker compose exec -T simulator python -m app.openstack.seed_cli --profile demo >/dev/null
|
||||
|
||||
echo "== 1) Python openstacksdk =="
|
||||
"$PYTHON" -m pip install -q --user openstacksdk >/dev/null 2>&1 || true
|
||||
"$PYTHON" examples/python/openstacksdk_cookbook.py
|
||||
|
||||
echo "== 2) Ansible =="
|
||||
ansible-playbook -i examples/ansible/inventory.ini examples/ansible/playbook.yml
|
||||
|
||||
if command -v terraform >/dev/null 2>&1; then
|
||||
echo "== 3) Terraform =="
|
||||
cd examples/terraform
|
||||
terraform init -input=false
|
||||
terraform apply -auto-approve -input=false
|
||||
terraform destroy -auto-approve -input=false
|
||||
cd "$ROOT"
|
||||
else
|
||||
echo "== 3) Terraform SKIPPED (terraform not installed) =="
|
||||
fi
|
||||
|
||||
if command -v pulumi >/dev/null 2>&1; then
|
||||
echo "== 4) Pulumi =="
|
||||
cd examples/pulumi
|
||||
"$PYTHON" -m pip install -q --user -r requirements.txt >/dev/null 2>&1 || "$PYTHON" -m pip install -q -r requirements.txt
|
||||
pulumi stack select dev --create 2>/dev/null || true
|
||||
pulumi config set openstack:authUrl "$OS_AUTH_URL"
|
||||
pulumi config set openstack:userName "$OS_USERNAME"
|
||||
pulumi config set --secret openstack:password "$OS_PASSWORD"
|
||||
pulumi config set openstack:tenantName "$OS_PROJECT_NAME"
|
||||
pulumi config set openstack:domainName "$OS_USER_DOMAIN_NAME"
|
||||
pulumi config set openstack:region "$OS_REGION_NAME"
|
||||
# Pulumi Python programs should use the same interpreter
|
||||
export PULUMI_PYTHON_CMD="$PYTHON"
|
||||
pulumi up --yes
|
||||
pulumi destroy --yes
|
||||
cd "$ROOT"
|
||||
else
|
||||
echo "== 4) Pulumi SKIPPED (pulumi CLI not installed; SDK cookbook covered by Python) =="
|
||||
echo " Install: brew install pulumi/tap/pulumi then re-run this script"
|
||||
fi
|
||||
|
||||
echo "IAC_STACK_DONE"
|
||||
Generated
+24
@@ -0,0 +1,24 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/terraform-provider-openstack/openstack" {
|
||||
version = "2.1.0"
|
||||
constraints = "~> 2.0"
|
||||
hashes = [
|
||||
"h1:FFgxjgOlyRstaP7vYdPpgai9q1U0T0OF9B4FF7ZknrM=",
|
||||
"zh:113661750398bf21c8fe36aade9fb6f5eb82b5bcd3bcd30bd37ac805d83398f4",
|
||||
"zh:1b3c26347b9cd61e413ee93c2f422cc3278a77f55fd3516eaabb3e2a85f65281",
|
||||
"zh:1b751bbf1e4152829a643b532fd3f5967a2e89a41fac381257e0b41665be3306",
|
||||
"zh:1b967bbfd9b344419c0e0df0c3a15fcbd731e91f19a18955a55aace8d9ec039a",
|
||||
"zh:1bc0fc7c0a21e568db043b654501ce668ba19bf7628d37a7d2aaa512fd6e5aeb",
|
||||
"zh:425cbf61757d4b503e7bf0f409ea59835ca3afbd2432d56ad552c2e5d234a572",
|
||||
"zh:67d4f059cb4d73bf6c060313ec32962c4e5bd8dc7be2542a6f2098ab32575cd9",
|
||||
"zh:7fe841ac5b68a4f52fb3cf45070828f3845de44746679d434e4349f3c23e3ef2",
|
||||
"zh:ac1ed4c6ef0b6a3410568a05d3f9933d184497f065988503c43da0b2f0786ab2",
|
||||
"zh:c5c0d14c86fabd9ab6a5d555e6a8d511942665fb5fa948dd452b0d1934068344",
|
||||
"zh:c9ae5c210192275185d6823566a9421983e8e64c2665a4cae00b92dd0706bd19",
|
||||
"zh:ee9865ccc053e7f345e532654fb628d1cf1e81cd2e929643c1691bebffcf7b98",
|
||||
"zh:f3416d2f666095e740522c4964e436470bb9ec17bd53aaae8169ad93297d07bd",
|
||||
"zh:fbca85457dd49e17168989d64f7cfc4a519d55ef4e00e89cea2859e87ad87f83",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
required_providers {
|
||||
openstack = {
|
||||
source = "terraform-provider-openstack/openstack"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# OpenStack lab against openstack-api-simulator (NOT vsphere_* / VMware).
|
||||
# Equivalent of a compute instance: openstack_compute_instance_v2
|
||||
|
||||
provider "openstack" {
|
||||
auth_url = var.auth_url
|
||||
user_name = var.user_name
|
||||
password = var.password
|
||||
tenant_name = var.project_name
|
||||
domain_name = var.domain_name
|
||||
region = var.region
|
||||
insecure = true
|
||||
}
|
||||
|
||||
variable "auth_url" {
|
||||
type = string
|
||||
default = "http://127.0.0.1:5000/v3"
|
||||
}
|
||||
|
||||
variable "user_name" {
|
||||
type = string
|
||||
default = "admin"
|
||||
}
|
||||
|
||||
variable "password" {
|
||||
type = string
|
||||
default = "secret"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "project_name" {
|
||||
type = string
|
||||
default = "demo"
|
||||
}
|
||||
|
||||
variable "domain_name" {
|
||||
type = string
|
||||
default = "Default"
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "RegionOne"
|
||||
}
|
||||
|
||||
variable "image_name" {
|
||||
type = string
|
||||
default = "cirros"
|
||||
}
|
||||
|
||||
variable "flavor_id" {
|
||||
type = string
|
||||
default = "1"
|
||||
}
|
||||
|
||||
data "openstack_images_image_v2" "boot" {
|
||||
name = var.image_name
|
||||
most_recent = true
|
||||
}
|
||||
|
||||
data "openstack_networking_network_v2" "private" {
|
||||
name = "demo-net"
|
||||
}
|
||||
|
||||
resource "openstack_networking_network_v2" "app" {
|
||||
name = "tf-app-net"
|
||||
admin_state_up = true
|
||||
}
|
||||
|
||||
resource "openstack_networking_subnet_v2" "app" {
|
||||
name = "tf-app-subnet"
|
||||
network_id = openstack_networking_network_v2.app.id
|
||||
cidr = "10.99.0.0/24"
|
||||
ip_version = 4
|
||||
}
|
||||
|
||||
resource "openstack_compute_instance_v2" "app" {
|
||||
name = "tf-cookbook-vm"
|
||||
flavor_id = var.flavor_id
|
||||
image_id = data.openstack_images_image_v2.boot.id
|
||||
|
||||
network {
|
||||
uuid = data.openstack_networking_network_v2.private.id
|
||||
}
|
||||
|
||||
metadata = {
|
||||
managed_by = "terraform"
|
||||
stack = "openstack-api-simulator"
|
||||
}
|
||||
}
|
||||
|
||||
resource "openstack_blockstorage_volume_v3" "data" {
|
||||
name = "tf-cookbook-vol"
|
||||
size = 10
|
||||
}
|
||||
|
||||
resource "openstack_compute_volume_attach_v2" "data" {
|
||||
instance_id = openstack_compute_instance_v2.app.id
|
||||
volume_id = openstack_blockstorage_volume_v3.data.id
|
||||
}
|
||||
|
||||
output "server_id" {
|
||||
value = openstack_compute_instance_v2.app.id
|
||||
}
|
||||
|
||||
output "server_name" {
|
||||
value = openstack_compute_instance_v2.app.name
|
||||
}
|
||||
|
||||
output "network_id" {
|
||||
value = openstack_networking_network_v2.app.id
|
||||
}
|
||||
|
||||
output "volume_id" {
|
||||
value = openstack_blockstorage_volume_v3.data.id
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.9.8",
|
||||
"serial": 33,
|
||||
"lineage": "1a96c121-a3b5-f759-513e-99d9ea2fbc67",
|
||||
"outputs": {},
|
||||
"resources": [],
|
||||
"check_results": null
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
{
|
||||
"version": 4,
|
||||
"terraform_version": "1.9.8",
|
||||
"serial": 25,
|
||||
"lineage": "1a96c121-a3b5-f759-513e-99d9ea2fbc67",
|
||||
"outputs": {
|
||||
"network_id": {
|
||||
"value": "e32feab4-8e14-49d6-9e17-7d9a6476f118",
|
||||
"type": "string"
|
||||
},
|
||||
"server_id": {
|
||||
"value": "12edaad6-b6e9-4dbd-a425-dbf5cc922989",
|
||||
"type": "string"
|
||||
},
|
||||
"server_name": {
|
||||
"value": "tf-cookbook-vm",
|
||||
"type": "string"
|
||||
},
|
||||
"volume_id": {
|
||||
"value": "64a899aa-48fd-43d8-9e2b-af8b0111b1f5",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"mode": "data",
|
||||
"type": "openstack_images_image_v2",
|
||||
"name": "boot",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"checksum": "",
|
||||
"container_format": "bare",
|
||||
"created_at": "2026-07-16T03:36:07Z",
|
||||
"disk_format": "qcow2",
|
||||
"file": "/v2/images/c58b99c0-2d7b-5842-b260-b617db2f7803/file",
|
||||
"hidden": false,
|
||||
"id": "c58b99c0-2d7b-5842-b260-b617db2f7803",
|
||||
"member_status": null,
|
||||
"metadata": {},
|
||||
"min_disk_gb": 0,
|
||||
"min_ram_mb": 0,
|
||||
"most_recent": true,
|
||||
"name": "cirros",
|
||||
"name_regex": null,
|
||||
"owner": "cfe100d7-d64c-530c-8286-4772dfea88ad",
|
||||
"properties": {},
|
||||
"protected": false,
|
||||
"region": "RegionOne",
|
||||
"schema": "/v2/schemas/image",
|
||||
"size_bytes": 13287936,
|
||||
"size_max": null,
|
||||
"size_min": null,
|
||||
"sort": "name:asc",
|
||||
"tag": null,
|
||||
"tags": [],
|
||||
"updated_at": "2026-07-16T03:36:07Z",
|
||||
"visibility": "public"
|
||||
},
|
||||
"sensitive_attributes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "data",
|
||||
"type": "openstack_networking_network_v2",
|
||||
"name": "private",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"admin_state_up": "true",
|
||||
"all_tags": [],
|
||||
"availability_zone_hints": [],
|
||||
"description": "",
|
||||
"dns_domain": "",
|
||||
"external": false,
|
||||
"id": "a245268b-88ba-597a-b8db-017810782f98",
|
||||
"matching_subnet_cidr": null,
|
||||
"mtu": 1450,
|
||||
"name": "demo-net",
|
||||
"network_id": null,
|
||||
"region": "RegionOne",
|
||||
"segments": [
|
||||
{
|
||||
"network_type": "vxlan",
|
||||
"physical_network": "",
|
||||
"segmentation_id": 0
|
||||
}
|
||||
],
|
||||
"shared": "false",
|
||||
"status": null,
|
||||
"subnets": [],
|
||||
"tags": null,
|
||||
"tenant_id": "8924e279-51c7-582e-b6f5-8e41b33e7581",
|
||||
"transparent_vlan": false
|
||||
},
|
||||
"sensitive_attributes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "openstack_blockstorage_volume_v3",
|
||||
"name": "data",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"attachment": [],
|
||||
"availability_zone": "",
|
||||
"backup_id": "",
|
||||
"consistency_group_id": null,
|
||||
"description": "",
|
||||
"enable_online_resize": null,
|
||||
"id": "64a899aa-48fd-43d8-9e2b-af8b0111b1f5",
|
||||
"image_id": null,
|
||||
"metadata": {},
|
||||
"name": "tf-cookbook-vol",
|
||||
"region": "RegionOne",
|
||||
"scheduler_hints": [],
|
||||
"size": 10,
|
||||
"snapshot_id": "",
|
||||
"source_replica": null,
|
||||
"source_vol_id": "",
|
||||
"timeouts": null,
|
||||
"volume_type": "lvmdriver-1"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfX0="
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "openstack_compute_instance_v2",
|
||||
"name": "app",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"access_ip_v4": "10.0.0.173",
|
||||
"access_ip_v6": "",
|
||||
"admin_pass": null,
|
||||
"all_metadata": {},
|
||||
"all_tags": [
|
||||
"demo"
|
||||
],
|
||||
"availability_zone": "",
|
||||
"availability_zone_hints": null,
|
||||
"block_device": [],
|
||||
"config_drive": null,
|
||||
"created": "2026-07-16 03:36:12 +0000 UTC",
|
||||
"flavor_id": "1",
|
||||
"flavor_name": "m1.tiny",
|
||||
"force_delete": false,
|
||||
"id": "12edaad6-b6e9-4dbd-a425-dbf5cc922989",
|
||||
"image_id": "c58b99c0-2d7b-5842-b260-b617db2f7803",
|
||||
"image_name": "cirros",
|
||||
"key_pair": "",
|
||||
"metadata": {
|
||||
"managed_by": "terraform",
|
||||
"stack": "openstack-api-simulator"
|
||||
},
|
||||
"name": "tf-cookbook-vm",
|
||||
"network": [
|
||||
{
|
||||
"access_network": false,
|
||||
"fixed_ip_v4": "10.0.0.173",
|
||||
"fixed_ip_v6": "",
|
||||
"mac": "fa:16:3e:12:ed:aa",
|
||||
"name": "demo-net",
|
||||
"port": "",
|
||||
"uuid": "a245268b-88ba-597a-b8db-017810782f98"
|
||||
}
|
||||
],
|
||||
"network_mode": null,
|
||||
"personality": [],
|
||||
"power_state": "active",
|
||||
"region": "RegionOne",
|
||||
"scheduler_hints": [],
|
||||
"security_groups": [],
|
||||
"stop_before_destroy": false,
|
||||
"tags": null,
|
||||
"timeouts": null,
|
||||
"updated": "2026-07-16 03:36:12 +0000 UTC",
|
||||
"user_data": null,
|
||||
"vendor_options": []
|
||||
},
|
||||
"sensitive_attributes": [
|
||||
[
|
||||
{
|
||||
"type": "get_attr",
|
||||
"value": "admin_pass"
|
||||
}
|
||||
]
|
||||
],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjoxODAwMDAwMDAwMDAwLCJkZWxldGUiOjE4MDAwMDAwMDAwMDAsInVwZGF0ZSI6MTgwMDAwMDAwMDAwMH19",
|
||||
"dependencies": [
|
||||
"data.openstack_images_image_v2.boot",
|
||||
"data.openstack_networking_network_v2.private"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "openstack_compute_volume_attach_v2",
|
||||
"name": "data",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"device": "/dev/vdb",
|
||||
"id": "12edaad6-b6e9-4dbd-a425-dbf5cc922989/1662ea63-0b2e-4a54-8d78-ad218134fd5b",
|
||||
"instance_id": "12edaad6-b6e9-4dbd-a425-dbf5cc922989",
|
||||
"multiattach": null,
|
||||
"region": "RegionOne",
|
||||
"tag": null,
|
||||
"timeouts": null,
|
||||
"vendor_options": [],
|
||||
"volume_id": "64a899aa-48fd-43d8-9e2b-af8b0111b1f5"
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfX0=",
|
||||
"dependencies": [
|
||||
"data.openstack_images_image_v2.boot",
|
||||
"data.openstack_networking_network_v2.private",
|
||||
"openstack_blockstorage_volume_v3.data",
|
||||
"openstack_compute_instance_v2.app"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "openstack_networking_network_v2",
|
||||
"name": "app",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"admin_state_up": true,
|
||||
"all_tags": [],
|
||||
"availability_zone_hints": [],
|
||||
"description": "",
|
||||
"dns_domain": "",
|
||||
"external": false,
|
||||
"id": "e32feab4-8e14-49d6-9e17-7d9a6476f118",
|
||||
"mtu": 1450,
|
||||
"name": "tf-app-net",
|
||||
"port_security_enabled": false,
|
||||
"qos_policy_id": "",
|
||||
"region": "RegionOne",
|
||||
"segments": [
|
||||
{
|
||||
"network_type": "vxlan",
|
||||
"physical_network": "",
|
||||
"segmentation_id": 0
|
||||
}
|
||||
],
|
||||
"shared": false,
|
||||
"tags": null,
|
||||
"tenant_id": "8924e279-51c7-582e-b6f5-8e41b33e7581",
|
||||
"timeouts": null,
|
||||
"transparent_vlan": false,
|
||||
"value_specs": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfX0="
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"mode": "managed",
|
||||
"type": "openstack_networking_subnet_v2",
|
||||
"name": "app",
|
||||
"provider": "provider[\"registry.terraform.io/terraform-provider-openstack/openstack\"]",
|
||||
"instances": [
|
||||
{
|
||||
"schema_version": 0,
|
||||
"attributes": {
|
||||
"all_tags": [],
|
||||
"allocation_pool": [],
|
||||
"cidr": "10.99.0.0/24",
|
||||
"description": "",
|
||||
"dns_nameservers": [
|
||||
"8.8.8.8"
|
||||
],
|
||||
"dns_publish_fixed_ip": false,
|
||||
"enable_dhcp": true,
|
||||
"gateway_ip": "",
|
||||
"id": "956c5f10-205b-484c-813a-ec5eafe02f1b",
|
||||
"ip_version": 4,
|
||||
"ipv6_address_mode": "",
|
||||
"ipv6_ra_mode": "",
|
||||
"name": "tf-app-subnet",
|
||||
"network_id": "e32feab4-8e14-49d6-9e17-7d9a6476f118",
|
||||
"no_gateway": true,
|
||||
"prefix_length": null,
|
||||
"region": "RegionOne",
|
||||
"service_types": [],
|
||||
"subnetpool_id": "",
|
||||
"tags": null,
|
||||
"tenant_id": "8924e279-51c7-582e-b6f5-8e41b33e7581",
|
||||
"timeouts": null,
|
||||
"value_specs": null
|
||||
},
|
||||
"sensitive_attributes": [],
|
||||
"private": "eyJlMmJmYjczMC1lY2FhLTExZTYtOGY4OC0zNDM2M2JjN2M0YzAiOnsiY3JlYXRlIjo2MDAwMDAwMDAwMDAsImRlbGV0ZSI6NjAwMDAwMDAwMDAwfX0=",
|
||||
"dependencies": [
|
||||
"openstack_networking_network_v2.app"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"check_results": null
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
auth_url = "http://127.0.0.1:5000/v3"
|
||||
user_name = "admin"
|
||||
password = "secret"
|
||||
project_name = "demo"
|
||||
domain_name = "Default"
|
||||
image_name = "cirros"
|
||||
flavor_id = "1"
|
||||
Reference in New Issue
Block a user