From 38a0b917a162496e55f6be0234acdab6cc570b85 Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Wed, 17 Jul 2024 02:59:54 +0300 Subject: [PATCH 1/3] implement openrpc-codegen tool: - create openrpc.json spec file that represent all zos types and api handlers - create a tool that generate go types/methods from the spec file with the limitation for the `net/rpc` package services --- openrpc.json | 1112 ++++++++++++++++++ tools/openrpc-codegen/Makefile | 3 + tools/openrpc-codegen/fileutils/parser.go | 22 + tools/openrpc-codegen/fileutils/writer.go | 27 + tools/openrpc-codegen/generator/generator.go | 132 +++ tools/openrpc-codegen/generator/templates.go | 39 + tools/openrpc-codegen/generator/utils.go | 53 + tools/openrpc-codegen/go.mod | 3 + tools/openrpc-codegen/go.sum | 0 tools/openrpc-codegen/main.go | 51 + tools/openrpc-codegen/readme.md | 52 + tools/openrpc-codegen/schema/types.go | 85 ++ 12 files changed, 1579 insertions(+) create mode 100644 openrpc.json create mode 100644 tools/openrpc-codegen/Makefile create mode 100644 tools/openrpc-codegen/fileutils/parser.go create mode 100644 tools/openrpc-codegen/fileutils/writer.go create mode 100644 tools/openrpc-codegen/generator/generator.go create mode 100644 tools/openrpc-codegen/generator/templates.go create mode 100644 tools/openrpc-codegen/generator/utils.go create mode 100644 tools/openrpc-codegen/go.mod create mode 100644 tools/openrpc-codegen/go.sum create mode 100644 tools/openrpc-codegen/main.go create mode 100644 tools/openrpc-codegen/readme.md create mode 100644 tools/openrpc-codegen/schema/types.go diff --git a/openrpc.json b/openrpc.json new file mode 100644 index 000000000..9cd6500a5 --- /dev/null +++ b/openrpc.json @@ -0,0 +1,1112 @@ +{ + "openrpc": "1.2.3", + "info": { + "title": "ZosRpcApi", + "description": "This is an API for interacting with the ZOS nodes.", + "version": "1.0.0" + }, + "methods": [ + { + "name": "zos.SystemVersion", + "params": [], + "result": { + "name": "Version", + "schema": { + "$ref": "#/components/schemas/Version" + } + } + }, + { + "name": "zos.SystemHypervisor", + "params": [], + "result": { + "name": "Hypervisor", + "schema": { + "type": "string" + } + } + }, + { + "name": "zos.SystemDiagnostics", + "params": [], + "result": { + "name": "Diagnostics", + "schema": { + "$ref": "#/components/schemas/Diagnostics" + } + } + }, + { + "name": "zos.SystemDmi", + "params": [], + "result": { + "name": "DMI", + "schema": { + "$ref": "#/components/schemas/DMI" + } + } + }, + { + "name": "zos.GpuList", + "params": [], + "result": { + "name": "Gpus", + "schema": { + "$ref": "#/components/schemas/GPUs" + } + } + }, + + { + "name": "zos.StorageMetrics", + "params": [], + "result": { + "name": "PoolMetrics", + "schema": { + "$ref": "#/components/schemas/PoolMetricsResult" + } + } + }, + { + "name": "zos.Statistics", + "params": [], + "result": { + "name": "Counters", + "schema": { + "$ref": "#/components/schemas/Counters" + } + } + }, + + { + "name": "zos.PerfGetCpuBench", + "params": [], + "result": { + "name": "CpuBenchTaskResult", + "schema": { + "$ref": "#/components/schemas/CpuBenchTaskResult" + } + } + }, + { + "name": "zos.PerfGetHealth", + "params": [], + "result": { + "name": "HealthTaskResult", + "schema": { + "$ref": "#/components/schemas/HealthTaskResult" + } + } + }, + { + "name": "zos.PerfGetIperf", + "params": [], + "result": { + "name": "IperfTaskResult", + "schema": { + "$ref": "#/components/schemas/IperfTaskResult" + } + } + }, + { + "name": "zos.PerfGetPublicIP", + "params": [], + "result": { + "name": "PublicIpTaskResult", + "schema": { + "$ref": "#/components/schemas/PublicIpTaskResult" + } + } + }, + { + "name": "zos.PerfGetAll", + "params": [], + "result": { + "name": "TaskResults", + "schema": { + "$ref": "#/components/schemas/AllTaskResult" + } + } + }, + + { + "name": "zos.NetworkPrivateIps", + "params": [ + { + "name": "networkName", + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "PrivateIps", + "schema": { + "$ref": "#/components/schemas/Ips" + } + } + }, + { + "name": "zos.NetworkPublicIps", + "params": [], + "result": { + "name": "PublicIps", + "schema": { + "$ref": "#/components/schemas/Ips" + } + } + }, + { + "name": "zos.NetworkHasIpv6", + "params": [], + "result": { + "name": "HasIpv6", + "schema": { + "type": "boolean" + } + } + }, + { + "name": "zos.NetworkInterfaces", + "params": [], + "result": { + "name": "Interface", + "schema": { + "$ref": "#/components/schemas/Interfaces" + } + } + }, + { + "name": "zos.NetworkPublicConfig", + "params": [], + "result": { + "name": "PublicConfig", + "schema": { + "$ref": "#/components/schemas/PublicConfig" + } + } + }, + { + "name": "zos.NetworkWGPorts", + "params": [], + "result": { + "name": "WGPorts", + "schema": { + "$ref": "#/components/schemas/WGPorts" + } + } + }, + + { + "name": "zos.AdminPublicNICSet", + "params": [ + { + "name": "iface", + "schema": { + "type": "string" + } + } + ], + "result": { + "name": "SetResult", + "schema": { + "type": "null" + } + } + }, + { + "name": "zos.AdminPublicNICGet", + "params": [], + "result": { + "name": "PublicNIC", + "schema": { + "$ref": "#/components/schemas/ExitDevice" + } + } + }, + { + "name": "zos.AdminInterfaces", + "params": [], + "result": { + "name": "Interfaces", + "schema": { + "$ref": "#/components/schemas/Interfaces" + } + } + }, + + { + "name": "zos.DeploymentList", + "params": [], + "result": { + "name": "Deployments", + "schema": { + "$ref": "#/components/schemas/Deployments" + } + } + }, + { + "name": "zos.DeploymentGet", + "params": [ + { + "name": "contractId", + "schema": { + "type": "integer" + } + } + ], + "result": { + "name": "Deployment", + "schema": { + "$ref": "#/components/schemas/Deployment" + } + } + }, + { + "name": "zos.DeploymentChanges", + "params": [ + { + "name": "contractId", + "schema": { + "type": "integer" + } + } + ], + "result": { + "name": "Workloads", + "schema": { + "$ref": "#/components/schemas/Workloads" + } + } + }, + { + "name": "zos.DeploymentUpdate", + "params": [ + { + "name": "deployment", + "schema": { + "$ref": "#/components/schemas/Deployment" + } + } + ], + "result": { + "name": "updateResult", + "schema": { + "type": "null" + } + } + }, + { + "name": "zos.DeploymentCreate", + "params": [ + { + "name": "deployment", + "schema": { + "$ref": "#/components/schemas/Deployment" + } + } + ], + "result": { + "name": "createResult", + "schema": { + "type": "null" + } + } + } + ], + "components": { + "schemas": { + "Version": { + "type": "object", + "properties": { + "Zinit": { + "tag": "zinit", + "type": "string" + }, + "Zos": { + "tag": "zos", + "type": "string" + } + } + }, + "DMI": { + "type": "object", + "properties": { + "Tooling": { + "tag": "tooling", + "$ref": "#/components/schemas/Tooling" + }, + "Sections": { + "tag": "sections", + "type": "array", + "items": { + "$ref": "#/components/schemas/Section" + } + } + } + }, + "Tooling": { + "type": "object", + "properties": { + "Aggregator": { + "tag": "aggregator", + "type": "string" + }, + "Decoder": { + "tag": "decoder", + "type": "string" + } + } + }, + "Section": { + "type": "object", + "properties": { + "HandleLine": { + "tag": "handleline", + "type": "string" + }, + "TypeStr": { + "tag": "typestr", + "type": "string" + }, + "Type": { + "tag": "typenum", + "type": "integer" + }, + "SubSections": { + "tag": "subsections", + "type": "array", + "items": { + "$ref": "#/components/schemas/SubSection" + } + } + } + }, + "SubSection": { + "type": "object", + "properties": { + "Title": { + "tag": "title", + "type": "string" + }, + "Properties": { + "tag": "properties", + "type": "array", + "items": { + "$ref": "#/components/schemas/PropertyData" + } + } + } + }, + "PropertyData": { + "type": "object", + "properties": { + "Name": { + "tag": "name", + "type": "string" + }, + "Val": { + "tag": "value", + "type": "string" + }, + "Items": { + "tag": "items", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Status": { + "type": "object", + "properties": { + "Objects": { + "tag": "objects", + "type": "array", + "items": { + "$ref": "#/components/schemas/ObjectID" + } + }, + "Workers": { + "tag": "workers", + "type": "array", + "items": { + "$ref": "#/components/schemas/WorkerStatus" + } + } + } + }, + "Diagnostics": { + "type": "object", + "properties": { + "SystemStatusOk": { + "tag": "system_status_ok", + "type": "boolean" + }, + "ZosModules": { + "tag": "modules", + "type": "array", + "items": { + "$ref": "#/components/schemas/ModuleStatus" + } + }, + "Healthy": { + "tag": "healthy", + "type": "boolean" + } + } + }, + "ModuleStatus": { + "type": "object", + "properties": { + "Name": { + "tag": "name", + "type": "string" + }, + "Status": { + "tag": "status", + "$ref": "#/components/schemas/Status" + }, + "Err": { + "tag": "error", + "type": "string" + } + } + }, + "ObjectID": { + "type": "object", + "properties": { + "Name": { + "tag": "name", + "type": "string" + }, + "Version": { + "tag": "version", + "type": "string" + } + } + }, + "WorkerStatus": { + "type": "object", + "properties": { + "State": { + "tag": "state", + "type": "string" + }, + "StartTime": { + "tag": "time", + "type": "string", + "format": "date-time" + }, + "Action": { + "tag": "action", + "type": "string" + } + } + }, + "PoolMetricsResult": { + "type": "object", + "properties": { + "Pools": { + "tag": "pools", + "type": "array", + "items": { + "$ref": "#/components/schemas/PoolMetrics" + } + } + } + }, + "PoolMetrics": { + "type": "object", + "properties": { + "Name": { + "tag": "name", + "type": "string" + }, + "Type": { + "tag": "type", + "type": "string" + }, + "Size": { + "tag": "size", + "type": "integer" + }, + "Used": { + "tag": "used", + "type": "integer" + } + } + }, + "Counters": { + "type": "object", + "properties": { + "Total": { + "tag": "total", + "$ref": "#/components/schemas/Capacity" + }, + "Used": { + "tag": "used", + "$ref": "#/components/schemas/Capacity" + }, + "System": { + "tag": "system", + "$ref": "#/components/schemas/Capacity" + }, + "Users": { + "tag": "users", + "$ref": "#/components/schemas/UsersCounters" + } + } + }, + "Capacity": { + "type": "object", + "properties": { + "CRU": { + "tag": "cru", + "type": "integer" + }, + "SRU": { + "tag": "sru", + "type": "integer" + }, + "HRU": { + "tag": "hru", + "type": "integer" + }, + "MRU": { + "tag": "mru", + "type": "integer" + }, + "IPV4U": { + "tag": "ipv4u", + "type": "integer" + } + } + }, + "UsersCounters": { + "type": "object", + "properties": { + "Deployments": { + "tag": "deployments", + "type": "integer" + }, + "Workloads": { + "tag": "workloads", + "type": "integer" + }, + "LastDeploymentTimestamp": { + "tag": "last_deployment_timestamp", + "type": "integer" + } + } + }, + "PublicConfig": { + "type": "object", + "properties": { + "Type": { + "tag": "type", + "type": "string" + }, + "IPv4": { + "tag": "ipv4", + "type": "string", + "format": "ipv4" + }, + "IPv6": { + "tag": "ipv6", + "type": "string", + "format": "ipv6" + }, + "GW4": { + "tag": "gw4", + "type": "string", + "format": "ipv4" + }, + "GW6": { + "tag": "gw6", + "type": "string", + "format": "ipv6" + }, + "Domain": { + "tag": "domain", + "type": "string" + } + } + }, + "Ips": { + "type": "object", + "properties": { + "Ips": { + "tag": "ips", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "WGPorts": { + "type": "object", + "properties": { + "Ports": { + "tag": "ports", + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "GPUs": { + "type": "object", + "properties": { + "GPUs": { + "type": "array", + "tag": "gpus", + "items": { + "$ref": "#/components/schemas/GPU" + } + } + } + }, + "GPU": { + "type": "object", + "properties": { + "ID": { + "tag": "id", + "type": "string" + }, + "Vendor": { + "tag": "vendor", + "type": "string" + }, + "Device": { + "tag": "device", + "type": "string" + }, + "Contract": { + "tag": "contract", + "type": "integer" + } + } + }, + "Deployments": { + "type": "object", + "properties": { + "Deployments": { + "tag": "deployments", + "type": "array", + "items": { + "$ref": "#/components/schemas/Deployment" + } + } + } + }, + "Workloads": { + "type": "object", + "properties": { + "Workloads": { + "tag": "workloads", + "type": "array", + "items": { + "$ref": "#/components/schemas/Workload" + } + } + } + }, + "Workload": { + "type": "object", + "properties": { + "Version": { + "tag": "version", + "type": "integer" + }, + "Name": { + "tag": "name", + "type": "string" + }, + "Type": { + "tag": "type", + "type": "string" + }, + "Data": { + "tag": "data", + "type": "object", + "format": "raw" + }, + "Metadata": { + "tag": "metadata", + "type": "string" + }, + "Description": { + "tag": "description", + "type": "string" + }, + "Result": { + "tag": "result", + "$ref": "#/components/schemas/Result" + } + } + }, + "Result": { + "type": "object", + "properties": { + "Created": { + "tag": "created", + "type": "integer" + }, + "State": { + "tag": "state", + "type": "string" + }, + "Error": { + "tag": "error", + "type": "string" + }, + "Data": { + "tag": "data", + "type": "object", + "format": "raw" + } + } + }, + "Deployment": { + "type": "object", + "properties": { + "Version": { + "tag": "version", + "type": "integer" + }, + "TwinID": { + "tag": "twin_id", + "type": "integer" + }, + "ContractID": { + "tag": "contract_id", + "type": "integer" + }, + "Metadata": { + "tag": "metadata", + "type": "string" + }, + "Description": { + "tag": "description", + "type": "string" + }, + "Expiration": { + "tag": "expiration", + "type": "integer" + }, + "SignatureRequirement": { + "tag": "signature_requirement", + "$ref": "#/components/schemas/SignatureRequirement" + }, + "Workloads": { + "tag": "workloads", + "type": "array", + "items": { + "$ref": "#/components/schemas/Workload" + } + } + } + }, + "SignatureRequirement": { + "type": "object", + "properties": { + "Requests": { + "type": "array", + "tag": "requests", + "items": { + "$ref": "#/components/schemas/SignatureRequest" + } + }, + "WeightRequired": { + "tag": "weight_required", + "type": "integer" + }, + "Signatures": { + "type": "array", + "tag": "signatures", + "items": { + "$ref": "#/components/schemas/Signature" + } + }, + "SignatureStyle": { + "tag": "signature_style", + "type": "string" + } + } + }, + "SignatureRequest": { + "type": "object", + "properties": { + "TwinID": { + "tag": "twin_id", + "type": "integer" + }, + "Required": { + "tag": "required", + "type": "boolean" + }, + "Weight": { + "tag": "weight", + "type": "integer" + } + } + }, + "Signature": { + "type": "object", + "properties": { + "TwinID": { + "tag": "twin_id", + "type": "integer" + }, + "Signature": { + "tag": "signature", + "type": "string" + }, + "SignatureType": { + "tag": "signature_type", + "type": "string" + } + } + }, + "ExitDevice": { + "type": "object", + "properties": { + "IsSingle": { + "tag": "is_single", + "type": "boolean" + }, + "IsDual": { + "tag": "is_dual", + "type": "boolean" + }, + "AsDualInterface": { + "tag": "dual_interface", + "type": "string" + } + } + }, + "Interface": { + "type": "object", + "properties": { + "Name": { + "tag": "name", + "type": "string" + }, + "Ips": { + "tag": "ips", + "type": "array", + "items": { + "type": "string" + } + }, + "Mac": { + "tag": "mac,omitempty", + "type": "string" + } + } + }, + "Interfaces": { + "type": "object", + "properties": { + "Interfaces": { + "tag": "interfaces", + "type": "array", + "items": { + "$ref": "#/components/schemas/Interface" + } + } + } + }, + "CPUBenchmarkResult": { + "type": "object", + "properties": { + "Single": { + "type": "number", + "format": "float", + "tag": "single" + }, + "Multi": { + "type": "number", + "format": "float", + "tag": "multi" + }, + "Threads": { "type": "integer", "tag": "threads" }, + "Workloads": { "type": "integer", "tag": "workloads" } + } + }, + "CpuBenchTaskResult": { + "type": "object", + "properties": { + "Name": { "type": "string", "tag": "name" }, + "Description": { "type": "string", "tag": "description" }, + "Timestamp": { + "type": "integer", + "format": "uint64", + "tag": "timestamp" + }, + "Result": { + "$ref": "#/components/schemas/CPUBenchmarkResult", + "tag": "result" + } + } + }, + "HealthReport": { + "type": "object", + "properties": { + "TestName": { "type": "string", "tag": "test_name" }, + "Errors": { + "type": "array", + "items": { "type": "string" }, + "tag": "errors" + } + } + }, + "HealthTaskResult": { + "type": "object", + "properties": { + "Name": { "type": "string", "tag": "name" }, + "Description": { "type": "string", "tag": "description" }, + "Timestamp": { + "type": "integer", + "format": "uint64", + "tag": "timestamp" + }, + "Result": { + "type": "array", + "items": { "$ref": "#/components/schemas/HealthReport" }, + "tag": "result" + } + } + }, + "IperfResult": { + "type": "object", + "properties": { + "UploadSpeed": { + "type": "number", + "format": "float", + "tag": "upload_speed" + }, + "DownloadSpeed": { + "type": "number", + "format": "float", + "tag": "download_speed" + }, + "NodeID": { + "type": "integer", + "format": "uint32", + "tag": "node_id" + }, + "NodeIpv4": { "type": "string", "tag": "node_ip" }, + "TestType": { "type": "string", "tag": "test_type" }, + "Error": { "type": "string", "tag": "error" }, + "CpuReport": { + "$ref": "#/components/schemas/CPUUtilizationPercent", + "tag": "cpu_report" + } + } + }, + "CPUUtilizationPercent": { + "type": "object", + "properties": { + "HostTotal": { + "type": "number", + "format": "float", + "tag": "host_total" + }, + "HostUser": { + "type": "number", + "format": "float", + "tag": "host_user" + }, + "HostSystem": { + "type": "number", + "format": "float", + "tag": "host_system" + }, + "RemoteTotal": { + "type": "number", + "format": "float", + "tag": "remote_total" + }, + "RemoteUser": { + "type": "number", + "format": "float", + "tag": "remote_user" + }, + "RemoteSystem": { + "type": "number", + "format": "float", + "tag": "remote_system" + } + } + }, + "IperfTaskResult": { + "type": "object", + "properties": { + "Name": { "type": "string", "tag": "name" }, + "Description": { "type": "string", "tag": "description" }, + "Timestamp": { + "type": "integer", + "format": "uint64", + "tag": "timestamp" + }, + "Result": { + "type": "array", + "items": { "$ref": "#/components/schemas/IperfResult" }, + "tag": "result" + } + } + }, + "IPReport": { + "type": "object", + "properties": { + "Ip": { "type": "string", "tag": "ip" }, + "State": { "type": "string", "tag": "state" }, + "Reason": { "type": "string", "tag": "reason" } + } + }, + "PublicIpTaskResult": { + "type": "object", + "properties": { + "Name": { "type": "string", "tag": "name" }, + "Description": { "type": "string", "tag": "description" }, + "Timestamp": { + "type": "integer", + "format": "uint64", + "tag": "timestamp" + }, + "Result": { + "type": "array", + "items": { "$ref": "#/components/schemas/IPReport" }, + "tag": "result" + } + } + }, + "AllTaskResult": { + "type": "object", + "properties": { + "CpuBenchmark": { + "$ref": "#/components/schemas/CpuBenchTaskResult", + "tag": "cpu_benchmark" + }, + "HealthCheck": { + "$ref": "#/components/schemas/HealthTaskResult", + "tag": "health_check" + }, + "Iperf": { + "$ref": "#/components/schemas/IperfTaskResult", + "tag": "iperf" + }, + "PublicIp": { + "$ref": "#/components/schemas/PublicIpTaskResult", + "tag": "public_ip" + } + } + } + } + } +} diff --git a/tools/openrpc-codegen/Makefile b/tools/openrpc-codegen/Makefile new file mode 100644 index 000000000..1fb58f685 --- /dev/null +++ b/tools/openrpc-codegen/Makefile @@ -0,0 +1,3 @@ + +build: + sudo go build -o /usr/local/bin/openrpc-codegen main.go \ No newline at end of file diff --git a/tools/openrpc-codegen/fileutils/parser.go b/tools/openrpc-codegen/fileutils/parser.go new file mode 100644 index 000000000..775119871 --- /dev/null +++ b/tools/openrpc-codegen/fileutils/parser.go @@ -0,0 +1,22 @@ +package fileutils + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/threefoldtech/zos/tools/openrpc-codegen/schema" +) + +func Parse(filePath string) (spec schema.Spec, err error) { + content, err := os.ReadFile(filePath) + if err != nil { + return spec, fmt.Errorf("failed to read file content filepath=%v: %w", filePath, err) + } + + if err := json.Unmarshal(content, &spec); err != nil { + return spec, fmt.Errorf("failed to unmarshal file content to schema spec: %w", err) + } + + return spec, nil +} diff --git a/tools/openrpc-codegen/fileutils/writer.go b/tools/openrpc-codegen/fileutils/writer.go new file mode 100644 index 000000000..ccdf42657 --- /dev/null +++ b/tools/openrpc-codegen/fileutils/writer.go @@ -0,0 +1,27 @@ +package fileutils + +import ( + "bytes" + "fmt" + "go/format" + "os" +) + +func Write(buf bytes.Buffer, filePath string) error { + formatted, err := format.Source(buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to format buffer content: %w", err) + } + + file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return fmt.Errorf("failed to open file for writing filepath=%v: %w", filePath, err) + } + defer file.Close() + + if _, err := file.Write(formatted); err != nil { + return fmt.Errorf("failed to write on file: %w", err) + } + + return nil +} diff --git a/tools/openrpc-codegen/generator/generator.go b/tools/openrpc-codegen/generator/generator.go new file mode 100644 index 000000000..bd6275d88 --- /dev/null +++ b/tools/openrpc-codegen/generator/generator.go @@ -0,0 +1,132 @@ +package generator + +import ( + "bytes" + "fmt" + + "github.com/threefoldtech/zos/tools/openrpc-codegen/schema" +) + +func generateStructs(buf *bytes.Buffer, name string, schema schema.Schema) error { + fields := []field{} + for n, prop := range schema.Properties { + // Handle nested properties recursively + propType, err := generateType(buf, n, prop) + if err != nil { + return err + } + fields = append(fields, field{ + Name: n, + Type: propType, + JSONName: prop.Tag, + }) + } + + return executeTemplate(buf, structTemplate, structType{ + Name: name, + Fields: fields, + }) +} + +func generateType(buf *bytes.Buffer, name string, schema schema.Schema) (string, error) { + if schema.Ref != "" { + return invokeRef(schema.Ref), nil + } + + if schema.Format == "raw" { + return convertToGoType(invokeRef(schema.Type), schema.Format), nil + } + + switch schema.Type { + case "object": + if err := generateStructs(buf, name, schema); err != nil { + return "", err + } + return name, nil + case "array": + if schema.Items.Ref != "" { + return "[]" + invokeRef(schema.Items.Ref), nil + } + itemType, err := generateType(buf, name, *schema.Items) + if err != nil { + return "", err + } + return "[]" + itemType, nil + default: + return convertToGoType(invokeRef(schema.Type), schema.Format), nil + } +} + +func generateSchemas(buf *bytes.Buffer, schemas map[string]schema.Schema) error { + for key, schema := range schemas { + _, err := generateType(buf, key, schema) + if err != nil { + return err + } + } + return nil +} + +func generateMethods(buf *bytes.Buffer, serviceName string, methods []schema.Method) error { + ms := []methodType{} + for _, method := range methods { + methodName := extractMethodName(method.Name) + arg, reply, err := getMethodTypes(method) + if err != nil { + return err + } + ms = append(ms, methodType{ + Name: methodName, + ArgType: arg, + ReplyType: reply, + }) + } + + return executeTemplate(buf, MethodTemplate, service{ + Name: serviceName, + Methods: ms, + }) +} + +func getMethodTypes(method schema.Method) (string, string, error) { + argType, replyType := "any", "" + + if len(method.Params) == 1 { + argType = getTypeFromSchema(method.Params[0].Schema) + } else if len(method.Params) > 1 { + return "", "", fmt.Errorf("multiple parameters not supported for method: %v", method.Name) + } + + if method.Result.Schema.Type != "" { + replyType = convertToGoType(method.Result.Schema.Type, method.Result.Schema.Format) + } else if method.Result.Schema.Ref != "" { + replyType = invokeRef(method.Result.Schema.Ref) + } else { + return "", "", fmt.Errorf("no result defined for method: %v", method.Name) + } + + return argType, replyType, nil +} + +func getTypeFromSchema(schema schema.Schema) string { + if schema.Type != "" { + return convertToGoType(schema.Type, schema.Format) + } + return invokeRef(schema.Ref) +} + +func GenerateServerCode(buf *bytes.Buffer, spec schema.Spec, pkg string) error { + if err := addPackageName(buf, pkg); err != nil { + return fmt.Errorf("failed to write pkg name: %w", err) + } + + if err := generateMethods(buf, spec.Info.Title, spec.Methods); err != nil { + return fmt.Errorf("failed to generate methods: %w", err) + } + + if err := generateSchemas(buf, spec.Components.Schemas); err != nil { + return fmt.Errorf("failed to generate schema: %w", err) + } + + return nil +} diff --git a/tools/openrpc-codegen/generator/templates.go b/tools/openrpc-codegen/generator/templates.go new file mode 100644 index 000000000..bd6033804 --- /dev/null +++ b/tools/openrpc-codegen/generator/templates.go @@ -0,0 +1,39 @@ +package generator + +type structType struct { + Name string + Fields []field +} + +type field struct { + Name string + Type string + JSONName string +} + +const structTemplate = ` +type {{.Name}} struct { +{{- range .Fields }} + {{ .Name }} {{ .Type }} ` + "`json:\"{{ .JSONName }}\"`" + ` +{{- end }} +} +` + +type service struct { + Name string + Methods []methodType +} + +type methodType struct { + Name string + ArgType string + ReplyType string +} + +const MethodTemplate = ` +type {{.Name}} interface { +{{- range .Methods }} + {{.Name}}({{.ArgType}}, *{{.ReplyType}}) error +{{- end}} +} +` diff --git a/tools/openrpc-codegen/generator/utils.go b/tools/openrpc-codegen/generator/utils.go new file mode 100644 index 000000000..7a3be6b47 --- /dev/null +++ b/tools/openrpc-codegen/generator/utils.go @@ -0,0 +1,53 @@ +package generator + +import ( + "bytes" + "fmt" + "path" + "strings" + "text/template" +) + +func convertToGoType(t string, f string) string { + if f == "raw" { + return "json.RawMessage" + } + + switch t { + case "integer": + return "uint64" + case "number": + return "float64" + case "boolean": + return "bool" + case "null": + return "any" + default: + return t + } +} + +func invokeRef(t string) string { + return path.Base(t) +} + +func extractMethodName(methodText string) string { + return strings.Split(methodText, ".")[len(strings.Split(methodText, "."))-1] +} + +func executeTemplate(buf *bytes.Buffer, tmpl string, data interface{}) error { + templ, err := template.New("").Parse(tmpl) + if err != nil { + return fmt.Errorf("failed to parse template: %w", err) + } + if err := templ.Execute(buf, data); err != nil { + return fmt.Errorf("failed to execute template: %w", err) + } + return nil +} + +func addPackageName(buf *bytes.Buffer, pkg string) error { + pkgLine := fmt.Sprintf("package %s\n", pkg) + _, err := buf.Write([]byte(pkgLine)) + return err +} diff --git a/tools/openrpc-codegen/go.mod b/tools/openrpc-codegen/go.mod new file mode 100644 index 000000000..1daf6df22 --- /dev/null +++ b/tools/openrpc-codegen/go.mod @@ -0,0 +1,3 @@ +module github.com/threefoldtech/zos/tools/openrpc-codegen + +go 1.21.0 diff --git a/tools/openrpc-codegen/go.sum b/tools/openrpc-codegen/go.sum new file mode 100644 index 000000000..e69de29bb diff --git a/tools/openrpc-codegen/main.go b/tools/openrpc-codegen/main.go new file mode 100644 index 000000000..6fa03af5b --- /dev/null +++ b/tools/openrpc-codegen/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "log" + + "github.com/threefoldtech/zos/tools/openrpc-codegen/fileutils" + "github.com/threefoldtech/zos/tools/openrpc-codegen/generator" +) + +type flags struct { + spec string + output string + pkg string +} + +func run() error { + var f flags + flag.StringVar(&f.spec, "spec", "", "openrpc spec file") + flag.StringVar(&f.output, "output", "", "generated go code") + flag.StringVar(&f.pkg, "pkg", "apirpc", "name of the go package") + flag.Parse() + + if f.spec == "" || f.output == "" { + return fmt.Errorf("missing flag is required") + } + + spec, err := fileutils.Parse(f.spec) + if err != nil { + return err + } + + var buf bytes.Buffer + if err := generator.GenerateServerCode(&buf, spec, f.pkg); err != nil { + return err + } + + if err := fileutils.Write(buf, f.output); err != nil { + return err + } + + return nil +} + +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} diff --git a/tools/openrpc-codegen/readme.md b/tools/openrpc-codegen/readme.md new file mode 100644 index 000000000..bda3e5b94 --- /dev/null +++ b/tools/openrpc-codegen/readme.md @@ -0,0 +1,52 @@ +# OpenRPC CodeGen + +this tools generate server side code in go from an openrpc spec file + +## Usage + +Manually generate code + +```bash +go run main.go -spec -output +``` + +Use it to generate the api code + +```bash +make build +# go to root +go generate +``` + +## Limitations + +Any openrpc file that passes the linting on the [playground](https://playground.open-rpc.org/) should be valid for this tool. with just some limitations: + +- Methods must have only one arg/reply: since we use `net/rpc` package it requires to have only a single arg and reply. +- Methods can have arg/reply defined only for a primitive types. but for custom types array/objects it must be defined on the components schema and referenced in the method. +- Array is not a valid reply type, you need to define an object in the components that have a field of this array. and reference it on the method. +- Method Name, component Name, and component fields name must be a `PascalCase` +- Component fields must have a tag field, it is interpreted to a json tag on the generated struct and it is necessary in the conversion to the zos types. + +## Extensions + +- for compatibility with gridtypes we needed to configure some extra formats like + + ```json + "Data": { + "tag": "data", + "type": "object", + "format": "raw" + } + ``` + + which will generate a json.RawMessage type + +## Notes + +- All types and fields should be upper case. + +## Enhancements + +- [ ] write structs in order +- [ ] extend the spec file to have errors and examples and docs diff --git a/tools/openrpc-codegen/schema/types.go b/tools/openrpc-codegen/schema/types.go new file mode 100644 index 000000000..61a751ee9 --- /dev/null +++ b/tools/openrpc-codegen/schema/types.go @@ -0,0 +1,85 @@ +package schema + +type Spec struct { + OpenRPC string `json:"openrpc"` + Info Info `json:"info"` + Servers []Server `json:"servers"` + Methods []Method `json:"methods"` + Components Components `json:"components"` +} + +type Info struct { + Version string `json:"version"` + Title string `json:"title"` + License License `json:"license"` +} + +type License struct { + Name string `json:"name"` +} + +type Server struct { + URL string `json:"url"` +} + +type Method struct { + Name string `json:"name"` + Summary string `json:"summary"` + Tags []Tag `json:"tags"` + Params []Param `json:"params"` + Result Result `json:"result"` + Errors []Error `json:"errors"` + Examples []Example `json:"examples"` +} + +type Tag struct { + Name string `json:"name"` +} + +type Param struct { + Name string `json:"name"` + Description string `json:"description"` + Required bool `json:"required"` + Schema Schema `json:"schema"` +} + +type Result struct { + Name string `json:"name"` + Description string `json:"description"` + Schema Schema `json:"schema"` +} + +type Schema struct { + Type string `json:"type"` + Minimum int `json:"minimum,omitempty"` + Ref string `json:"$ref,omitempty"` + Items *Schema `json:"items,omitempty"` + Required []string `json:"required,omitempty"` + Tag string `json:"tag"` + Properties map[string]Schema `json:"properties,omitempty"` + Format string `json:"format,omitempty"` +} + +type Error struct { + Code int `json:"code"` + Message string `json:"message"` +} + +type Example struct { + Name string `json:"name"` + Description string `json:"description"` + Params []Param `json:"params"` + Result Result `json:"result"` +} + +type Components struct { + ContentDescriptors map[string]ContentDescriptor `json:"contentDescriptors"` + Schemas map[string]Schema `json:"schemas"` +} + +type ContentDescriptor struct { + Name string `json:"name"` + Required bool `json:"required"` + Description string `json:"description"` + Schema Schema `json:"schema"` +} From 8b64faedf4c20c77cec34fdf4e003901f45df942 Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Wed, 17 Jul 2024 04:36:15 +0300 Subject: [PATCH 2/3] internal code modifications to be compatible with openrpc spec: - seperate the perf task getters to have a method for each task with a defined result type instead of interface{} - modify healthcheck task to have its result as slice of object containes the check name instead of map from name to error - modify public ip task to have its result as slice of reports with the ip included instead of map from the ip to report - update performance monitor stub - modify the diagnositcs response to be slice of modules status instead of map --- pkg/diagnostics/diagnostics.go | 18 ++-- pkg/perf/cache.go | 125 ++++++++++++++++++++++---- pkg/perf/cpubench/cpubench_task.go | 3 +- pkg/perf/healthcheck/healthcheck.go | 14 +-- pkg/perf/iperf/iperf_task.go | 3 +- pkg/perf/publicip/publicip_task.go | 119 +++++++++++++----------- pkg/performance_monitor.go | 86 ++++++++++++++++++ pkg/stubs/performance_monitor_stub.go | 85 ++++++++++++++++++ 8 files changed, 368 insertions(+), 85 deletions(-) diff --git a/pkg/diagnostics/diagnostics.go b/pkg/diagnostics/diagnostics.go index 22e800754..9dc149e09 100644 --- a/pkg/diagnostics/diagnostics.go +++ b/pkg/diagnostics/diagnostics.go @@ -8,13 +8,14 @@ import ( "github.com/gomodule/redigo/redis" "github.com/threefoldtech/zbus" + "github.com/threefoldtech/zos/pkg" + "github.com/threefoldtech/zos/pkg/perf" "github.com/threefoldtech/zos/pkg/utils" ) -const ( - callTimeout = 3 * time.Second - testNetworkKey = "perf.healthcheck" -) +const callTimeout = 3 * time.Second + +var testNetworkKey = perf.GeneratePerfKey(pkg.HealthCheckTaskName) // Modules is all the registered modules on zbus var Modules = []string{ @@ -32,6 +33,8 @@ var Modules = []string{ // ModuleStatus represents the status of a module or shows if error type ModuleStatus struct { + // Name is the name of the module + Name string `json:"name"` // Status holds the status of the module Status zbus.Status `json:"status,omitempty"` // Err contains any error related to the module @@ -43,7 +46,7 @@ type Diagnostics struct { // SystemStatusOk is the overall system status SystemStatusOk bool `json:"system_status_ok"` // ZosModules is a list of modules with their objects and workers - ZosModules map[string]ModuleStatus `json:"modules"` + ZosModules []ModuleStatus `json:"modules"` // Healthy is the state of the node health check Healthy bool `json:"healthy"` } @@ -70,7 +73,7 @@ func NewDiagnosticsManager( func (m *DiagnosticsManager) GetSystemDiagnostics(ctx context.Context) (Diagnostics, error) { results := Diagnostics{ SystemStatusOk: true, - ZosModules: make(map[string]ModuleStatus), + ZosModules: []ModuleStatus{}, } var wg sync.WaitGroup @@ -86,7 +89,7 @@ func (m *DiagnosticsManager) GetSystemDiagnostics(ctx context.Context) (Diagnost mut.Lock() defer mut.Unlock() - results.ZosModules[module] = report + results.ZosModules = append(results.ZosModules, report) if report.Err != nil { hasError = true @@ -109,6 +112,7 @@ func (m *DiagnosticsManager) getModuleStatus(ctx context.Context, module string) status, err := m.zbusClient.Status(ctx, module) return ModuleStatus{ + Name: module, Status: status, Err: err, } diff --git a/pkg/perf/cache.go b/pkg/perf/cache.go index 5b0a3eb74..6dc12a14d 100644 --- a/pkg/perf/cache.go +++ b/pkg/perf/cache.go @@ -18,13 +18,25 @@ var ( ErrResultNotFound = errors.New("result not found") ) -// generateKey is helper method to add moduleName as prefix for the taskName -func generateKey(taskName string) string { +// GeneratePerfKey is helper method to add moduleName as prefix for the taskName +func GeneratePerfKey(taskName string) string { return fmt.Sprintf("%s.%s", moduleName, taskName) } +// exists check if a key exists +func (pm *PerformanceMonitor) exists(key string) (bool, error) { + conn := pm.pool.Get() + defer conn.Close() + + ok, err := redis.Bool(conn.Do("EXISTS", GeneratePerfKey(key))) + if err != nil { + return false, errors.Wrapf(err, "error checking if key %s exists", GeneratePerfKey(key)) + } + return ok, nil +} + // setCache set result in redis -func (pm *PerformanceMonitor) setCache(ctx context.Context, result pkg.TaskResult) error { +func (pm *PerformanceMonitor) setCache(_ context.Context, result pkg.TaskResult) error { data, err := json.Marshal(result) if err != nil { return errors.Wrap(err, "failed to marshal data to JSON") @@ -33,10 +45,99 @@ func (pm *PerformanceMonitor) setCache(ctx context.Context, result pkg.TaskResul conn := pm.pool.Get() defer conn.Close() - _, err = conn.Do("SET", generateKey(result.Name), data) + _, err = conn.Do("SET", GeneratePerfKey(result.Name), data) return err } +func (pm *PerformanceMonitor) getTaskResult(conn redis.Conn, key string, result interface{}) error { + data, err := conn.Do("GET", GeneratePerfKey(key)) + if err != nil { + return errors.Wrap(err, "failed to get the result") + } + + if data == nil { + return ErrResultNotFound + } + + err = json.Unmarshal(data.([]byte), result) + if err != nil { + return errors.Wrap(err, "failed to unmarshal data from json") + } + + return nil +} + +func (pm *PerformanceMonitor) GetIperfTaskResult() (pkg.IperfTaskResult, error) { + conn := pm.pool.Get() + defer conn.Close() + + var res pkg.IperfTaskResult + err := pm.getTaskResult(conn, pkg.IperfTaskName, &res) + return res, err +} + +func (pm *PerformanceMonitor) GetHealthTaskResult() (pkg.HealthTaskResult, error) { + conn := pm.pool.Get() + defer conn.Close() + + var res pkg.HealthTaskResult + err := pm.getTaskResult(conn, pkg.HealthCheckTaskName, &res) + return res, err +} + +func (pm *PerformanceMonitor) GetPublicIpTaskResult() (pkg.PublicIpTaskResult, error) { + conn := pm.pool.Get() + defer conn.Close() + + var res pkg.PublicIpTaskResult + err := pm.getTaskResult(conn, pkg.PublicIpTaskName, &res) + return res, err +} + +func (pm *PerformanceMonitor) GetCpuBenchTaskResult() (pkg.CpuBenchTaskResult, error) { + conn := pm.pool.Get() + defer conn.Close() + + var res pkg.CpuBenchTaskResult + err := pm.getTaskResult(conn, pkg.CpuBenchmarkTaskName, &res) + return res, err +} + +func (pm *PerformanceMonitor) GetAllTaskResult() (pkg.AllTaskResult, error) { + conn := pm.pool.Get() + defer conn.Close() + + var results pkg.AllTaskResult + + var cpu pkg.CpuBenchTaskResult + if err := pm.getTaskResult(conn, pkg.CpuBenchmarkTaskName, &cpu); err != nil { + return pkg.AllTaskResult{}, fmt.Errorf("failed to get health result: %w", err) + } + results.CpuBenchmark = cpu + + var health pkg.HealthTaskResult + if err := pm.getTaskResult(conn, pkg.HealthCheckTaskName, &health); err != nil { + return pkg.AllTaskResult{}, fmt.Errorf("failed to get health result: %w", err) + } + results.HealthCheck = health + + var iperf pkg.IperfTaskResult + if err := pm.getTaskResult(conn, pkg.IperfTaskName, &iperf); err != nil { + return pkg.AllTaskResult{}, fmt.Errorf("failed to get iperf result: %w", err) + } + results.Iperf = iperf + + var pIp pkg.PublicIpTaskResult + if err := pm.getTaskResult(conn, pkg.PublicIpTaskName, &pIp); err != nil { + return pkg.AllTaskResult{}, fmt.Errorf("failed to get public ip result: %w", err) + } + results.PublicIp = pIp + + return results, nil +} + +// DEPRECATED + // get directly gets result for some key func get(conn redis.Conn, key string) (pkg.TaskResult, error) { var res pkg.TaskResult @@ -62,7 +163,7 @@ func get(conn redis.Conn, key string) (pkg.TaskResult, error) { func (pm *PerformanceMonitor) Get(taskName string) (pkg.TaskResult, error) { conn := pm.pool.Get() defer conn.Close() - return get(conn, generateKey(taskName)) + return get(conn, GeneratePerfKey(taskName)) } // GetAll gets the results for all the tests with moduleName as prefix @@ -76,7 +177,7 @@ func (pm *PerformanceMonitor) GetAll() ([]pkg.TaskResult, error) { cursor := 0 for { - values, err := redis.Values(conn.Do("SCAN", cursor, "MATCH", generateKey("*"))) + values, err := redis.Values(conn.Do("SCAN", cursor, "MATCH", GeneratePerfKey("*"))) if err != nil { return nil, err } @@ -101,15 +202,3 @@ func (pm *PerformanceMonitor) GetAll() ([]pkg.TaskResult, error) { } return res, nil } - -// exists check if a key exists -func (pm *PerformanceMonitor) exists(key string) (bool, error) { - conn := pm.pool.Get() - defer conn.Close() - - ok, err := redis.Bool(conn.Do("EXISTS", generateKey(key))) - if err != nil { - return false, errors.Wrapf(err, "error checking if key %s exists", generateKey(key)) - } - return ok, nil -} diff --git a/pkg/perf/cpubench/cpubench_task.go b/pkg/perf/cpubench/cpubench_task.go index 820b9b383..0adbbcf9a 100644 --- a/pkg/perf/cpubench/cpubench_task.go +++ b/pkg/perf/cpubench/cpubench_task.go @@ -6,6 +6,7 @@ import ( "fmt" "os/exec" + "github.com/threefoldtech/zos/pkg" "github.com/threefoldtech/zos/pkg/perf" "github.com/threefoldtech/zos/pkg/stubs" ) @@ -30,7 +31,7 @@ func NewTask() perf.Task { // ID returns task ID. func (c *CPUBenchmarkTask) ID() string { - return "cpu-benchmark" + return pkg.CpuBenchmarkTaskName } // Cron returns task cron schedule. diff --git a/pkg/perf/healthcheck/healthcheck.go b/pkg/perf/healthcheck/healthcheck.go index 6c4958bc3..f28c79072 100644 --- a/pkg/perf/healthcheck/healthcheck.go +++ b/pkg/perf/healthcheck/healthcheck.go @@ -9,13 +9,13 @@ import ( "github.com/cenkalti/backoff" "github.com/rs/zerolog/log" + "github.com/threefoldtech/zos/pkg" "github.com/threefoldtech/zos/pkg/app" "github.com/threefoldtech/zos/pkg/perf" "github.com/threefoldtech/zos/pkg/stubs" ) const ( - id = "healthcheck" schedule = "0 */15 * * * *" description = "health check task runs multiple checks to ensure the node is in a usable state and set flags for the power daemon to stop reporting uptime if it is not usable" ) @@ -41,7 +41,7 @@ var _ perf.Task = (*healthcheckTask)(nil) // ID returns task ID. func (h *healthcheckTask) ID() string { - return id + return pkg.HealthCheckTaskName } func (h *healthcheckTask) Jitter() uint32 { @@ -61,7 +61,7 @@ func (h *healthcheckTask) Description() string { // Run executes the health checks. func (h *healthcheckTask) Run(ctx context.Context) (interface{}, error) { log.Debug().Msg("starting health check task") - errs := make(map[string][]string) + errs := []pkg.HealthReport{} cl := perf.GetZbusClient(ctx) zui := stubs.NewZUIStub(cl) @@ -79,9 +79,13 @@ func (h *healthcheckTask) Run(ctx context.Context) (interface{}, error) { mut.Lock() defer mut.Unlock() - errs[label] = errorsToStrings(errors) + errsStr := errorsToStrings(errors) + errs = append(errs, pkg.HealthReport{ + TestName: label, + Errors: errsStr, + }) - if err := zui.PushErrors(ctx, label, errs[label]); err != nil { + if err := zui.PushErrors(ctx, label, errsStr); err != nil { return err } diff --git a/pkg/perf/iperf/iperf_task.go b/pkg/perf/iperf/iperf_task.go index 8dd5af2d5..e6237b531 100644 --- a/pkg/perf/iperf/iperf_task.go +++ b/pkg/perf/iperf/iperf_task.go @@ -13,6 +13,7 @@ import ( "github.com/cenkalti/backoff" "github.com/pkg/errors" "github.com/rs/zerolog/log" + "github.com/threefoldtech/zos/pkg" "github.com/threefoldtech/zos/pkg/environment" "github.com/threefoldtech/zos/pkg/network/iperf" "github.com/threefoldtech/zos/pkg/perf" @@ -55,7 +56,7 @@ func NewTask() perf.Task { // ID returns the ID of the tcp task func (t *IperfTest) ID() string { - return "iperf" + return pkg.IperfTaskName } // Cron returns the schedule for the tcp task diff --git a/pkg/perf/publicip/publicip_task.go b/pkg/perf/publicip/publicip_task.go index 5b68c1740..b7e55c086 100644 --- a/pkg/perf/publicip/publicip_task.go +++ b/pkg/perf/publicip/publicip_task.go @@ -14,6 +14,7 @@ import ( "github.com/containernetworking/plugins/pkg/ns" "github.com/rs/zerolog/log" substrate "github.com/threefoldtech/tfchain/clients/tfchain-client-go" + "github.com/threefoldtech/zos/pkg" "github.com/threefoldtech/zos/pkg/environment" "github.com/threefoldtech/zos/pkg/network/macvlan" "github.com/threefoldtech/zos/pkg/network/namespace" @@ -43,6 +44,7 @@ const testNamespace = "pubtestns" type publicIPValidationTask struct{} type IPReport struct { + Ip string `json:"ip"` State string `json:"state"` Reason string `json:"reason"` } @@ -54,11 +56,11 @@ func NewTask() perf.Task { } func (p *publicIPValidationTask) ID() string { - return "public-ip-validation" + return pkg.PublicIpTaskName } func (p *publicIPValidationTask) Cron() string { - return "0 0 */6 * * *" + return "0 * * * * *" } func (p *publicIPValidationTask) Description() string { @@ -91,7 +93,7 @@ func (p *publicIPValidationTask) Run(ctx context.Context) (interface{}, error) { if err != nil { return nil, fmt.Errorf("failed to get farm with id %d: %w", farmID, err) } - var report map[string]IPReport + var report []IPReport err = netNS.Do(func(_ ns.NetNS) error { report, err = p.validateIPs(farm.PublicIPs) return err @@ -103,8 +105,8 @@ func (p *publicIPValidationTask) Run(ctx context.Context) (interface{}, error) { return report, nil } -func (p *publicIPValidationTask) validateIPs(publicIPs []substrate.PublicIP) (map[string]IPReport, error) { - report := make(map[string]IPReport) +func (p *publicIPValidationTask) validateIPs(publicIPs []substrate.PublicIP) ([]IPReport, error) { + reports := []IPReport{} mv, err := macvlan.GetByName(testMacvlan) if err != nil { return nil, fmt.Errorf("failed to get macvlan %s in namespace %s: %w", testMacvlan, testNamespace, err) @@ -116,65 +118,76 @@ func (p *publicIPValidationTask) validateIPs(publicIPs []substrate.PublicIP) (ma } for _, publicIP := range publicIPs { - report[publicIP.IP] = IPReport{ - State: ValidState, - } - if publicIP.ContractID != 0 { - report[publicIP.IP] = IPReport{ - State: SkippedState, - Reason: IPIsUsed, - } - continue - } + report := validateIp(publicIP, mv) + report.Ip = publicIP.IP + reports = append(reports, report) + } - ip, ipNet, routes, err := getIPWithRoute(publicIP) - if err != nil { - report[publicIP.IP] = IPReport{ - State: InvalidState, - Reason: PublicIPDataInvalid, - } - log.Err(err).Send() - continue + err = netlink.LinkSetDown(mv) + if err != nil { + return nil, fmt.Errorf("failed to set link down: %w", err) + } + + return reports, nil +} + +func validateIp(publicIp substrate.PublicIP, mv *netlink.Macvlan) (report IPReport) { + report = IPReport{ + State: ValidState, + } + + if publicIp.ContractID != 0 { + report = IPReport{ + State: SkippedState, + Reason: IPIsUsed, } - err = macvlan.Install(mv, nil, ipNet, routes, nil) - if err != nil { - report[publicIP.IP] = IPReport{ - State: InvalidState, - Reason: PublicIPDataInvalid, - } - log.Err(err).Msgf("failed to install macvlan %s with ip %s to namespace %s", testMacvlan, ipNet, testNamespace) - continue + return + } + + ip, ipNet, routes, err := getIPWithRoute(publicIp) + if err != nil { + report = IPReport{ + State: InvalidState, + Reason: PublicIPDataInvalid, } + log.Err(err).Send() + return + } - realIP, err := getRealPublicIP() - if errors.Is(err, errPublicIPLookup) { - report[publicIP.IP] = IPReport{ - State: InvalidState, - Reason: PublicIPDataInvalid, - } - } else if err != nil { - report[publicIP.IP] = IPReport{ - State: SkippedState, - Reason: FetchRealIPFailed, - } - } else if !ip.Equal(realIP) { - report[publicIP.IP] = IPReport{ - State: InvalidState, - Reason: IPsNotMatching, - } + err = macvlan.Install(mv, nil, ipNet, routes, nil) + if err != nil { + report = IPReport{ + State: InvalidState, + Reason: PublicIPDataInvalid, } + log.Err(err).Msgf("failed to install macvlan %s with ip %s to namespace %s", testMacvlan, ipNet, testNamespace) + return + } - err = deleteAllIPsAndRoutes(mv) - if err != nil { - log.Err(err).Send() + realIP, err := getRealPublicIP() + if errors.Is(err, errPublicIPLookup) { + report = IPReport{ + State: InvalidState, + Reason: PublicIPDataInvalid, + } + } else if err != nil { + report = IPReport{ + State: SkippedState, + Reason: FetchRealIPFailed, + } + } else if !ip.Equal(realIP) { + report = IPReport{ + State: InvalidState, + Reason: IPsNotMatching, } } - err = netlink.LinkSetDown(mv) + + err = deleteAllIPsAndRoutes(mv) if err != nil { - return nil, fmt.Errorf("failed to set link down: %w", err) + log.Err(err).Send() } - return report, nil + return } func isLeastValidNode(ctx context.Context, farmID uint32, substrateGateway *stubs.SubstrateGatewayStub) (bool, error) { diff --git a/pkg/performance_monitor.go b/pkg/performance_monitor.go index 178a41220..aa2fb1018 100644 --- a/pkg/performance_monitor.go +++ b/pkg/performance_monitor.go @@ -2,11 +2,70 @@ package pkg //go:generate zbusc -module node -version 0.0.1 -name performance-monitor -package stubs github.com/threefoldtech/zos/pkg+PerformanceMonitor stubs/performance_monitor_stub.go +var ( + HealthCheckTaskName = "healthcheck" + IperfTaskName = "iperf" + PublicIpTaskName = "public-ip-validation" + CpuBenchmarkTaskName = "cpu-benchmark" +) + type PerformanceMonitor interface { + GetAllTaskResult() (AllTaskResult, error) + GetIperfTaskResult() (IperfTaskResult, error) + GetHealthTaskResult() (HealthTaskResult, error) + GetPublicIpTaskResult() (PublicIpTaskResult, error) + GetCpuBenchTaskResult() (CpuBenchTaskResult, error) + // Deprecated Get(taskName string) (TaskResult, error) GetAll() ([]TaskResult, error) } +type CPUBenchmarkResult struct { + SingleThreaded float64 `json:"single"` + MultiThreaded float64 `json:"multi"` + Threads int `json:"threads"` + Workloads int `json:"workloads"` +} + +type CpuBenchTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result CPUBenchmarkResult `json:"result"` +} + +type AllTaskResult struct { + CpuBenchmark CpuBenchTaskResult `json:"cpu_benchmark"` + HealthCheck HealthTaskResult `json:"health_check"` + Iperf IperfTaskResult `json:"iperf"` + PublicIp PublicIpTaskResult `json:"public_ip"` +} + +type HealthReport struct { + TestName string `json:"test_name"` + Errors []string `json:"errors"` +} + +type HealthTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []HealthReport `json:"result"` +} + +type Report struct { + Ip string `json:"ip"` + State string `json:"state"` + Reason string `json:"reason"` +} + +type PublicIpTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []Report `json:"result"` +} + // TaskResult the result test schema type TaskResult struct { Name string `json:"name"` @@ -14,3 +73,30 @@ type TaskResult struct { Timestamp uint64 `json:"timestamp"` Result interface{} `json:"result"` } + +type IperfTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []IperfResult `json:"result"` +} + +// IperfResult for iperf test results +type IperfResult struct { + UploadSpeed float64 `json:"upload_speed"` // in bit/sec + DownloadSpeed float64 `json:"download_speed"` // in bit/sec + NodeID uint32 `json:"node_id"` + NodeIpv4 string `json:"node_ip"` + TestType string `json:"test_type"` + Error string `json:"error"` + CpuReport CPUUtilizationPercent `json:"cpu_report"` +} + +type CPUUtilizationPercent struct { + HostTotal float64 `json:"host_total"` + HostUser float64 `json:"host_user"` + HostSystem float64 `json:"host_system"` + RemoteTotal float64 `json:"remote_total"` + RemoteUser float64 `json:"remote_user"` + RemoteSystem float64 `json:"remote_system"` +} diff --git a/pkg/stubs/performance_monitor_stub.go b/pkg/stubs/performance_monitor_stub.go index cd0ec7440..acff88c1d 100644 --- a/pkg/stubs/performance_monitor_stub.go +++ b/pkg/stubs/performance_monitor_stub.go @@ -60,3 +60,88 @@ func (s *PerformanceMonitorStub) GetAll(ctx context.Context) (ret0 []pkg.TaskRes } return } + +func (s *PerformanceMonitorStub) GetAllTaskResult(ctx context.Context) (ret0 pkg.AllTaskResult, ret1 error) { + args := []interface{}{} + result, err := s.client.RequestContext(ctx, s.module, s.object, "GetAllTaskResult", args...) + if err != nil { + panic(err) + } + result.PanicOnError() + ret1 = result.CallError() + loader := zbus.Loader{ + &ret0, + } + if err := result.Unmarshal(&loader); err != nil { + panic(err) + } + return +} + +func (s *PerformanceMonitorStub) GetCpuBenchTaskResult(ctx context.Context) (ret0 pkg.CpuBenchTaskResult, ret1 error) { + args := []interface{}{} + result, err := s.client.RequestContext(ctx, s.module, s.object, "GetCpuBenchTaskResult", args...) + if err != nil { + panic(err) + } + result.PanicOnError() + ret1 = result.CallError() + loader := zbus.Loader{ + &ret0, + } + if err := result.Unmarshal(&loader); err != nil { + panic(err) + } + return +} + +func (s *PerformanceMonitorStub) GetHealthTaskResult(ctx context.Context) (ret0 pkg.HealthTaskResult, ret1 error) { + args := []interface{}{} + result, err := s.client.RequestContext(ctx, s.module, s.object, "GetHealthTaskResult", args...) + if err != nil { + panic(err) + } + result.PanicOnError() + ret1 = result.CallError() + loader := zbus.Loader{ + &ret0, + } + if err := result.Unmarshal(&loader); err != nil { + panic(err) + } + return +} + +func (s *PerformanceMonitorStub) GetIperfTaskResult(ctx context.Context) (ret0 pkg.IperfTaskResult, ret1 error) { + args := []interface{}{} + result, err := s.client.RequestContext(ctx, s.module, s.object, "GetIperfTaskResult", args...) + if err != nil { + panic(err) + } + result.PanicOnError() + ret1 = result.CallError() + loader := zbus.Loader{ + &ret0, + } + if err := result.Unmarshal(&loader); err != nil { + panic(err) + } + return +} + +func (s *PerformanceMonitorStub) GetPublicIpTaskResult(ctx context.Context) (ret0 pkg.PublicIpTaskResult, ret1 error) { + args := []interface{}{} + result, err := s.client.RequestContext(ctx, s.module, s.object, "GetPublicIpTaskResult", args...) + if err != nil { + panic(err) + } + result.PanicOnError() + ret1 = result.CallError() + loader := zbus.Loader{ + &ret0, + } + if err := result.Unmarshal(&loader); err != nil { + panic(err) + } + return +} From 7c12991a6286f8d7d9cec46f8441f52dcc37755e Mon Sep 17 00:00:00 2001 From: Omar Abdulaziz Date: Wed, 17 Jul 2024 07:08:16 +0300 Subject: [PATCH 3/3] implement a jsonrpc api for all zos endpoint followin `net/rpc` pkg protocol. --- cmds/modules/api_gateway/main.go | 14 + openrpc.json | 2 +- pkg/rpc/admin.go | 40 +++ pkg/rpc/converter.go | 66 +++++ pkg/rpc/deployment.go | 66 +++++ pkg/rpc/gpu.go | 19 ++ pkg/rpc/network.go | 96 ++++++ pkg/rpc/perf.go | 46 +++ pkg/rpc/readme.md | 24 ++ pkg/rpc/service.go | 102 +++++++ pkg/rpc/statistics.go | 13 + pkg/rpc/storage.go | 23 ++ pkg/rpc/system.go | 49 +++ pkg/rpc/types.go | 297 +++++++++++++++++++ pkg/rpc/utils.go | 23 ++ tools/openrpc-codegen/generator/generator.go | 2 +- tools/openrpc-codegen/generator/utils.go | 8 +- tools/openrpc-codegen/main.go | 2 +- 18 files changed, 886 insertions(+), 6 deletions(-) create mode 100644 pkg/rpc/admin.go create mode 100644 pkg/rpc/converter.go create mode 100644 pkg/rpc/deployment.go create mode 100644 pkg/rpc/gpu.go create mode 100644 pkg/rpc/network.go create mode 100644 pkg/rpc/perf.go create mode 100644 pkg/rpc/readme.md create mode 100644 pkg/rpc/service.go create mode 100644 pkg/rpc/statistics.go create mode 100644 pkg/rpc/storage.go create mode 100644 pkg/rpc/system.go create mode 100644 pkg/rpc/types.go create mode 100644 pkg/rpc/utils.go diff --git a/cmds/modules/api_gateway/main.go b/cmds/modules/api_gateway/main.go index b0f1f8cc2..39606615d 100644 --- a/cmds/modules/api_gateway/main.go +++ b/cmds/modules/api_gateway/main.go @@ -12,6 +12,7 @@ import ( "github.com/threefoldtech/tfgrid-sdk-go/rmb-sdk-go/peer" "github.com/threefoldtech/zbus" "github.com/threefoldtech/zos/pkg/environment" + "github.com/threefoldtech/zos/pkg/rpc" "github.com/threefoldtech/zos/pkg/stubs" substrategw "github.com/threefoldtech/zos/pkg/substrate_gateway" "github.com/threefoldtech/zos/pkg/utils" @@ -36,6 +37,11 @@ var Module cli.Command = cli.Command{ Usage: "number of workers `N`", Value: 1, }, + &cli.UintFlag{ + Name: "port", + Usage: "rpc server port", + Value: 3000, + }, }, Action: action, } @@ -44,6 +50,7 @@ func action(cli *cli.Context) error { var ( msgBrokerCon string = cli.String("broker") workerNr uint = cli.Uint("workers") + port uint = cli.Uint("port") ) server, err := zbus.NewRedisServer(module, msgBrokerCon, workerNr) @@ -98,6 +105,13 @@ func action(cli *cli.Context) error { } api.SetupRoutes(router) + go func() { + if err := rpc.Run(ctx, port, manager, redis, msgBrokerCon); err != nil { + log.Error().Err(err).Msg("failed to run rpc server") + return + } + }() + pair, err := id.KeyPair() if err != nil { return err diff --git a/openrpc.json b/openrpc.json index 9cd6500a5..285de1641 100644 --- a/openrpc.json +++ b/openrpc.json @@ -297,7 +297,7 @@ } }, { - "name": "zos.DeploymentCreate", + "name": "zos.DeploymentDeploy", "params": [ { "name": "deployment", diff --git a/pkg/rpc/admin.go b/pkg/rpc/admin.go new file mode 100644 index 000000000..089adbb01 --- /dev/null +++ b/pkg/rpc/admin.go @@ -0,0 +1,40 @@ +package rpc + +func (s *Service) AdminPublicNICSet(arg string, reply *any) error { + return s.networkerStub.SetPublicExitDevice(s.ctx, arg) +} + +func (s *Service) AdminPublicNICGet(arg any, reply *ExitDevice) error { + ed, err := s.networkerStub.GetPublicExitDevice(s.ctx) + if err != nil { + return err + } + + reply.AsDualInterface = ed.AsDualInterface + reply.IsDual = ed.IsDual + reply.IsSingle = ed.IsSingle + return nil +} + +func (s *Service) AdminInterfaces(arg any, reply *Interfaces) error { + interfaces, err := s.networkerStub.Interfaces(s.ctx, "", "") + if err != nil { + return err + } + + for name, inf := range interfaces.Interfaces { + reply.Interfaces = append(reply.Interfaces, Interface{ + Name: name, + Mac: inf.Mac, + Ips: func() []string { + var ips []string + for _, ip := range inf.IPs { + ips = append(ips, ip.String()) + } + return ips + }(), + }) + } + + return nil +} diff --git a/pkg/rpc/converter.go b/pkg/rpc/converter.go new file mode 100644 index 000000000..c7926c09f --- /dev/null +++ b/pkg/rpc/converter.go @@ -0,0 +1,66 @@ +package rpc + +import ( + "encoding/json" + + "github.com/threefoldtech/zos/pkg/capacity/dmi" +) + +// This function heavily used and it depends on matching json tags on both types +func convert(input any, output any) error { + data, err := json.Marshal(input) + if err != nil { + return err + } + + if err := json.Unmarshal(data, &output); err != nil { + return err + } + + return nil +} + +func convertDmi(input *dmi.DMI, output *DMI) { + dmi := *input + + *output = DMI{ + Tooling: Tooling{ + Aggregator: dmi.Tooling.Aggregator, + Decoder: dmi.Tooling.Decoder, + }, + Sections: func() []Section { + var sections []Section + for _, sec := range dmi.Sections { + section := Section{ + HandleLine: sec.HandleLine, + TypeStr: sec.TypeStr, + Type: uint64(sec.Type), + SubSections: func() []SubSection { + var subsections []SubSection + for _, subsec := range sec.SubSections { + subsection := SubSection{ + Title: subsec.Title, + Properties: func() []PropertyData { + var properties []PropertyData + for key, prop := range subsec.Properties { + property := PropertyData{ + Name: key, + Val: prop.Val, + Items: prop.Items, + } + properties = append(properties, property) + } + return properties + }(), + } + subsections = append(subsections, subsection) + } + return subsections + }(), + } + sections = append(sections, section) + } + return sections + }(), + } +} diff --git a/pkg/rpc/deployment.go b/pkg/rpc/deployment.go new file mode 100644 index 000000000..f74da508b --- /dev/null +++ b/pkg/rpc/deployment.go @@ -0,0 +1,66 @@ +package rpc + +import ( + "context" + + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +func (s *Service) DeploymentChanges(arg uint64, reply *Workloads) error { + wls, err := s.provisionStub.Changes(context.Background(), GetTwinID(s.ctx), arg) + if err != nil { + return err + } + + for _, wl := range wls { + var workload Workload + if err := convert(wl, &workload); err != nil { + return err + } + reply.Workloads = append(reply.Workloads, workload) + } + + return nil +} + +func (s *Service) DeploymentList(arg any, reply *Deployments) error { + deps, err := s.provisionStub.List(s.ctx, GetTwinID(s.ctx)) + if err != nil { + return err + } + + for _, dep := range deps { + var deployment Deployment + if err := convert(dep, &deployment); err != nil { + return err + } + reply.Deployments = append(reply.Deployments, deployment) + } + + return nil +} + +func (s *Service) DeploymentGet(arg uint64, reply *Deployment) error { + dep, err := s.provisionStub.Get(s.ctx, GetTwinID(s.ctx), arg) + if err != nil { + return err + } + + return convert(dep, reply) +} + +func (s *Service) DeploymentUpdate(arg Deployment, reply *any) error { + var deployment gridtypes.Deployment + if err := convert(arg, &deployment); err != nil { + return err + } + return s.provisionStub.CreateOrUpdate(s.ctx, GetTwinID(s.ctx), deployment, true) +} + +func (s *Service) DeploymentDeploy(arg Deployment, reply *any) error { + var deployment gridtypes.Deployment + if err := convert(arg, &deployment); err != nil { + return err + } + return s.provisionStub.CreateOrUpdate(s.ctx, GetTwinID(s.ctx), deployment, false) +} diff --git a/pkg/rpc/gpu.go b/pkg/rpc/gpu.go new file mode 100644 index 000000000..55d5c7f93 --- /dev/null +++ b/pkg/rpc/gpu.go @@ -0,0 +1,19 @@ +package rpc + +func (s *Service) GpuList(arg any, reply *GPUs) error { + gpus, err := s.statisticsStub.ListGPUs(s.ctx) + if err != nil { + return err + } + + for _, gpu := range gpus { + reply.GPUs = append(reply.GPUs, GPU{ + ID: gpu.ID, + Vendor: gpu.Vendor, + Device: gpu.Device, + Contract: gpu.Contract, + }) + } + + return nil +} diff --git a/pkg/rpc/network.go b/pkg/rpc/network.go new file mode 100644 index 000000000..6fc026581 --- /dev/null +++ b/pkg/rpc/network.go @@ -0,0 +1,96 @@ +package rpc + +import ( + "fmt" + "net" + + "github.com/threefoldtech/zos/pkg/gridtypes" +) + +func (s *Service) NetworkPublicIps(arg any, reply *Ips) error { + ips, err := s.provisionStub.ListPublicIPs(s.ctx) + if err != nil { + return err + } + + reply.Ips = append(reply.Ips, ips...) + return nil +} + +func (s *Service) NetworkHasIpv6(arg any, reply *bool) error { + ipData, err := s.networkerStub.GetPublicIPv6Subnet(s.ctx) + hasIP := ipData.IP != nil && err == nil + *reply = hasIP + return nil +} + +func (s *Service) NetworkInterfaces(arg any, reply *Interfaces) error { + type q struct { + inf string + ns string + rename string + } + + for _, i := range []q{ + {"zos", "", "zos"}, + {"nygg6", "ndmz", "ygg"}, + } { + ips, _, err := s.networkerStub.Addrs(s.ctx, i.inf, i.ns) + if err != nil { + return fmt.Errorf("failed to get ips for '%s' interface: %w", i, err) + } + + reply.Interfaces = append(reply.Interfaces, Interface{ + Name: i.rename, + Ips: func() []string { + var list []string + for _, item := range ips { + ip := net.IP(item) + list = append(list, ip.String()) + } + + return list + }(), + }) + } + + return nil +} + +func (s *Service) NetworkPublicConfig(arg any, reply *PublicConfig) error { + config, err := s.networkerStub.GetPublicConfig(s.ctx) + if err != nil { + return err + } + + reply.Domain = config.Domain + reply.IPv4 = config.IPv4.String() + reply.IPv6 = config.IPv6.String() + reply.GW4 = config.GW4.String() + reply.GW6 = config.GW6.String() + reply.Type = string(config.Type) + return nil +} + +func (s *Service) NetworkWGPorts(arg any, reply *WGPorts) error { + ports, err := s.networkerStub.WireguardPorts(s.ctx) + if err != nil { + return err + } + + for _, port := range ports { + reply.Ports = append(reply.Ports, uint64(port)) + } + + return nil +} + +func (s *Service) NetworkPrivateIps(arg string, reply *Ips) error { + ips, err := s.provisionStub.ListPrivateIPs(s.ctx, GetTwinID(s.ctx), gridtypes.Name(arg)) + if err != nil { + return err + } + + reply.Ips = append(reply.Ips, ips...) + return nil +} diff --git a/pkg/rpc/perf.go b/pkg/rpc/perf.go new file mode 100644 index 000000000..1e4386b72 --- /dev/null +++ b/pkg/rpc/perf.go @@ -0,0 +1,46 @@ +package rpc + +func (s *Service) PerfGetCpuBench(arg any, reply *CpuBenchTaskResult) error { + r, err := s.performanceMonitorStub.GetCpuBenchTaskResult(s.ctx) + if err != nil { + return err + } + + return convert(r, reply) +} + +func (s *Service) PerfGetHealth(arg any, reply *HealthTaskResult) error { + r, err := s.performanceMonitorStub.GetHealthTaskResult(s.ctx) + if err != nil { + return err + } + + return convert(r, reply) +} + +func (s *Service) PerfGetIperf(arg any, reply *IperfTaskResult) error { + r, err := s.performanceMonitorStub.GetIperfTaskResult(s.ctx) + if err != nil { + return err + } + + return convert(r, reply) +} + +func (s *Service) PerfGetPublicIP(arg any, reply *PublicIpTaskResult) error { + r, err := s.performanceMonitorStub.GetPublicIpTaskResult(s.ctx) + if err != nil { + return err + } + + return convert(r, reply) +} + +func (s *Service) PerfGetAll(arg any, reply *AllTaskResult) error { + r, err := s.performanceMonitorStub.GetAllTaskResult(s.ctx) + if err != nil { + return err + } + + return convert(r, reply) +} diff --git a/pkg/rpc/readme.md b/pkg/rpc/readme.md new file mode 100644 index 000000000..373219e03 --- /dev/null +++ b/pkg/rpc/readme.md @@ -0,0 +1,24 @@ +# ZOS RPC API + +this package implements a jsonrpc api for all zos endpoint with `net/rpc` and `net/rpc/jsonrpc` packages. + +`types.go` file is auto generated from the `openrpc.json` spec file on the repo root directory with the tool in `tools/openrpc-codegen` + +to generate the types from the openrpc spec file you can use the tool manually check [docs](./../../tools/openrpc-codegen/readme.md) or use `go generate` + +first you need to have: + +- `openrpc-codegen` bin: `cd tools/openrpc-codegen/ && make build` +- `goimports`: `go install golang.org/x/tools/cmd/goimports@latest` + +then you can generate: + +`go generate ./pkg/rpc/...` + +## How to call the api + +it is can be called with `nc` for example: + +```bash +echo '{"method": "zos.SystemVersion", "params": [], "id": 1}' | nc -q 1 3000 +``` diff --git a/pkg/rpc/service.go b/pkg/rpc/service.go new file mode 100644 index 000000000..bc26a9800 --- /dev/null +++ b/pkg/rpc/service.go @@ -0,0 +1,102 @@ +package rpc + +import ( + "context" + "fmt" + "net" + "net/rpc" + "net/rpc/jsonrpc" + + "github.com/rs/zerolog/log" + substrate "github.com/threefoldtech/tfchain/clients/tfchain-client-go" + "github.com/threefoldtech/zbus" + "github.com/threefoldtech/zos/pkg/capacity" + "github.com/threefoldtech/zos/pkg/diagnostics" + "github.com/threefoldtech/zos/pkg/environment" + "github.com/threefoldtech/zos/pkg/stubs" +) + +//go:generate openrpc-codegen -spec ../../openrpc.json -output ./types.go +//go:generate goimports -w ./types.go + +type Service struct { + ctx context.Context + oracle *capacity.ResourceOracle + versionMonitorStub *stubs.VersionMonitorStub + provisionStub *stubs.ProvisionStub + networkerStub *stubs.NetworkerStub + statisticsStub *stubs.StatisticsStub + storageStub *stubs.StorageModuleStub + performanceMonitorStub *stubs.PerformanceMonitorStub + diagnosticsManager *diagnostics.DiagnosticsManager + farmerID uint32 +} + +func NewService(ctx context.Context, manager substrate.Manager, client zbus.Client, msgBrokerCon string) (*Service, error) { + sub, err := manager.Substrate() + if err != nil { + return nil, err + } + defer sub.Close() + + diagnosticsManager, err := diagnostics.NewDiagnosticsManager(msgBrokerCon, client) + if err != nil { + return nil, err + } + + storageModuleStub := stubs.NewStorageModuleStub(client) + + server := Service{ + oracle: capacity.NewResourceOracle(storageModuleStub), + versionMonitorStub: stubs.NewVersionMonitorStub(client), + provisionStub: stubs.NewProvisionStub(client), + networkerStub: stubs.NewNetworkerStub(client), + statisticsStub: stubs.NewStatisticsStub(client), + storageStub: storageModuleStub, + performanceMonitorStub: stubs.NewPerformanceMonitorStub(client), + diagnosticsManager: diagnosticsManager, + ctx: ctx, + } + + farm, err := sub.GetFarm(uint32(environment.MustGet().FarmID)) + if err != nil { + return nil, fmt.Errorf("failed to get farm: %w", err) + } + farmer, err := sub.GetTwin(uint32(farm.TwinID)) + if err != nil { + return nil, err + } + server.farmerID = uint32(farmer.ID) + return &server, nil +} + +var _ ZosRpcApi = (*Service)(nil) + +func Run(ctx context.Context, port uint, manager substrate.Manager, client zbus.Client, msgBrokerCon string) error { + service, err := NewService(ctx, manager, client, msgBrokerCon) + if err != nil { + return fmt.Errorf("failed to create api service: %w", err) + } + + if err := rpc.RegisterName("zos", service); err != nil { + return fmt.Errorf("failed to register api service: %w", err) + } + + l, err := net.Listen("tcp", fmt.Sprintf(":%v", port)) + if err != nil { + return fmt.Errorf("failed to listen on port %v: %w", port, err) + } + + log.Info().Uint("port", port).Msg("rpc server started") + for { + conn, err := l.Accept() + if err != nil { + log.Error().Err(err).Send() + continue + } + + // SetTwinId(ctx, GetTwinFromIp(conn.RemoteAddr().String())) + log.Info().Uint32("twinId", 0).Msg("got rpc request") + go jsonrpc.ServeConn(conn) + } +} diff --git a/pkg/rpc/statistics.go b/pkg/rpc/statistics.go new file mode 100644 index 000000000..5ae52c61e --- /dev/null +++ b/pkg/rpc/statistics.go @@ -0,0 +1,13 @@ +package rpc + +import ( + "fmt" +) + +func (s *Service) Statistics(arg any, reply *Counters) error { + stats, err := s.statisticsStub.GetCounters(s.ctx) + if err != nil { + return fmt.Errorf("failed to get diagnostics: %w", err) + } + return convert(stats, reply) +} diff --git a/pkg/rpc/storage.go b/pkg/rpc/storage.go new file mode 100644 index 000000000..47a5b5e9b --- /dev/null +++ b/pkg/rpc/storage.go @@ -0,0 +1,23 @@ +package rpc + +import ( + "fmt" +) + +func (s *Service) StorageMetrics(arg any, reply *PoolMetricsResult) error { + pools, err := s.storageStub.Metrics(s.ctx) + if err != nil { + return fmt.Errorf("failed to get pools: %w", err) + } + + for _, pool := range pools { + reply.Pools = append(reply.Pools, PoolMetrics{ + Name: pool.Name, + Type: pool.Type.String(), + Size: uint64(pool.Size), + Used: uint64(pool.Used), + }) + } + + return nil +} diff --git a/pkg/rpc/system.go b/pkg/rpc/system.go new file mode 100644 index 000000000..bdef19c44 --- /dev/null +++ b/pkg/rpc/system.go @@ -0,0 +1,49 @@ +package rpc + +import ( + "fmt" + "os/exec" + "strings" +) + +func (s *Service) SystemVersion(arg any, reply *Version) error { + output, err := exec.CommandContext(s.ctx, "zinit", "-V").CombinedOutput() + var zInitVer string + if err != nil { + zInitVer = err.Error() + } else { + zInitVer = strings.TrimSpace(strings.TrimPrefix(string(output), "zinit")) + } + + reply.Zos = s.versionMonitorStub.GetVersion(s.ctx).String() + reply.Zinit = zInitVer + + return nil +} + +func (s *Service) SystemHypervisor(arg any, reply *string) error { + hv, err := s.oracle.GetHypervisor() + if err != nil { + return fmt.Errorf("failed to get hypervisor: %w", err) + } + + *reply = hv + return nil +} + +func (s *Service) SystemDiagnostics(arg any, reply *Diagnostics) error { + dia, err := s.diagnosticsManager.GetSystemDiagnostics(s.ctx) + if err != nil { + return fmt.Errorf("failed to get diagnostics: %w", err) + } + return convert(dia, reply) +} + +func (s *Service) SystemDmi(arg any, reply *DMI) error { + dmi, err := s.oracle.DMI() + if err != nil { + return fmt.Errorf("failed to get dmi: %w", err) + } + convertDmi(dmi, reply) + return nil +} diff --git a/pkg/rpc/types.go b/pkg/rpc/types.go new file mode 100644 index 000000000..2e1828708 --- /dev/null +++ b/pkg/rpc/types.go @@ -0,0 +1,297 @@ +// AUTO-GENERATED: this file is auto generated please don't edit +package rpc + +import "encoding/json" + +type ZosRpcApi interface { + SystemVersion(any, *Version) error + SystemHypervisor(any, *string) error + SystemDiagnostics(any, *Diagnostics) error + SystemDmi(any, *DMI) error + GpuList(any, *GPUs) error + StorageMetrics(any, *PoolMetricsResult) error + Statistics(any, *Counters) error + PerfGetCpuBench(any, *CpuBenchTaskResult) error + PerfGetHealth(any, *HealthTaskResult) error + PerfGetIperf(any, *IperfTaskResult) error + PerfGetPublicIP(any, *PublicIpTaskResult) error + PerfGetAll(any, *AllTaskResult) error + NetworkPrivateIps(string, *Ips) error + NetworkPublicIps(any, *Ips) error + NetworkHasIpv6(any, *bool) error + NetworkInterfaces(any, *Interfaces) error + NetworkPublicConfig(any, *PublicConfig) error + NetworkWGPorts(any, *WGPorts) error + AdminPublicNICSet(string, *any) error + AdminPublicNICGet(any, *ExitDevice) error + AdminInterfaces(any, *Interfaces) error + DeploymentList(any, *Deployments) error + DeploymentGet(uint64, *Deployment) error + DeploymentChanges(uint64, *Workloads) error + DeploymentUpdate(Deployment, *any) error + DeploymentDeploy(Deployment, *any) error +} + +type Interfaces struct { + Interfaces []Interface `json:"interfaces"` +} + +type CpuBenchTaskResult struct { + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result CPUBenchmarkResult `json:"result"` + Name string `json:"name"` +} + +type DMI struct { + Tooling Tooling `json:"tooling"` + Sections []Section `json:"sections"` +} + +type SubSection struct { + Title string `json:"title"` + Properties []PropertyData `json:"properties"` +} + +type PropertyData struct { + Val string `json:"value"` + Items []string `json:"items"` + Name string `json:"name"` +} + +type ModuleStatus struct { + Name string `json:"name"` + Status Status `json:"status"` + Err string `json:"error"` +} + +type Counters struct { + Total Capacity `json:"total"` + Used Capacity `json:"used"` + System Capacity `json:"system"` + Users UsersCounters `json:"users"` +} + +type Capacity struct { + SRU uint64 `json:"sru"` + HRU uint64 `json:"hru"` + MRU uint64 `json:"mru"` + IPV4U uint64 `json:"ipv4u"` + CRU uint64 `json:"cru"` +} + +type Workload struct { + Version uint64 `json:"version"` + Name string `json:"name"` + Type string `json:"type"` + Data json.RawMessage `json:"data"` + Metadata string `json:"metadata"` + Description string `json:"description"` + Result Result `json:"result"` +} + +type Deployment struct { + Version uint64 `json:"version"` + TwinID uint64 `json:"twin_id"` + ContractID uint64 `json:"contract_id"` + Metadata string `json:"metadata"` + Description string `json:"description"` + Expiration uint64 `json:"expiration"` + SignatureRequirement SignatureRequirement `json:"signature_requirement"` + Workloads []Workload `json:"workloads"` +} + +type Version struct { + Zinit string `json:"zinit"` + Zos string `json:"zos"` +} + +type Diagnostics struct { + Healthy bool `json:"healthy"` + SystemStatusOk bool `json:"system_status_ok"` + ZosModules []ModuleStatus `json:"modules"` +} + +type WorkerStatus struct { + State string `json:"state"` + StartTime string `json:"time"` + Action string `json:"action"` +} + +type PoolMetrics struct { + Size uint64 `json:"size"` + Used uint64 `json:"used"` + Name string `json:"name"` + Type string `json:"type"` +} + +type PublicIpTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []IPReport `json:"result"` +} + +type CPUBenchmarkResult struct { + Single float64 `json:"single"` + Multi float64 `json:"multi"` + Threads uint64 `json:"threads"` + Workloads uint64 `json:"workloads"` +} + +type PublicConfig struct { + Type string `json:"type"` + IPv4 string `json:"ipv4"` + IPv6 string `json:"ipv6"` + GW4 string `json:"gw4"` + GW6 string `json:"gw6"` + Domain string `json:"domain"` +} + +type Deployments struct { + Deployments []Deployment `json:"deployments"` +} + +type SignatureRequirement struct { + Requests []SignatureRequest `json:"requests"` + WeightRequired uint64 `json:"weight_required"` + Signatures []Signature `json:"signatures"` + SignatureStyle string `json:"signature_style"` +} + +type SignatureRequest struct { + Weight uint64 `json:"weight"` + TwinID uint64 `json:"twin_id"` + Required bool `json:"required"` +} + +type HealthTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []HealthReport `json:"result"` +} + +type IPReport struct { + Ip string `json:"ip"` + State string `json:"state"` + Reason string `json:"reason"` +} + +type UsersCounters struct { + Deployments uint64 `json:"deployments"` + Workloads uint64 `json:"workloads"` + LastDeploymentTimestamp uint64 `json:"last_deployment_timestamp"` +} + +type GPUs struct { + GPUs []GPU `json:"gpus"` +} + +type Workloads struct { + Workloads []Workload `json:"workloads"` +} + +type Result struct { + Created uint64 `json:"created"` + State string `json:"state"` + Error string `json:"error"` + Data json.RawMessage `json:"data"` +} + +type CPUUtilizationPercent struct { + HostUser float64 `json:"host_user"` + HostSystem float64 `json:"host_system"` + RemoteTotal float64 `json:"remote_total"` + RemoteUser float64 `json:"remote_user"` + RemoteSystem float64 `json:"remote_system"` + HostTotal float64 `json:"host_total"` +} + +type Tooling struct { + Aggregator string `json:"aggregator"` + Decoder string `json:"decoder"` +} + +type Status struct { + Objects []ObjectID `json:"objects"` + Workers []WorkerStatus `json:"workers"` +} + +type HealthReport struct { + Errors []string `json:"errors"` + TestName string `json:"test_name"` +} + +type IperfResult struct { + NodeID uint64 `json:"node_id"` + NodeIpv4 string `json:"node_ip"` + TestType string `json:"test_type"` + Error string `json:"error"` + CpuReport CPUUtilizationPercent `json:"cpu_report"` + UploadSpeed float64 `json:"upload_speed"` + DownloadSpeed float64 `json:"download_speed"` +} + +type Ips struct { + Ips []string `json:"ips"` +} + +type Signature struct { + TwinID uint64 `json:"twin_id"` + Signature string `json:"signature"` + SignatureType string `json:"signature_type"` +} + +type ExitDevice struct { + IsSingle bool `json:"is_single"` + IsDual bool `json:"is_dual"` + AsDualInterface string `json:"dual_interface"` +} + +type IperfTaskResult struct { + Name string `json:"name"` + Description string `json:"description"` + Timestamp uint64 `json:"timestamp"` + Result []IperfResult `json:"result"` +} + +type ObjectID struct { + Name string `json:"name"` + Version string `json:"version"` +} + +type PoolMetricsResult struct { + Pools []PoolMetrics `json:"pools"` +} + +type GPU struct { + ID string `json:"id"` + Vendor string `json:"vendor"` + Device string `json:"device"` + Contract uint64 `json:"contract"` +} + +type AllTaskResult struct { + HealthCheck HealthTaskResult `json:"health_check"` + Iperf IperfTaskResult `json:"iperf"` + PublicIp PublicIpTaskResult `json:"public_ip"` + CpuBenchmark CpuBenchTaskResult `json:"cpu_benchmark"` +} + +type Section struct { + SubSections []SubSection `json:"subsections"` + HandleLine string `json:"handleline"` + TypeStr string `json:"typestr"` + Type uint64 `json:"typenum"` +} + +type WGPorts struct { + Ports []uint64 `json:"ports"` +} + +type Interface struct { + Mac string `json:"mac,omitempty"` + Name string `json:"name"` + Ips []string `json:"ips"` +} diff --git a/pkg/rpc/utils.go b/pkg/rpc/utils.go new file mode 100644 index 000000000..7618ae793 --- /dev/null +++ b/pkg/rpc/utils.go @@ -0,0 +1,23 @@ +package rpc + +import "context" + +type twinId struct{} + +func GetTwinFromIp(ip string) uint32 { + // placeholder, waiting for implementation in mycelium + return 0 +} + +func SetTwinId(ctx context.Context, id uint32) context.Context { + return context.WithValue(ctx, twinId{}, id) +} + +func GetTwinID(ctx context.Context) uint32 { + twin, ok := ctx.Value(twinId{}).(uint32) + if !ok { + panic("failed to load twin id from context") + } + + return twin +} diff --git a/tools/openrpc-codegen/generator/generator.go b/tools/openrpc-codegen/generator/generator.go index bd6275d88..12222a3bb 100644 --- a/tools/openrpc-codegen/generator/generator.go +++ b/tools/openrpc-codegen/generator/generator.go @@ -116,7 +116,7 @@ func getTypeFromSchema(schema schema.Schema) string { } func GenerateServerCode(buf *bytes.Buffer, spec schema.Spec, pkg string) error { - if err := addPackageName(buf, pkg); err != nil { + if err := addHeading(buf, pkg); err != nil { return fmt.Errorf("failed to write pkg name: %w", err) } diff --git a/tools/openrpc-codegen/generator/utils.go b/tools/openrpc-codegen/generator/utils.go index 7a3be6b47..9c3304733 100644 --- a/tools/openrpc-codegen/generator/utils.go +++ b/tools/openrpc-codegen/generator/utils.go @@ -46,8 +46,10 @@ func executeTemplate(buf *bytes.Buffer, tmpl string, data interface{}) error { return nil } -func addPackageName(buf *bytes.Buffer, pkg string) error { - pkgLine := fmt.Sprintf("package %s\n", pkg) - _, err := buf.Write([]byte(pkgLine)) +func addHeading(buf *bytes.Buffer, pkg string) error { + heading := "" + heading += "// AUTO-GENERATED: this file is auto generated please don't edit\n" + heading += fmt.Sprintf("package %s\n", pkg) + _, err := buf.Write([]byte(heading)) return err } diff --git a/tools/openrpc-codegen/main.go b/tools/openrpc-codegen/main.go index 6fa03af5b..1b2f16f33 100644 --- a/tools/openrpc-codegen/main.go +++ b/tools/openrpc-codegen/main.go @@ -20,7 +20,7 @@ func run() error { var f flags flag.StringVar(&f.spec, "spec", "", "openrpc spec file") flag.StringVar(&f.output, "output", "", "generated go code") - flag.StringVar(&f.pkg, "pkg", "apirpc", "name of the go package") + flag.StringVar(&f.pkg, "pkg", "rpc", "name of the go package") flag.Parse() if f.spec == "" || f.output == "" {