tag v0.155.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.155.0 ---------------- * Fedora 43: add shadow-utils when LockRoot is enabled, update cloud-init service name (osbuild/images#1618) * Author: Achilleas Koutsou, Reviewers: Gianluca Zuccarelli, Michael Vogt * Update osbuild dependency commit ID to latest (osbuild/images#1609) * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger, Tomáš Hozza * Update snapshots to 20250626 (osbuild/images#1623) * Author: SchutzBot, Reviewers: Achilleas Koutsou, Simon de Vlieger * distro/rhel9: xz compress azure-cvm image type [HMS-8587] (osbuild/images#1620) * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza * distro/rhel: introduce new image type: Azure SAP Apps [HMS-8738] (osbuild/images#1612) * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza * distro/rhel: move ansible-core to sap_extras_pkgset (osbuild/images#1624) * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Tomáš Hozza * github/create-tag: allow passing the version when run manually (osbuild/images#1621) * Author: Achilleas Koutsou, Reviewers: Lukáš Zapletal, Tomáš Hozza * rhel9: move image-config into pure YAML (HMS-8593) (osbuild/images#1616) * Author: Michael Vogt, Reviewers: Achilleas Koutsou, Simon de Vlieger * test: split manifest checksums into separate files (osbuild/images#1625) * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza — Somewhere on the Internet, 2025-06-30 --- tag v0.156.0 Tagger: imagebuilder-bot <imagebuilder-bots+imagebuilder-bot@redhat.com> Changes with 0.156.0 ---------------- * Many: delete repositories for EOL distributions (HMS-7044) (osbuild/images#1607) * Author: Tomáš Hozza, Reviewers: Michael Vogt, Simon de Vlieger * RHSM/facts: add 'image-builder CLI' API type (osbuild/images#1640) * Author: Tomáš Hozza, Reviewers: Brian C. Lane, Simon de Vlieger * Update dependencies 2025-06-29 (osbuild/images#1628) * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza * Update osbuild dependency commit ID to latest (osbuild/images#1627) * Author: SchutzBot, Reviewers: Simon de Vlieger, Tomáš Hozza * [RFC] image: drop `InstallWeakDeps` from image.DiskImage (osbuild/images#1642) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger, Tomáš Hozza * build(deps): bump the go-deps group across 1 directory with 3 updates (osbuild/images#1632) * Author: dependabot[bot], Reviewers: SchutzBot, Tomáš Hozza * distro/rhel10: xz compress azure-cvm image type (osbuild/images#1638) * Author: Achilleas Koutsou, Reviewers: Brian C. Lane, Simon de Vlieger * distro: cleanup/refactor distro/{defs,generic} (HMS-8744) (osbuild/images#1570) * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza * distro: remove some hardcoded values from generic/images.go (osbuild/images#1636) * Author: Michael Vogt, Reviewers: Simon de Vlieger, Tomáš Hozza * distro: small tweaks for the YAML based imagetypes (osbuild/images#1622) * Author: Michael Vogt, Reviewers: Brian C. Lane, Simon de Vlieger * fedora/wsl: packages and locale (osbuild/images#1635) * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza * image/many: make compression more generic (osbuild/images#1634) * Author: Simon de Vlieger, Reviewers: Brian C. Lane, Michael Vogt * manifest: handle content template name with spaces (osbuild/images#1641) * Author: Bryttanie, Reviewers: Brian C. Lane, Michael Vogt, Tomáš Hozza * many: implement gzip (osbuild/images#1633) * Author: Simon de Vlieger, Reviewers: Michael Vogt, Tomáš Hozza * rhel/azure: set GRUB_TERMINAL based on architecture [RHEL-91383] (osbuild/images#1626) * Author: Achilleas Koutsou, Reviewers: Simon de Vlieger, Tomáš Hozza — Somewhere on the Internet, 2025-07-07 ---
494 lines
13 KiB
Go
494 lines
13 KiB
Go
// © Broadcom. All Rights Reserved.
|
|
// The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package object
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/vmware/govmomi/internal"
|
|
"github.com/vmware/govmomi/property"
|
|
"github.com/vmware/govmomi/session"
|
|
"github.com/vmware/govmomi/vim25"
|
|
"github.com/vmware/govmomi/vim25/mo"
|
|
"github.com/vmware/govmomi/vim25/soap"
|
|
"github.com/vmware/govmomi/vim25/types"
|
|
)
|
|
|
|
// DatastoreNoSuchDirectoryError is returned when a directory could not be found.
|
|
type DatastoreNoSuchDirectoryError struct {
|
|
verb string
|
|
subject string
|
|
}
|
|
|
|
func (e DatastoreNoSuchDirectoryError) Error() string {
|
|
return fmt.Sprintf("cannot %s '%s': No such directory", e.verb, e.subject)
|
|
}
|
|
|
|
// DatastoreNoSuchFileError is returned when a file could not be found.
|
|
type DatastoreNoSuchFileError struct {
|
|
verb string
|
|
subject string
|
|
}
|
|
|
|
func (e DatastoreNoSuchFileError) Error() string {
|
|
return fmt.Sprintf("cannot %s '%s': No such file", e.verb, e.subject)
|
|
}
|
|
|
|
type Datastore struct {
|
|
Common
|
|
|
|
DatacenterPath string
|
|
}
|
|
|
|
func NewDatastore(c *vim25.Client, ref types.ManagedObjectReference) *Datastore {
|
|
return &Datastore{
|
|
Common: NewCommon(c, ref),
|
|
}
|
|
}
|
|
|
|
// FindInventoryPath sets InventoryPath and DatacenterPath,
|
|
// needed by NewURL() to compose an upload/download endpoint URL
|
|
func (d *Datastore) FindInventoryPath(ctx context.Context) error {
|
|
entities, err := mo.Ancestors(ctx, d.c, d.c.ServiceContent.PropertyCollector, d.r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
val := "/"
|
|
|
|
for _, entity := range entities {
|
|
if entity.Parent == nil {
|
|
continue // root folder
|
|
}
|
|
val = path.Join(val, entity.Name)
|
|
if entity.Self.Type == "Datacenter" {
|
|
d.DatacenterPath = val
|
|
}
|
|
}
|
|
|
|
d.InventoryPath = val
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d Datastore) Path(path string) string {
|
|
var p DatastorePath
|
|
if p.FromString(path) {
|
|
return p.String() // already in "[datastore] path" format
|
|
}
|
|
|
|
return (&DatastorePath{
|
|
Datastore: d.Name(),
|
|
Path: path,
|
|
}).String()
|
|
}
|
|
|
|
// NewDatastoreURL constructs a url.URL with the given file path for datastore access over HTTP.
|
|
func NewDatastoreURL(base url.URL, dcPath, dsName, path string) *url.URL {
|
|
scheme := base.Scheme
|
|
// In rare cases where vCenter and ESX are accessed using different schemes.
|
|
if overrideScheme := os.Getenv("GOVMOMI_DATASTORE_ACCESS_SCHEME"); overrideScheme != "" {
|
|
scheme = overrideScheme
|
|
}
|
|
|
|
base.Scheme = scheme
|
|
base.Path = fmt.Sprintf("/folder/%s", path)
|
|
base.RawQuery = url.Values{
|
|
"dcPath": []string{dcPath},
|
|
"dsName": []string{dsName},
|
|
}.Encode()
|
|
|
|
return &base
|
|
}
|
|
|
|
// NewURL constructs a url.URL with the given file path for datastore access over HTTP.
|
|
// The Datastore object is used to derive url, dcPath and dsName params to NewDatastoreURL.
|
|
// For dcPath, Datastore.DatacenterPath must be set and for dsName, Datastore.InventoryPath.
|
|
// This is the case when the object.Datastore instance is created by Finder.
|
|
// Otherwise, Datastore.FindInventoryPath should be called first, to set DatacenterPath
|
|
// and InventoryPath.
|
|
func (d Datastore) NewURL(path string) *url.URL {
|
|
u := d.c.URL()
|
|
return NewDatastoreURL(*u, d.DatacenterPath, d.Name(), path)
|
|
}
|
|
|
|
func (d Datastore) Browser(ctx context.Context) (*HostDatastoreBrowser, error) {
|
|
var do mo.Datastore
|
|
|
|
err := d.Properties(ctx, d.Reference(), []string{"browser"}, &do)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return NewHostDatastoreBrowser(d.c, do.Browser), nil
|
|
}
|
|
|
|
func (d Datastore) useServiceTicket() bool {
|
|
// If connected to workstation, service ticketing not supported
|
|
// If connected to ESX, service ticketing not needed
|
|
if !d.c.IsVC() {
|
|
return false
|
|
}
|
|
|
|
key := "GOVMOMI_USE_SERVICE_TICKET"
|
|
|
|
val := d.c.URL().Query().Get(key)
|
|
if val == "" {
|
|
val = os.Getenv(key)
|
|
}
|
|
|
|
if val == "1" || val == "true" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (d Datastore) useServiceTicketHostName(name string) bool {
|
|
// No need if talking directly to ESX.
|
|
if !d.c.IsVC() {
|
|
return false
|
|
}
|
|
|
|
// If version happens to be < 5.1
|
|
if name == "" {
|
|
return false
|
|
}
|
|
|
|
// If the HostSystem is using DHCP on a network without dynamic DNS,
|
|
// HostSystem.Config.Network.DnsConfig.HostName is set to "localhost" by default.
|
|
// This resolves to "localhost.localdomain" by default via /etc/hosts on ESX.
|
|
// In that case, we will stick with the HostSystem.Name which is the IP address that
|
|
// was used to connect the host to VC.
|
|
if name == "localhost.localdomain" {
|
|
return false
|
|
}
|
|
|
|
// Still possible to have HostName that don't resolve via DNS,
|
|
// so we default to false.
|
|
key := "GOVMOMI_USE_SERVICE_TICKET_HOSTNAME"
|
|
|
|
val := d.c.URL().Query().Get(key)
|
|
if val == "" {
|
|
val = os.Getenv(key)
|
|
}
|
|
|
|
if val == "1" || val == "true" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
type datastoreServiceTicketHostKey struct{}
|
|
|
|
// HostContext returns a Context where the given host will be used for datastore HTTP access
|
|
// via the ServiceTicket method.
|
|
func (d Datastore) HostContext(ctx context.Context, host *HostSystem) context.Context {
|
|
return context.WithValue(ctx, datastoreServiceTicketHostKey{}, host)
|
|
}
|
|
|
|
// ServiceTicket obtains a ticket via AcquireGenericServiceTicket and returns it an http.Cookie with the url.URL
|
|
// that can be used along with the ticket cookie to access the given path. An host is chosen at random unless the
|
|
// the given Context was created with a specific host via the HostContext method.
|
|
func (d Datastore) ServiceTicket(ctx context.Context, path string, method string) (*url.URL, *http.Cookie, error) {
|
|
if d.InventoryPath == "" {
|
|
_ = d.FindInventoryPath(ctx)
|
|
}
|
|
|
|
u := d.NewURL(path)
|
|
|
|
host, ok := ctx.Value(datastoreServiceTicketHostKey{}).(*HostSystem)
|
|
|
|
if !ok {
|
|
if !d.useServiceTicket() {
|
|
return u, nil, nil
|
|
}
|
|
|
|
hosts, err := d.AttachedHosts(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(hosts) == 0 {
|
|
// Fallback to letting vCenter choose a host
|
|
return u, nil, nil
|
|
}
|
|
|
|
// Pick a random attached host
|
|
host = hosts[rand.Intn(len(hosts))]
|
|
}
|
|
|
|
ips, err := host.ManagementIPs(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if len(ips) > 0 {
|
|
// prefer a ManagementIP
|
|
u.Host = ips[0].String()
|
|
} else {
|
|
// fallback to inventory name
|
|
u.Host, err = host.ObjectName(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
// VC datacenter path will not be valid against ESX
|
|
q := u.Query()
|
|
delete(q, "dcPath")
|
|
u.RawQuery = q.Encode()
|
|
|
|
// Now that we have a host selected, take a copy of the URL.
|
|
transferURL := *u
|
|
|
|
if internal.UsingEnvoySidecar(d.Client()) {
|
|
// Rewrite the host URL to go through the Envoy sidecar on VC.
|
|
// Reciever must use a custom dialer.
|
|
u = internal.HostGatewayTransferURL(u, host.Reference())
|
|
}
|
|
|
|
spec := types.SessionManagerHttpServiceRequestSpec{
|
|
// Use the original URL (without rewrites) for the session ticket.
|
|
Url: transferURL.String(),
|
|
// See SessionManagerHttpServiceRequestSpecMethod enum
|
|
Method: fmt.Sprintf("http%s%s", method[0:1], strings.ToLower(method[1:])),
|
|
}
|
|
|
|
sm := session.NewManager(d.Client())
|
|
|
|
ticket, err := sm.AcquireGenericServiceTicket(ctx, &spec)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
cookie := &http.Cookie{
|
|
Name: "vmware_cgi_ticket",
|
|
Value: ticket.Id,
|
|
}
|
|
|
|
if d.useServiceTicketHostName(ticket.HostName) {
|
|
u.Host = ticket.HostName
|
|
}
|
|
|
|
d.Client().SetThumbprint(u.Host, ticket.SslThumbprint)
|
|
|
|
return u, cookie, nil
|
|
}
|
|
|
|
func (d Datastore) uploadTicket(ctx context.Context, path string, param *soap.Upload) (*url.URL, *soap.Upload, error) {
|
|
p := soap.DefaultUpload
|
|
if param != nil {
|
|
p = *param // copy
|
|
}
|
|
|
|
u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if ticket != nil {
|
|
p.Ticket = ticket
|
|
p.Close = true // disable Keep-Alive connection to ESX
|
|
}
|
|
|
|
return u, &p, nil
|
|
}
|
|
|
|
func (d Datastore) downloadTicket(ctx context.Context, path string, param *soap.Download) (*url.URL, *soap.Download, error) {
|
|
p := soap.DefaultDownload
|
|
if param != nil {
|
|
p = *param // copy
|
|
}
|
|
|
|
u, ticket, err := d.ServiceTicket(ctx, path, p.Method)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if ticket != nil {
|
|
p.Ticket = ticket
|
|
p.Close = true // disable Keep-Alive connection to ESX
|
|
}
|
|
|
|
return u, &p, nil
|
|
}
|
|
|
|
// Upload via soap.Upload with an http service ticket
|
|
func (d Datastore) Upload(ctx context.Context, f io.Reader, path string, param *soap.Upload) error {
|
|
u, p, err := d.uploadTicket(ctx, path, param)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return d.Client().Upload(ctx, f, u, p)
|
|
}
|
|
|
|
// UploadFile via soap.Upload with an http service ticket
|
|
func (d Datastore) UploadFile(ctx context.Context, file string, path string, param *soap.Upload) error {
|
|
u, p, err := d.uploadTicket(ctx, path, param)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vc := d.Client()
|
|
if internal.UsingEnvoySidecar(vc) {
|
|
// Override the vim client with a new one that wraps a Unix socket transport.
|
|
// Using HTTP here so secure means nothing.
|
|
vc = internal.ClientWithEnvoyHostGateway(vc)
|
|
}
|
|
return vc.UploadFile(ctx, file, u, p)
|
|
}
|
|
|
|
// Download via soap.Download with an http service ticket
|
|
func (d Datastore) Download(ctx context.Context, path string, param *soap.Download) (io.ReadCloser, int64, error) {
|
|
u, p, err := d.downloadTicket(ctx, path, param)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return d.Client().Download(ctx, u, p)
|
|
}
|
|
|
|
// DownloadFile via soap.Download with an http service ticket
|
|
func (d Datastore) DownloadFile(ctx context.Context, path string, file string, param *soap.Download) error {
|
|
u, p, err := d.downloadTicket(ctx, path, param)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
vc := d.Client()
|
|
if internal.UsingEnvoySidecar(vc) {
|
|
// Override the vim client with a new one that wraps a Unix socket transport.
|
|
// Using HTTP here so secure means nothing.
|
|
vc = internal.ClientWithEnvoyHostGateway(vc)
|
|
}
|
|
return vc.DownloadFile(ctx, file, u, p)
|
|
}
|
|
|
|
// AttachedHosts returns hosts that have this Datastore attached, accessible and writable.
|
|
func (d Datastore) AttachedHosts(ctx context.Context) ([]*HostSystem, error) {
|
|
var ds mo.Datastore
|
|
var hosts []*HostSystem
|
|
|
|
pc := property.DefaultCollector(d.Client())
|
|
err := pc.RetrieveOne(ctx, d.Reference(), []string{"host"}, &ds)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mounts := make(map[types.ManagedObjectReference]types.DatastoreHostMount)
|
|
var refs []types.ManagedObjectReference
|
|
for _, host := range ds.Host {
|
|
refs = append(refs, host.Key)
|
|
mounts[host.Key] = host
|
|
}
|
|
|
|
var hs []mo.HostSystem
|
|
err = pc.Retrieve(ctx, refs, []string{"runtime.connectionState", "runtime.powerState"}, &hs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, host := range hs {
|
|
if host.Runtime.ConnectionState == types.HostSystemConnectionStateConnected &&
|
|
host.Runtime.PowerState == types.HostSystemPowerStatePoweredOn {
|
|
|
|
mount := mounts[host.Reference()]
|
|
info := mount.MountInfo
|
|
|
|
if *info.Mounted && *info.Accessible && info.AccessMode == string(types.HostMountModeReadWrite) {
|
|
hosts = append(hosts, NewHostSystem(d.Client(), mount.Key))
|
|
}
|
|
}
|
|
}
|
|
|
|
return hosts, nil
|
|
}
|
|
|
|
// AttachedClusterHosts returns hosts that have this Datastore attached, accessible and writable and are members of the given cluster.
|
|
func (d Datastore) AttachedClusterHosts(ctx context.Context, cluster *ComputeResource) ([]*HostSystem, error) {
|
|
var hosts []*HostSystem
|
|
|
|
clusterHosts, err := cluster.Hosts(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
attachedHosts, err := d.AttachedHosts(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
refs := make(map[types.ManagedObjectReference]bool)
|
|
for _, host := range attachedHosts {
|
|
refs[host.Reference()] = true
|
|
}
|
|
|
|
for _, host := range clusterHosts {
|
|
if refs[host.Reference()] {
|
|
hosts = append(hosts, host)
|
|
}
|
|
}
|
|
|
|
return hosts, nil
|
|
}
|
|
|
|
func (d Datastore) Stat(ctx context.Context, file string) (types.BaseFileInfo, error) {
|
|
b, err := d.Browser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
spec := types.HostDatastoreBrowserSearchSpec{
|
|
Details: &types.FileQueryFlags{
|
|
FileType: true,
|
|
FileSize: true,
|
|
Modification: true,
|
|
FileOwner: types.NewBool(true),
|
|
},
|
|
MatchPattern: []string{path.Base(file)},
|
|
}
|
|
|
|
dsPath := d.Path(path.Dir(file))
|
|
task, err := b.SearchDatastore(ctx, dsPath, &spec)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info, err := task.WaitForResult(ctx, nil)
|
|
if err != nil {
|
|
if types.IsFileNotFound(err) {
|
|
// FileNotFound means the base path doesn't exist.
|
|
return nil, DatastoreNoSuchDirectoryError{"stat", dsPath}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
res := info.Result.(types.HostDatastoreBrowserSearchResults)
|
|
if len(res.File) == 0 {
|
|
// File doesn't exist
|
|
return nil, DatastoreNoSuchFileError{"stat", d.Path(file)}
|
|
}
|
|
|
|
return res.File[0], nil
|
|
|
|
}
|
|
|
|
// Type returns the type of file system volume.
|
|
func (d Datastore) Type(ctx context.Context) (types.HostFileSystemVolumeFileSystemType, error) {
|
|
var mds mo.Datastore
|
|
|
|
if err := d.Properties(ctx, d.Reference(), []string{"summary.type"}, &mds); err != nil {
|
|
return types.HostFileSystemVolumeFileSystemType(""), err
|
|
}
|
|
return types.HostFileSystemVolumeFileSystemType(mds.Summary.Type), nil
|
|
}
|