{"id":1616,"date":"2026-09-14T10:18:50","date_gmt":"2026-09-14T13:18:50","guid":{"rendered":"https:\/\/www.linuxpro.com.br\/?p=1616"},"modified":"2026-10-07T08:30:09","modified_gmt":"2026-10-07T11:30:09","slug":"api-do-virtualbox-em-go-liste-vms-com-soap","status":"publish","type":"post","link":"https:\/\/www.linuxpro.com.br\/en\/2026\/09\/api-do-virtualbox-em-go-liste-vms-com-soap\/","title":{"rendered":"VirtualBox API \u2014 part 2: SOAP client in Go"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/09\/virtualbox-api-golang-v1.webp\" alt=\"Mascote LinuxPro e cachorro caramelo cyborg em laborat\u00f3rio com m\u00e1quinas virtuais, Gopher do Go e logo VirtualBox.\" width=\"1486\" height=\"856\" \/><\/p>\n<p>Vamos implementar um cliente em Go que autentica no web service do VirtualBox, lista UUID, nome e estado das m\u00e1quinas e encerra a sess\u00e3o. O programa utiliza somente a biblioteca padr\u00e3o e produz um invent\u00e1rio JSON, sem executar a\u00e7\u00f5es de escrita sobre as VMs.<\/p>\n<p>Esta \u00e9 a <strong>parte 2<\/strong>. Se voc\u00ea ainda n\u00e3o conhece o servi\u00e7o, leia primeiro a <a href=\"\/2026\/09\/api-do-virtualbox-parte-1-soap-sessoes-e-seguranca\/\">parte 1: SOAP, sess\u00f5es e seguran\u00e7a na API do VirtualBox<\/a>. L\u00e1 est\u00e3o a prepara\u00e7\u00e3o do <code>vboxwebsrv<\/code>, o t\u00fanel SSH e a diferen\u00e7a entre refer\u00eancias e UUIDs.<\/p>\n<h2>Pr\u00e9-requisitos do exemplo<\/h2>\n<p>Tenha Go instalado e um host VirtualBox com <code>vboxwebsrv<\/code> ativo, autentica\u00e7\u00e3o configurada e acesso protegido. Os comandos abaixo usam <code>http:\/\/127.0.0.1:18083\/<\/code>: execute o cliente no mesmo host ou use o t\u00fanel explicado na parte 1. N\u00e3o desabilite a autentica\u00e7\u00e3o nem exponha essa porta diretamente \u00e0 internet.<\/p>\n<h2>Qual contrato foi usado e o que foi testado<\/h2>\n<p>As opera\u00e7\u00f5es e seus par\u00e2metros foram conferidos no WSDL inclu\u00eddo no <a href=\"https:\/\/download.virtualbox.org\/virtualbox\/7.2.0\/\">SDK oficial 7.2.0<\/a>, usado como refer\u00eancia documental da linha 7.2. Isso n\u00e3o \u00e9 recomenda\u00e7\u00e3o para instalar um patch antigo: use uma vers\u00e3o mantida do VirtualBox e confira o SDK correspondente ao seu ambiente.<\/p>\n<p><strong>Valida\u00e7\u00e3o original:<\/strong> o c\u00f3digo foi compilado, verificado com <code>go vet<\/code> e testado com servidor SOAP simulado e detector de corridas. Tamb\u00e9m verificamos estruturas de requisi\u00e7\u00e3o contra o XSD do SDK. N\u00e3o houve teste de uma sess\u00e3o autenticada em um host VirtualBox real. Portanto, a integra\u00e7\u00e3o precisa passar por homologa\u00e7\u00e3o no seu laborat\u00f3rio antes de uso operacional.<\/p>\n<p>Os testes adicionais cobriram erro SOAP, XML inv\u00e1lido, resposta excessiva, redirecionamento, cancelamento, escape de caracteres e limpeza de sess\u00e3o ap\u00f3s falha. O exemplo n\u00e3o pretende substituir um SDK completo.<\/p>\n<p><strong>Revis\u00e3o de 7 de outubro de 2026:<\/strong> repetimos compila\u00e7\u00e3o, <code>go vet<\/code> e testes com <code>-race<\/code> usando o c\u00f3digo extra\u00eddo do artigo. Acrescentamos cancelamento por Ctrl+C, teste de invent\u00e1rio vazio com logoff e verifica\u00e7\u00f5es simuladas de ciclo completo, escape, resposta inv\u00e1lida, limite de tamanho e limpeza ap\u00f3s cancelamento. A verifica\u00e7\u00e3o original do XSD n\u00e3o foi repetida; continua sem homologa\u00e7\u00e3o autenticada em host real.<\/p>\n<h2>Fluxo: login, invent\u00e1rio, limpeza e sa\u00edda<\/h2>\n<p>O cliente autentica, recebe uma refer\u00eancia remota de <code>IVirtualBox<\/code>, solicita as refer\u00eancias das m\u00e1quinas e consulta UUID, nome e estado de cada uma. S\u00f3 depois de tentar encerrar a sess\u00e3o ele devolve o invent\u00e1rio para a sa\u00edda JSON. Uma refer\u00eancia SOAP identifica um objeto dentro do contexto remoto; n\u00e3o \u00e9 o UUID persistente da VM.<\/p>\n<figure><img decoding=\"async\" src=\"\/wp-content\/uploads\/2026\/10\/virtualbox-soap-go-flow.webp\" width=\"1200\" height=\"469\" alt=\"Fluxo do cliente Go: login SOAP, leitura das m\u00e1quinas, tentativa de logoff e sa\u00edda JSON somente ap\u00f3s sucesso.\" loading=\"lazy\" \/><figcaption>As chamadas usam um endpoint protegido. A limpeza tem prazo pr\u00f3prio para continuar poss\u00edvel ap\u00f3s o cancelamento da coleta.<\/figcaption><\/figure>\n<table>\n<thead>\n<tr>\n<th>Opera\u00e7\u00e3o<\/th>\n<th>Entrada principal<\/th>\n<th>Resultado esperado<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>IWebsessionManager_logon<\/code><\/td>\n<td>Usu\u00e1rio e senha.<\/td>\n<td>Refer\u00eancia do objeto IVirtualBox.<\/td>\n<\/tr>\n<tr>\n<td><code>IVirtualBox_getMachines<\/code><\/td>\n<td><code>_this<\/code> com a refer\u00eancia recebida.<\/td>\n<td>Refer\u00eancias das m\u00e1quinas, n\u00e3o seus UUIDs.<\/td>\n<\/tr>\n<tr>\n<td><code>IMachine_getId\/getName\/getState<\/code><\/td>\n<td><code>_this<\/code> com a refer\u00eancia da m\u00e1quina.<\/td>\n<td>Um valor por consulta.<\/td>\n<\/tr>\n<tr>\n<td><code>IWebsessionManager_logoff<\/code><\/td>\n<td><code>refIVirtualBox<\/code>.<\/td>\n<td>Encerramento da sess\u00e3o remota.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Para <em>N<\/em> m\u00e1quinas, o caminho de sucesso deste exemplo faz <code>3N + 3<\/code> chamadas SOAP: login, listagem, tr\u00eas atributos por VM e logoff. \u00c9 uma consequ\u00eancia do c\u00f3digo sequencial abaixo, n\u00e3o um requisito de todos os clientes VirtualBox. Quanto maior o invent\u00e1rio, mais importante medir lat\u00eancia e prazo total.<\/p>\n<h2>1. Criar o projeto Go<\/h2>\n<pre><code class=\"language-bash\">mkdir vbox-inventory\ncd vbox-inventory\ngo mod init example.com\/vbox-inventory<\/code><\/pre>\n<p>O exemplo utiliza <code>errors.Join<\/code>, dispon\u00edvel a partir do Go 1.20; prefira uma vers\u00e3o atualmente mantida. N\u00e3o h\u00e1 depend\u00eancias externas para baixar. Vamos separar o transporte SOAP em <code>client.go<\/code> e o invent\u00e1rio em <code>main.go<\/code>.<\/p>\n<h2>2. Implementar o transporte SOAP<\/h2>\n<p>Salve como <code>client.go<\/code>. O cliente aceita HTTP apenas para loopback, n\u00e3o segue redirecionamentos, limita a resposta e deixa a valida\u00e7\u00e3o TLS padr\u00e3o ativa:<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t&quot;bytes&quot;\n\t&quot;context&quot;\n\t&quot;encoding\/xml&quot;\n\t&quot;errors&quot;\n\t&quot;fmt&quot;\n\t&quot;io&quot;\n\t&quot;net&quot;\n\t&quot;net\/http&quot;\n\t&quot;net\/url&quot;\n\t&quot;time&quot;\n)\n\nconst apiNS = &quot;http:\/\/www.virtualbox.org\/&quot;\nconst soapNS = &quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/&quot;\nconst maxResponse = 2 &lt;&lt; 20\n\ntype client struct {\n\tendpoint string\n\thttp     *http.Client\n}\n\ntype field struct{ name, value string }\n\ntype reply struct {\n\tXMLName xml.Name `xml:&quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/ Envelope&quot;`\n\tBody    struct {\n\t\tFault  *struct{} `xml:&quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/ Fault&quot;`\n\t\tResult struct {\n\t\t\tXMLName xml.Name\n\t\t\tValues  []string `xml:&quot;returnval&quot;`\n\t\t} `xml:&quot;,any&quot;`\n\t} `xml:&quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/ Body&quot;`\n}\n\nfunc newClient(endpoint string) (*client, error) {\n\tu, err := url.Parse(endpoint)\n\tif err != nil || u.Host == &quot;&quot; || u.User != nil || u.RawQuery != &quot;&quot; || u.Fragment != &quot;&quot; {\n\t\treturn nil, errors.New(&quot;endpoint inv\u00e1lido: use URL sem credenciais ou par\u00e2metros&quot;)\n\t}\n\tip := net.ParseIP(u.Hostname())\n\tloopback := u.Hostname() == &quot;localhost&quot; || (ip != nil &amp;&amp; ip.IsLoopback())\n\tif u.Scheme != &quot;https&quot; &amp;&amp; !(u.Scheme == &quot;http&quot; &amp;&amp; loopback) {\n\t\treturn nil, errors.New(&quot;use HTTPS; HTTP s\u00f3 \u00e9 permitido no loopback&quot;)\n\t}\n\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\ttransport.Proxy = nil \/\/ N\u00e3o encaminhar credenciais por proxy do ambiente.\n\treturn &amp;client{\n\t\tendpoint: endpoint,\n\t\thttp: &amp;http.Client{\n\t\t\tTransport:     transport,\n\t\t\tTimeout:       10 * time.Second,\n\t\t\tCheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },\n\t\t},\n\t}, nil\n}\n\nfunc (c *client) call(ctx context.Context, method string, fields ...field) (values []string, err error) {\n\tswitch method {\n\tcase &quot;IWebsessionManager_logon&quot;, &quot;IWebsessionManager_logoff&quot;, &quot;IVirtualBox_getMachines&quot;,\n\t\t&quot;IMachine_getId&quot;, &quot;IMachine_getName&quot;, &quot;IMachine_getState&quot;:\n\tdefault:\n\t\treturn nil, errors.New(&quot;opera\u00e7\u00e3o fora do escopo somente leitura&quot;)\n\t}\n\tvar params bytes.Buffer\n\tenc := xml.NewEncoder(&amp;params)\n\tfor _, f := range fields {\n\t\tif err := enc.EncodeElement(f.value, xml.StartElement{Name: xml.Name{Local: f.name}}); err != nil {\n\t\t\treturn nil, fmt.Errorf(&quot;codificar par\u00e2metros: %w&quot;, err)\n\t\t}\n\t}\n\tif err := enc.Flush(); err != nil {\n\t\treturn nil, fmt.Errorf(&quot;finalizar XML: %w&quot;, err)\n\t}\n\tbody := fmt.Sprintf(\n\t\t`&lt;s:Envelope xmlns:s=&quot;%s&quot; xmlns:v=&quot;%s&quot;&gt;&lt;s:Body&gt;&lt;v:%s&gt;%s&lt;\/v:%s&gt;&lt;\/s:Body&gt;&lt;\/s:Envelope&gt;`,\n\t\tsoapNS, apiNS, method, params.String(), method,\n\t)\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewBufferString(body))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(&quot;criar requisi\u00e7\u00e3o: %w&quot;, err)\n\t}\n\treq.Header.Set(&quot;Content-Type&quot;, &quot;text\/xml; charset=utf-8&quot;)\n\treq.Header.Set(&quot;SOAPAction&quot;, `&quot;&quot;`)\n\tres, err := c.http.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(&quot;transportar %s: %w&quot;, method, err)\n\t}\n\tdefer func() { err = errors.Join(err, res.Body.Close()) }()\n\tdata, err := io.ReadAll(io.LimitReader(res.Body, maxResponse+1))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(&quot;ler resposta: %w&quot;, err)\n\t}\n\tif len(data) &gt; maxResponse {\n\t\treturn nil, errors.New(&quot;resposta excede 2 MiB&quot;)\n\t}\n\tvar decoded reply\n\tdecodeErr := xml.Unmarshal(data, &amp;decoded)\n\tif decodeErr == nil &amp;&amp; decoded.Body.Fault != nil {\n\t\t\/\/ N\u00e3o imprimir faultstring\/detail: podem revelar informa\u00e7\u00f5es do host.\n\t\treturn nil, fmt.Errorf(&quot;falha SOAP em %s (HTTP %d)&quot;, method, res.StatusCode)\n\t}\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(&quot;%s: HTTP %d&quot;, method, res.StatusCode)\n\t}\n\tif decodeErr != nil {\n\t\treturn nil, fmt.Errorf(&quot;decodificar resposta: %w&quot;, decodeErr)\n\t}\n\tresult := decoded.Body.Result\n\tif result.XMLName.Space != apiNS || result.XMLName.Local != method+&quot;Response&quot; {\n\t\treturn nil, errors.New(&quot;resposta SOAP inesperada&quot;)\n\t}\n\treturn result.Values, nil\n}\n\nfunc (c *client) one(ctx context.Context, method string, fields ...field) (string, error) {\n\tvalues, err := c.call(ctx, method, fields...)\n\tif err != nil {\n\t\treturn &quot;&quot;, err\n\t}\n\tif len(values) != 1 || values[0] == &quot;&quot; {\n\t\treturn &quot;&quot;, errors.New(&quot;retorno escalar vazio ou inv\u00e1lido&quot;)\n\t}\n\treturn values[0], nil\n}<\/code><\/pre>\n<p>O <code>xml.Encoder<\/code> cuida do escape dos valores. N\u00e3o concatenamos a senha diretamente como XML. O namespace, as opera\u00e7\u00f5es e o <code>SOAPAction<\/code> vazio seguem o contrato consultado; erros HTTP e SOAP s\u00e3o tratados separadamente.<\/p>\n<p>Detalhes completos de falhas do servidor foram omitidos da mensagem para reduzir exposi\u00e7\u00e3o de informa\u00e7\u00f5es. Em um produto real, voc\u00ea pode classificar os tipos de falha internamente, mantendo segredos e detalhes sens\u00edveis fora dos logs. Veja as refer\u00eancias de <a href=\"https:\/\/pkg.go.dev\/encoding\/xml\">encoding\/xml<\/a> e <a href=\"https:\/\/pkg.go.dev\/net\/http\">net\/http<\/a>.<\/p>\n<h2>3. Listar as m\u00e1quinas e tentar encerrar a sess\u00e3o<\/h2>\n<p>Salve como <code>main.go<\/code>:<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t&quot;context&quot;\n\t&quot;encoding\/json&quot;\n\t&quot;errors&quot;\n\t&quot;fmt&quot;\n\t&quot;os&quot;\n\t&quot;os\/signal&quot;\n\t&quot;time&quot;\n)\n\ntype machine struct {\n\tID    string `json:&quot;id&quot;`\n\tName  string `json:&quot;name&quot;`\n\tState string `json:&quot;state&quot;`\n}\n\nfunc inventory(ctx context.Context, c *client, username, password string) (vms []machine, err error) {\n\tref, err := c.one(ctx, &quot;IWebsessionManager_logon&quot;,\n\t\tfield{name: &quot;username&quot;, value: username},\n\t\tfield{name: &quot;password&quot;, value: password},\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\t\/\/ A limpeza precisa funcionar mesmo se o contexto principal expirar.\n\t\tcleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\t\tdefer cancel()\n\t\t_, closeErr := c.call(cleanup, &quot;IWebsessionManager_logoff&quot;, field{name: &quot;refIVirtualBox&quot;, value: ref})\n\t\tif closeErr != nil {\n\t\t\terr = errors.Join(err, fmt.Errorf(&quot;encerrar sess\u00e3o: %w&quot;, closeErr))\n\t\t}\n\t}()\n\trefs, err := c.call(ctx, &quot;IVirtualBox_getMachines&quot;, field{name: &quot;_this&quot;, value: ref})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvms = make([]machine, 0, len(refs))\n\tfor _, machineRef := range refs {\n\t\ttarget := field{name: &quot;_this&quot;, value: machineRef}\n\t\tid, err := c.one(ctx, &quot;IMachine_getId&quot;, target)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tname, err := c.one(ctx, &quot;IMachine_getName&quot;, target)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tstate, err := c.one(ctx, &quot;IMachine_getState&quot;, target)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvms = append(vms, machine{ID: id, Name: name, State: state})\n\t}\n\treturn vms, nil\n}\n\nfunc run() error {\n\tendpoint := os.Getenv(&quot;VBOX_URL&quot;)\n\tif endpoint == &quot;&quot; {\n\t\tendpoint = &quot;http:\/\/127.0.0.1:18083\/&quot;\n\t}\n\tusername, password := os.Getenv(&quot;VBOX_USER&quot;), os.Getenv(&quot;VBOX_PASSWORD&quot;)\n\tif username == &quot;&quot; || password == &quot;&quot; {\n\t\treturn errors.New(&quot;defina VBOX_USER e VBOX_PASSWORD&quot;)\n\t}\n\tc, err := newClient(endpoint)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer c.http.CloseIdleConnections()\n\tshutdown, stop := signal.NotifyContext(context.Background(), os.Interrupt)\n\tdefer stop()\n\tctx, cancel := context.WithTimeout(shutdown, 30*time.Second)\n\tdefer cancel()\n\tvms, err := inventory(ctx, c, username, password)\n\tif err != nil {\n\t\treturn err\n\t}\n\tout := json.NewEncoder(os.Stdout)\n\tout.SetIndent(&quot;&quot;, &quot;  &quot;)\n\tif err := out.Encode(vms); err != nil {\n\t\treturn fmt.Errorf(&quot;escrever invent\u00e1rio: %w&quot;, err)\n\t}\n\treturn nil\n}\n\nfunc main() {\n\tif err := run(); err != nil {\n\t\tfmt.Fprintln(os.Stderr, &quot;erro:&quot;, err)\n\t\tos.Exit(1)\n\t}\n}\n<\/code><\/pre>\n<p>O <code>defer<\/code> para limpeza \u00e9 registrado assim que o login retorna uma refer\u00eancia v\u00e1lida. Ele usa um contexto independente e curto: se a consulta principal expirar, ainda haver\u00e1 uma tentativa de encerrar a sess\u00e3o. Uma falha no logoff n\u00e3o \u00e9 silenciosamente descartada. O tratamento de interrup\u00e7\u00e3o usa <a href=\"https:\/\/pkg.go.dev\/os\/signal#NotifyContext\">signal.NotifyContext<\/a>, da biblioteca padr\u00e3o.<\/p>\n<p>A execu\u00e7\u00e3o \u00e9 sequencial por clareza. O nome, estado e UUID exigem consultas separadas; n\u00e3o \u00e9 um coletor otimizado para invent\u00e1rios enormes. O programa interrompe a coleta no primeiro erro e n\u00e3o apresenta um invent\u00e1rio parcial como se fosse completo. A restri\u00e7\u00e3o de opera\u00e7\u00f5es \u00e9 do cliente did\u00e1tico, n\u00e3o uma pol\u00edtica de autoriza\u00e7\u00e3o aplicada pelo servidor.<\/p>\n<h3>O que a limpeza garante \u2014 e o que ela n\u00e3o garante<\/h3>\n<p>O contexto da coleta agora observa <code>os.Interrupt<\/code>: Ctrl+C pede cancelamento e permite que a fun\u00e7\u00e3o retorne passando pelo <code>defer<\/code>. A limpeza ainda pode consumir at\u00e9 cinco segundos adicionais. Somente Ctrl+C\/SIGINT foi registrado: SIGTERM n\u00e3o recebe esse tratamento cooperativo no exemplo. Isso tamb\u00e9m n\u00e3o cobre encerramento for\u00e7ado do processo, queda da m\u00e1quina ou perda de comunica\u00e7\u00e3o com o servi\u00e7o.<\/p>\n<p>Se o servidor aceitar o login, mas a resposta se perder antes de o cliente receber a refer\u00eancia, o programa n\u00e3o ter\u00e1 como executar esse logoff. A expira\u00e7\u00e3o de sess\u00f5es do servi\u00e7o continua importante. Por outro lado, se o logoff falhar depois de uma coleta completa, esta implementa\u00e7\u00e3o retorna erro e n\u00e3o imprime JSON de sucesso: uma decis\u00e3o conservadora, documentada, n\u00e3o prova de que a sess\u00e3o remota permaneceu aberta.<\/p>\n<p>\u201cSomente leitura\u201d descreve as a\u00e7\u00f5es sobre VMs: autenticar e encerrar a sess\u00e3o ainda alteram o estado administrativo do web service. O programa n\u00e3o solicita bloqueio de m\u00e1quina, n\u00e3o inicia VMs e n\u00e3o modifica discos. A conta de servi\u00e7o, por\u00e9m, pode possuir mais permiss\u00f5es que o cliente utiliza; a lista permitida de m\u00e9todos n\u00e3o substitui isolamento e autoriza\u00e7\u00e3o no ambiente.<\/p>\n<h2>4. Compilar e executar sem colocar a senha no hist\u00f3rico<\/h2>\n<p>No Bash, compile e leia a senha sem escrev\u00ea-la na linha de comando:<\/p>\n<pre><code class=\"language-bash\">gofmt -w client.go main.go\ngo vet .\/...\ngo build -o vbox-inventory .\n\nexport VBOX_URL=&#x27;http:\/\/127.0.0.1:18083\/&#x27;\nIFS= read -r -p &#x27;Usu\u00e1rio do servi\u00e7o: &#x27; VBOX_USER\nexport VBOX_USER\nIFS= read -r -s -p &#x27;Senha do servi\u00e7o: &#x27; VBOX_PASSWORD\nprintf &#x27;\\n&#x27;\nexport VBOX_PASSWORD\n.\/vbox-inventory\nunset VBOX_PASSWORD<\/code><\/pre>\n<p>Vari\u00e1veis de ambiente n\u00e3o s\u00e3o um cofre: processos privilegiados e ferramentas de diagn\u00f3stico podem exp\u00f4-las. Em automa\u00e7\u00e3o, integre um mecanismo de segredos apropriado e n\u00e3o imprima ambiente, XML de login ou refer\u00eancias de sess\u00e3o.<\/p>\n<p>Formato ilustrativo de sa\u00edda, n\u00e3o capturado de um host real:<\/p>\n<pre><code class=\"language-json\">[\n  {\n    &quot;id&quot;: &quot;11111111-2222-3333-4444-555555555555&quot;,\n    &quot;name&quot;: &quot;Linux lab&quot;,\n    &quot;state&quot;: &quot;PoweredOff&quot;\n  }\n]<\/code><\/pre>\n<p>Se a conta n\u00e3o tiver m\u00e1quinas registradas, a sa\u00edda ser\u00e1 <code>[]<\/code>. Um erro faz o programa terminar com c\u00f3digo diferente de zero. Antes de concluir que o invent\u00e1rio est\u00e1 errado, confirme qual ambiente de usu\u00e1rio o servi\u00e7o est\u00e1 expondo.<\/p>\n<h2>5. Um teste automatizado sem hipervisor<\/h2>\n<p>Salve este teste como <code>client_test.go<\/code>. Ele verifica que um SOAP Fault produz erro sem reproduzir os detalhes privados do servidor:<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t&quot;context&quot;\n\t&quot;io&quot;\n\t&quot;net\/http&quot;\n\t&quot;net\/http\/httptest&quot;\n\t&quot;strings&quot;\n\t&quot;testing&quot;\n)\n\nfunc TestSOAPFaultIsNotSuccess(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tbody := `&lt;s:Envelope xmlns:s=&quot;http:\/\/schemas.xmlsoap.org\/soap\/envelope\/&quot;&gt;` +\n\t\t\t`&lt;s:Body&gt;&lt;s:Fault&gt;&lt;faultstring&gt;private-host-detail&lt;\/faultstring&gt;&lt;\/s:Fault&gt;&lt;\/s:Body&gt;&lt;\/s:Envelope&gt;`\n\t\tif _, err := io.WriteString(w, body); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}))\n\tdefer server.Close()\n\tc, err := newClient(server.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.http.CloseIdleConnections()\n\t_, err = c.call(context.Background(), &quot;IVirtualBox_getMachines&quot;, field{name: &quot;_this&quot;, value: &quot;test-ref&quot;})\n\tif err == nil {\n\t\tt.Fatal(&quot;SOAP Fault deveria produzir erro&quot;)\n\t}\n\tif strings.Contains(err.Error(), &quot;private-host-detail&quot;) {\n\t\tt.Fatal(&quot;erro exp\u00f4s detalhes do host&quot;)\n\t}\n}<\/code><\/pre>\n<pre><code class=\"language-bash\">go test -race -count=1 .\/...<\/code><\/pre>\n<p>Esse teste exercita o transporte com um servidor simulado. Ele n\u00e3o comprova credenciais, permiss\u00f5es ou compatibilidade operacional com seu VirtualBox. Complete a homologa\u00e7\u00e3o com uma m\u00e1quina de laborat\u00f3rio e compare UUID, nome e estado com a administra\u00e7\u00e3o local.<\/p>\n<h3>Teste tamb\u00e9m invent\u00e1rio vazio e encerramento<\/h3>\n<p>Salve o teste abaixo como <code>empty_test.go<\/code>, ao lado dos demais arquivos, e repita <code>go test -race -count=1 .\/...<\/code>. Ele exige sa\u00edda serializada como lista vazia e confirma\u00e7\u00e3o de que o cliente tentou o logoff:<\/p>\n<pre><code class=\"language-go\">package main\n\nimport (\n\t&quot;context&quot;\n\t&quot;encoding\/json&quot;\n\t&quot;encoding\/xml&quot;\n\t&quot;fmt&quot;\n\t&quot;io&quot;\n\t&quot;net\/http&quot;\n\t&quot;net\/http\/httptest&quot;\n\t&quot;sync\/atomic&quot;\n\t&quot;testing&quot;\n)\n\nfunc TestEmptyInventoryLogsOff(t *testing.T) {\n\tvar loggedOff atomic.Bool\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar req struct {\n\t\t\tBody struct {\n\t\t\t\tMethod struct{ XMLName xml.Name } `xml:&quot;,any&quot;`\n\t\t\t} `xml:&quot;Body&quot;`\n\t\t}\n\t\tif err := xml.NewDecoder(r.Body).Decode(&amp;req); err != nil {\n\t\t\tt.Error(err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tmethod := req.Body.Method.XMLName.Local\n\t\tvalue := &quot;&quot;\n\t\tswitch method {\n\t\tcase &quot;IWebsessionManager_logon&quot;:\n\t\t\tvalue = &quot;&lt;returnval&gt;test-session&lt;\/returnval&gt;&quot;\n\t\tcase &quot;IVirtualBox_getMachines&quot;:\n\t\tcase &quot;IWebsessionManager_logoff&quot;:\n\t\t\tloggedOff.Store(true)\n\t\tdefault:\n\t\t\tt.Errorf(&quot;opera\u00e7\u00e3o inesperada: %s&quot;, method)\n\t\t}\n\t\tbody := fmt.Sprintf(\n\t\t\t`&lt;s:Envelope xmlns:s=&quot;%s&quot;&gt;&lt;s:Body&gt;&lt;v:%sResponse xmlns:v=&quot;%s&quot;&gt;%s&lt;\/v:%sResponse&gt;&lt;\/s:Body&gt;&lt;\/s:Envelope&gt;`,\n\t\t\tsoapNS, method, apiNS, value, method,\n\t\t)\n\t\tif _, err := io.WriteString(w, body); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}))\n\tdefer server.Close()\n\tc, err := newClient(server.URL)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer c.http.CloseIdleConnections()\n\tvms, err := inventory(context.Background(), c, &quot;lab-user&quot;, &quot;lab-password&quot;)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdata, err := json.Marshal(vms)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(data) != &quot;[]&quot; || !loggedOff.Load() {\n\t\tt.Fatalf(&quot;invent\u00e1rio=%s; logoff=%v&quot;, data, loggedOff.Load())\n\t}\n}\n<\/code><\/pre>\n<h2>Homologa\u00e7\u00e3o em host real: roteiro de aceita\u00e7\u00e3o<\/h2>\n<ol>\n<li>Registre vers\u00e3o do VirtualBox, SDK usado como contrato, configura\u00e7\u00e3o do servi\u00e7o e conta sob a qual ele roda.<\/li>\n<li>Use um endpoint protegido do laborat\u00f3rio, com autentica\u00e7\u00e3o ativa. N\u00e3o reutilize credenciais de produ\u00e7\u00e3o em fixtures.<\/li>\n<li>Compare os UUIDs com <code>VBoxManage list vms<\/code> executado no mesmo contexto de usu\u00e1rio do servi\u00e7o. Consulte estados por <code>VBoxManage showvminfo UUID --machinereadable<\/code>, substituindo UUID pelo identificador real.<\/li>\n<li>Teste lista vazia, nomes com acentos e caracteres XML, credencial inv\u00e1lida, cancelamento e indisponibilidade.<\/li>\n<li>Confirme que o programa s\u00f3 consulta atributos e que nenhum comando de escrita foi acrescentado ao adaptador.<\/li>\n<li>Documente as falhas e os limites encontrados antes de usar o invent\u00e1rio em automa\u00e7\u00e3o.<\/li>\n<\/ol>\n<p>O invent\u00e1rio n\u00e3o \u00e9 uma fotografia at\u00f4mica: uma VM pode mudar de estado entre consultas. A ordem da lista tamb\u00e9m n\u00e3o deve ser tratada como contrato. Para compara\u00e7\u00e3o automatizada, use UUID como chave e aceite que estados podem mudar durante a coleta.<\/p>\n<h2>Falhas comuns e diagn\u00f3stico<\/h2>\n<table>\n<thead>\n<tr>\n<th>Problema<\/th>\n<th>O que verificar<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Conex\u00e3o recusada<\/td>\n<td>Servi\u00e7o ativo, endere\u00e7o, porta e t\u00fanel SSH.<\/td>\n<\/tr>\n<tr>\n<td>Falha SOAP no login<\/td>\n<td>Autentica\u00e7\u00e3o configurada, usu\u00e1rio e senha; n\u00e3o desligue a prote\u00e7\u00e3o como corre\u00e7\u00e3o.<\/td>\n<\/tr>\n<tr>\n<td>Lista vazia<\/td>\n<td>Usu\u00e1rio que executa o servi\u00e7o e m\u00e1quinas registradas naquele ambiente.<\/td>\n<\/tr>\n<tr>\n<td>Refer\u00eancia inv\u00e1lida<\/td>\n<td>Sess\u00e3o expirada ou refer\u00eancia reutilizada de outra execu\u00e7\u00e3o.<\/td>\n<\/tr>\n<tr>\n<td>Erro de certificado<\/td>\n<td>Nome do servidor e cadeia de confian\u00e7a; n\u00e3o use <code>InsecureSkipVerify<\/code>.<\/td>\n<\/tr>\n<tr>\n<td>Resposta inesperada<\/td>\n<td>Endpoint correto, namespace, vers\u00e3o e contrato WSDL.<\/td>\n<\/tr>\n<tr>\n<td>Prazo excedido<\/td>\n<td>Lat\u00eancia, n\u00famero de VMs e limites configurados.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>O programa utiliza dez segundos por requisi\u00e7\u00e3o, trinta para a coleta e cinco para a limpeza. S\u00e3o escolhas did\u00e1ticas, n\u00e3o valores universais. Ajuste-os com medi\u00e7\u00e3o. N\u00e3o acrescente repeti\u00e7\u00e3o autom\u00e1tica indiscriminada quando evoluir para a\u00e7\u00f5es de escrita: um timeout n\u00e3o prova que o servidor deixou de executar a opera\u00e7\u00e3o.<\/p>\n<p>Quer evoluir de consultas para a\u00e7\u00f5es sobre as VMs? Revise na <a href=\"\/2026\/09\/api-do-virtualbox-parte-1-soap-sessoes-e-seguranca\/\">parte 1<\/a> os cuidados com sess\u00f5es de m\u00e1quina, bloqueios, autoriza\u00e7\u00e3o e opera\u00e7\u00f5es de escrita.<\/p>\n<h2>Como transformar o exemplo em um projeto maior<\/h2>\n<ul>\n<li>Fixar uma matriz de vers\u00f5es e validar o WSDL correspondente.<\/li>\n<li>Separar transporte, autentica\u00e7\u00e3o, invent\u00e1rio e opera\u00e7\u00f5es de escrita.<\/li>\n<li>Adicionar testes com respostas reais sanitizadas, al\u00e9m dos simulados.<\/li>\n<li>Tratar pagina\u00e7\u00e3o ou limites da aplica\u00e7\u00e3o, cache e prazo total de coleta.<\/li>\n<li>Para agentes persistentes, estudar libera\u00e7\u00e3o de refer\u00eancias e reconex\u00e3o, sem acumular objetos indefinidamente.<\/li>\n<li>Versionar o contrato p\u00fablico sem expor os detalhes SOAP aos consumidores.<\/li>\n<\/ul>\n<p>Se o escopo crescer, um cliente gerado a partir do WSDL pode reduzir trabalho repetitivo, mas ainda exige valida\u00e7\u00e3o de tipos, namespaces e falhas. O pequeno cliente manual deste artigo serve para entender o caminho, n\u00e3o para reimplementar toda a API \u00e0 m\u00e3o.<\/p>\n<p>Esse trabalho se conecta \u00e0 nossa s\u00e9rie: <a href=\"\/2026\/09\/phpvirtualbox-em-go-parte-1-echo-vue-e-binario-unico\/\">arquitetura em Go, Echo e Vue<\/a>, <a href=\"\/2026\/09\/phpvirtualbox-em-go-parte-2-migracao-com-ia-e-openspec\/\">planejamento com IA e OpenSpec<\/a> e <a href=\"\/2026\/09\/phpvirtualbox-em-go-parte-3-roteiro-de-implementacao-e-testes\/\">implementa\u00e7\u00e3o incremental e testes<\/a>. Aqui, a base \u00e9 pr\u00e1tica: primeiro consultar corretamente; depois ampliar com evid\u00eancias.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Create a Go client for the VirtualBox SOAP API: authentication, VM inventory, session termination, timeouts, security, and testing.<\/p>","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[45,2,120],"tags":[31],"class_list":["post-1616","post","type-post","status-publish","format-standard","hentry","category-desenv","category-linux","category-servidores","tag-golang"],"_links":{"self":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1616","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/comments?post=1616"}],"version-history":[{"count":4,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1616\/revisions"}],"predecessor-version":[{"id":1832,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/posts\/1616\/revisions\/1832"}],"wp:attachment":[{"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/media?parent=1616"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/categories?post=1616"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.linuxpro.com.br\/en\/wp-json\/wp\/v2\/tags?post=1616"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}