github.com/openshift/installer@v1.4.17/pkg/destroy/openstack/openstack.go (about)

     1  package openstack
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/http"
     8  	"regexp"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/gophercloud/gophercloud/v2"
    13  	"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/snapshots"
    14  	"github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes"
    15  	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups"
    16  	"github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers"
    17  	"github.com/gophercloud/gophercloud/v2/openstack/image/v2/images"
    18  	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions"
    19  	"github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers"
    20  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags"
    21  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips"
    22  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers"
    23  	sg "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups"
    24  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks"
    25  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks"
    26  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports"
    27  	"github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets"
    28  	"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/containers"
    29  	"github.com/gophercloud/gophercloud/v2/openstack/objectstorage/v1/objects"
    30  	"github.com/gophercloud/gophercloud/v2/openstack/sharedfilesystems/v2/shares"
    31  	sharesnapshots "github.com/gophercloud/gophercloud/v2/openstack/sharedfilesystems/v2/snapshots"
    32  	"github.com/gophercloud/gophercloud/v2/pagination"
    33  	"github.com/gophercloud/utils/v2/openstack/clientconfig"
    34  	"github.com/sirupsen/logrus"
    35  	k8serrors "k8s.io/apimachinery/pkg/util/errors"
    36  	"k8s.io/apimachinery/pkg/util/wait"
    37  
    38  	"github.com/openshift/installer/pkg/destroy/providers"
    39  	"github.com/openshift/installer/pkg/types"
    40  	openstackdefaults "github.com/openshift/installer/pkg/types/openstack/defaults"
    41  	"github.com/openshift/installer/pkg/types/openstack/validation/networkextensions"
    42  )
    43  
    44  const (
    45  	cinderCSIClusterIDKey           = "cinder.csi.openstack.org/cluster"
    46  	manilaCSIClusterIDKey           = "manila.csi.openstack.org/cluster"
    47  	minOctaviaVersionWithTagSupport = "v2.5"
    48  	cloudProviderSGNamePattern      = `^lb-sg-[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}`
    49  )
    50  
    51  // Filter holds the key/value pairs for the tags we will be matching
    52  // against.
    53  type Filter map[string]string
    54  
    55  // ObjectWithTags is a generic way to represent an OpenStack object
    56  // and its tags so that filtering objects client-side can be done in a generic
    57  // way.
    58  //
    59  // Note we use UUID not Name as not all OpenStack services require a unique
    60  // name.
    61  type ObjectWithTags struct {
    62  	ID   string
    63  	Tags map[string]string
    64  }
    65  
    66  // deleteFunc type is the interface a function needs to implement to be called as a goroutine.
    67  // The (bool, error) return type mimics wait.ExponentialBackoff where the bool indicates successful
    68  // completion, and the error is for unrecoverable errors.
    69  type deleteFunc func(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error)
    70  
    71  // ClusterUninstaller holds the various options for the cluster we want to delete.
    72  type ClusterUninstaller struct {
    73  	// Cloud is the cloud name as set in clouds.yml
    74  	Cloud string
    75  	// Filter contains the openshiftClusterID to filter tags
    76  	Filter Filter
    77  	// InfraID contains unique cluster identifier
    78  	InfraID string
    79  	Logger  logrus.FieldLogger
    80  }
    81  
    82  // New returns an OpenStack destroyer from ClusterMetadata.
    83  func New(logger logrus.FieldLogger, metadata *types.ClusterMetadata) (providers.Destroyer, error) {
    84  	return &ClusterUninstaller{
    85  		Cloud:   metadata.ClusterPlatformMetadata.OpenStack.Cloud,
    86  		Filter:  metadata.ClusterPlatformMetadata.OpenStack.Identifier,
    87  		InfraID: metadata.InfraID,
    88  		Logger:  logger,
    89  	}, nil
    90  }
    91  
    92  // Run is the entrypoint to start the uninstall process.
    93  func (o *ClusterUninstaller) Run() (*types.ClusterQuota, error) {
    94  	ctx := context.TODO()
    95  	opts := openstackdefaults.DefaultClientOpts(o.Cloud)
    96  
    97  	// Check that the cloud has the minimum requirements for the destroy
    98  	// script to work properly.
    99  	if err := validateCloud(ctx, opts, o.Logger); err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	// deleteFuncs contains the functions that will be launched as
   104  	// goroutines.
   105  	deleteFuncs := map[string]deleteFunc{
   106  		"cleanVIPsPorts":        cleanVIPsPorts,
   107  		"deleteServers":         deleteServers,
   108  		"deleteServerGroups":    deleteServerGroups,
   109  		"deleteTrunks":          deleteTrunks,
   110  		"deleteLoadBalancers":   deleteLoadBalancers,
   111  		"deletePorts":           deletePortsByFilter,
   112  		"deleteSecurityGroups":  deleteSecurityGroups,
   113  		"clearRouterInterfaces": clearRouterInterfaces,
   114  		"deleteSubnets":         deleteSubnets,
   115  		"deleteNetworks":        deleteNetworks,
   116  		"deleteContainers":      deleteContainers,
   117  		"deleteVolumes":         deleteVolumes,
   118  		"deleteShares":          deleteShares,
   119  		"deleteVolumeSnapshots": deleteVolumeSnapshots,
   120  		"deleteFloatingIPs":     deleteFloatingIPs,
   121  		"deleteImages":          deleteImages,
   122  	}
   123  	returnChannel := make(chan string)
   124  
   125  	// launch goroutines
   126  	for name, function := range deleteFuncs {
   127  		go deleteRunner(ctx, name, function, opts, o.Filter, o.Logger, returnChannel)
   128  	}
   129  
   130  	// wait for them to finish
   131  	for i := 0; i < len(deleteFuncs); i++ {
   132  		res := <-returnChannel
   133  		o.Logger.Debugf("goroutine %v complete", res)
   134  	}
   135  
   136  	// we want to remove routers as the last thing as it requires detaching the
   137  	// FIPs and that will cause it impossible to track which FIPs are tied to
   138  	// LBs being deleted.
   139  	err := deleteRouterRunner(ctx, opts, o.Filter, o.Logger)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	// we need to untag the custom network if it was provided by the user
   145  	err = untagRunner(ctx, opts, o.InfraID, o.Logger)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	return nil, nil
   151  }
   152  
   153  func deleteRunner(ctx context.Context, deleteFuncName string, dFunction deleteFunc, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger, channel chan string) {
   154  	backoffSettings := wait.Backoff{
   155  		Duration: time.Second * 15,
   156  		Factor:   1.3,
   157  		Steps:    25,
   158  	}
   159  
   160  	err := wait.ExponentialBackoff(backoffSettings, func() (bool, error) {
   161  		return dFunction(ctx, opts, filter, logger)
   162  	})
   163  
   164  	if err != nil {
   165  		logger.Fatalf("Unrecoverable error/timed out: %v", err)
   166  	}
   167  
   168  	// record that the goroutine has run to completion
   169  	channel <- deleteFuncName
   170  }
   171  
   172  // filterObjects will do client-side filtering given an appropriately filled out
   173  // list of ObjectWithTags.
   174  func filterObjects(osObjects []ObjectWithTags, filters Filter) []ObjectWithTags {
   175  	objectsWithTags := []ObjectWithTags{}
   176  	filteredObjects := []ObjectWithTags{}
   177  
   178  	// first find the objects that have all the desired tags
   179  	for _, object := range osObjects {
   180  		allTagsFound := true
   181  		for key := range filters {
   182  			if _, ok := object.Tags[key]; !ok {
   183  				// doesn't have one of the tags we're looking for so skip it
   184  				allTagsFound = false
   185  				break
   186  			}
   187  		}
   188  		if allTagsFound {
   189  			objectsWithTags = append(objectsWithTags, object)
   190  		}
   191  	}
   192  
   193  	// now check that the values match
   194  	for _, object := range objectsWithTags {
   195  		valuesMatch := true
   196  		for key, val := range filters {
   197  			if object.Tags[key] != val {
   198  				valuesMatch = false
   199  				break
   200  			}
   201  		}
   202  		if valuesMatch {
   203  			filteredObjects = append(filteredObjects, object)
   204  		}
   205  	}
   206  	return filteredObjects
   207  }
   208  
   209  func filterTags(filters Filter) []string {
   210  	tags := []string{}
   211  	for k, v := range filters {
   212  		tags = append(tags, strings.Join([]string{k, v}, "="))
   213  	}
   214  	return tags
   215  }
   216  
   217  func deleteServers(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   218  	logger.Debug("Deleting openstack servers")
   219  	defer logger.Debugf("Exiting deleting openstack servers")
   220  
   221  	conn, err := openstackdefaults.NewServiceClient(ctx, "compute", opts)
   222  	if err != nil {
   223  		logger.Error(err)
   224  		return false, nil
   225  	}
   226  
   227  	allPages, err := servers.List(conn, servers.ListOpts{}).AllPages(ctx)
   228  	if err != nil {
   229  		logger.Error(err)
   230  		return false, nil
   231  	}
   232  
   233  	allServers, err := servers.ExtractServers(allPages)
   234  	if err != nil {
   235  		logger.Error(err)
   236  		return false, nil
   237  	}
   238  
   239  	serverObjects := []ObjectWithTags{}
   240  	for _, server := range allServers {
   241  		serverObjects = append(
   242  			serverObjects, ObjectWithTags{
   243  				ID:   server.ID,
   244  				Tags: server.Metadata})
   245  	}
   246  
   247  	filteredServers := filterObjects(serverObjects, filter)
   248  	numberToDelete := len(filteredServers)
   249  	numberDeleted := 0
   250  	for _, server := range filteredServers {
   251  		logger.Debugf("Deleting Server %q", server.ID)
   252  		err = servers.Delete(ctx, conn, server.ID).ExtractErr()
   253  		if err != nil {
   254  			// Ignore the error if the server cannot be found and return with an appropriate message if it's another type of error
   255  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   256  				// Just log the error and move on to the next server
   257  				logger.Errorf("Deleting server %q failed: %v", server.ID, err)
   258  				continue
   259  			}
   260  			logger.Debugf("Cannot find server %q. It's probably already been deleted.", server.ID)
   261  		}
   262  		numberDeleted++
   263  	}
   264  	return numberDeleted == numberToDelete, nil
   265  }
   266  
   267  func deleteServerGroups(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   268  	logger.Debug("Deleting openstack server groups")
   269  	defer logger.Debugf("Exiting deleting openstack server groups")
   270  
   271  	// We need to delete all server groups that have names with the cluster
   272  	// ID as a prefix
   273  	var clusterID string
   274  	for k, v := range filter {
   275  		if strings.ToLower(k) == "openshiftclusterid" {
   276  			clusterID = v
   277  			break
   278  		}
   279  	}
   280  
   281  	conn, err := openstackdefaults.NewServiceClient(ctx, "compute", opts)
   282  	if err != nil {
   283  		logger.Error(err)
   284  		return false, nil
   285  	}
   286  
   287  	allPages, err := servergroups.List(conn, nil).AllPages(ctx)
   288  	if err != nil {
   289  		logger.Error(err)
   290  		return false, nil
   291  	}
   292  
   293  	allServerGroups, err := servergroups.ExtractServerGroups(allPages)
   294  	if err != nil {
   295  		logger.Error(err)
   296  		return false, nil
   297  	}
   298  
   299  	filteredGroups := make([]servergroups.ServerGroup, 0, len(allServerGroups))
   300  	for _, serverGroup := range allServerGroups {
   301  		if strings.HasPrefix(serverGroup.Name, clusterID) {
   302  			filteredGroups = append(filteredGroups, serverGroup)
   303  		}
   304  	}
   305  
   306  	numberToDelete := len(filteredGroups)
   307  	numberDeleted := 0
   308  	for _, serverGroup := range filteredGroups {
   309  		logger.Debugf("Deleting Server Group %q", serverGroup.ID)
   310  		if err = servergroups.Delete(ctx, conn, serverGroup.ID).ExtractErr(); err != nil {
   311  			// Ignore the error if the server cannot be found and
   312  			// return with an appropriate message if it's another
   313  			// type of error
   314  			if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   315  				// Just log the error and move on to the next server group
   316  				logger.Errorf("Deleting server group %q failed: %v", serverGroup.ID, err)
   317  				continue
   318  			}
   319  			logger.Debugf("Cannot find server group %q. It's probably already been deleted.", serverGroup.ID)
   320  		}
   321  		numberDeleted++
   322  	}
   323  	return numberDeleted == numberToDelete, nil
   324  }
   325  
   326  func deletePortsByNetwork(ctx context.Context, opts *clientconfig.ClientOpts, networkID string, logger logrus.FieldLogger) (bool, error) {
   327  	listOpts := ports.ListOpts{
   328  		NetworkID: networkID,
   329  	}
   330  
   331  	result, err := deletePorts(ctx, opts, listOpts, logger)
   332  	if err != nil {
   333  		logger.Error(err)
   334  		return false, nil
   335  	}
   336  	return result, err
   337  }
   338  
   339  func deletePortsByFilter(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   340  	tags := filterTags(filter)
   341  	listOpts := ports.ListOpts{
   342  		TagsAny: strings.Join(tags, ","),
   343  	}
   344  
   345  	result, err := deletePorts(ctx, opts, listOpts, logger)
   346  	if err != nil {
   347  		logger.Error(err)
   348  		return false, nil
   349  	}
   350  	return result, err
   351  }
   352  
   353  func getFIPsByPort(ctx context.Context, conn *gophercloud.ServiceClient, logger logrus.FieldLogger) (map[string]floatingips.FloatingIP, error) {
   354  	// Prefetch list of FIPs to save list calls for each port
   355  	fipByPort := make(map[string]floatingips.FloatingIP)
   356  	allPages, err := floatingips.List(conn, floatingips.ListOpts{}).AllPages(ctx)
   357  	if err != nil {
   358  		logger.Error(err)
   359  		return fipByPort, nil
   360  	}
   361  	allFIPs, err := floatingips.ExtractFloatingIPs(allPages)
   362  	if err != nil {
   363  		logger.Error(err)
   364  		return fipByPort, nil
   365  	}
   366  
   367  	// Organize FIPs for easy lookup
   368  	for _, fip := range allFIPs {
   369  		fipByPort[fip.PortID] = fip
   370  	}
   371  	return fipByPort, err
   372  }
   373  
   374  // getSGsByID prefetches a list of SGs and organizes it by ID for easy lookup.
   375  func getSGsByID(ctx context.Context, conn *gophercloud.ServiceClient, logger logrus.FieldLogger) (map[string]sg.SecGroup, error) {
   376  	sgByID := make(map[string]sg.SecGroup)
   377  	allPages, err := sg.List(conn, sg.ListOpts{}).AllPages(ctx)
   378  	if err != nil {
   379  		logger.Error(err)
   380  		return sgByID, nil
   381  	}
   382  	allSGs, err := sg.ExtractGroups(allPages)
   383  	if err != nil {
   384  		logger.Error(err)
   385  		return sgByID, nil
   386  	}
   387  
   388  	// Organize SGs for easy lookup
   389  	for _, group := range allSGs {
   390  		sgByID[group.ID] = group
   391  	}
   392  	return sgByID, err
   393  }
   394  
   395  func deletePorts(ctx context.Context, opts *clientconfig.ClientOpts, listOpts ports.ListOpts, logger logrus.FieldLogger) (bool, error) {
   396  	logger.Debug("Deleting openstack ports")
   397  	defer logger.Debugf("Exiting deleting openstack ports")
   398  
   399  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   400  	if err != nil {
   401  		logger.Error(err)
   402  		return false, nil
   403  	}
   404  
   405  	allPages, err := ports.List(conn, listOpts).AllPages(ctx)
   406  	if err != nil {
   407  		logger.Error(err)
   408  		return false, nil
   409  	}
   410  
   411  	allPorts, err := ports.ExtractPorts(allPages)
   412  	if err != nil {
   413  		logger.Error(err)
   414  		return false, nil
   415  	}
   416  	numberToDelete := len(allPorts)
   417  	numberDeleted := 0
   418  
   419  	fipByPort, err := getFIPsByPort(ctx, conn, logger)
   420  	if err != nil {
   421  		logger.Error(err)
   422  		return false, nil
   423  	}
   424  
   425  	sgByID, err := getSGsByID(ctx, conn, logger)
   426  	if err != nil {
   427  		logger.Error(err)
   428  		return false, nil
   429  	}
   430  	cloudProviderSGNameRegexp := regexp.MustCompile(cloudProviderSGNamePattern)
   431  
   432  	deletePortsWorker := func(portsChannel <-chan ports.Port, deletedChannel chan<- int) {
   433  		localDeleted := 0
   434  		for port := range portsChannel {
   435  			// If a user provisioned floating ip was used, it needs to be dissociated.
   436  			// Any floating Ip's associated with ports that are going to be deleted will be dissociated.
   437  			if fip, ok := fipByPort[port.ID]; ok {
   438  				logger.Debugf("Dissociating Floating IP %q", fip.ID)
   439  				_, err := floatingips.Update(ctx, conn, fip.ID, floatingips.UpdateOpts{}).Extract()
   440  				if err != nil {
   441  					// Ignore the error if the floating ip cannot be found and return with an appropriate message if it's another type of error
   442  					if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   443  						// Just log the error and move on to the next port
   444  						logger.Errorf("While deleting port %q, the update of the floating IP %q failed with error: %v", port.ID, fip.ID, err)
   445  						continue
   446  					}
   447  					logger.Debugf("Cannot find floating ip %q. It's probably already been deleted.", fip.ID)
   448  				}
   449  			}
   450  
   451  			// If there is a security group created by cloud-provider-openstack we should find it and delete it.
   452  			// We'll look through the ones on each of the ports and attempt to remove it from the port and delete it.
   453  			// Most of the time it's a conflict, but last port should be guaranteed to allow deletion.
   454  			// TODO(dulek): Currently this is the only way to do it and if delete fails there's no way to get back to
   455  			//              that SG. This is bad and we should make groups created by CPO tagged by cluster ID ASAP.
   456  			assignedSGs := port.SecurityGroups
   457  			ports.Update(ctx, conn, port.ID, ports.UpdateOpts{
   458  				SecurityGroups: &[]string{}, // We can just detach all, we're deleting this port anyway.
   459  			})
   460  			for _, groupID := range assignedSGs {
   461  				if group, ok := sgByID[groupID]; ok {
   462  					if cloudProviderSGNameRegexp.MatchString(group.Name) {
   463  						logger.Debugf("Deleting cloud-provider-openstack SG %q", groupID)
   464  						err := sg.Delete(ctx, conn, groupID).ExtractErr()
   465  						if err == nil || gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   466  							// If SG is gone let's remove it from the map and it'll save us these calls later on.
   467  							delete(sgByID, groupID)
   468  						} else if !gophercloud.ResponseCodeIs(err, http.StatusConflict) { // Ignore 404 Not Found (clause before) and 409 Conflict
   469  							logger.Errorf("Deleting SG %q at port %q failed. SG might get orphaned: %v", groupID, port.ID, err)
   470  						}
   471  					}
   472  				}
   473  			}
   474  
   475  			logger.Debugf("Deleting Port %q", port.ID)
   476  			err = ports.Delete(ctx, conn, port.ID).ExtractErr()
   477  			if err != nil {
   478  				// This can fail when port is still in use so return/retry
   479  				// Just log the error and move on to the next port
   480  				logger.Debugf("Deleting Port %q failed with error: %v", port.ID, err)
   481  				// Try to delete associated trunk
   482  				deleteAssociatedTrunk(ctx, conn, logger, port.ID)
   483  				continue
   484  			}
   485  			localDeleted++
   486  		}
   487  		deletedChannel <- localDeleted
   488  	}
   489  
   490  	const workersNumber = 10
   491  	portsChannel := make(chan ports.Port, workersNumber)
   492  	deletedChannel := make(chan int, workersNumber)
   493  
   494  	// start worker goroutines
   495  	for i := 0; i < workersNumber; i++ {
   496  		go deletePortsWorker(portsChannel, deletedChannel)
   497  	}
   498  
   499  	// feed worker goroutines with ports
   500  	for _, port := range allPorts {
   501  		portsChannel <- port
   502  	}
   503  	close(portsChannel)
   504  
   505  	// wait for them to finish and accumulate number of ports deleted by each
   506  	for i := 0; i < workersNumber; i++ {
   507  		numberDeleted += <-deletedChannel
   508  	}
   509  
   510  	return numberDeleted == numberToDelete, nil
   511  }
   512  
   513  func getSecurityGroups(ctx context.Context, conn *gophercloud.ServiceClient, filter Filter) ([]sg.SecGroup, error) {
   514  	var emptySecurityGroups []sg.SecGroup
   515  	tags := filterTags(filter)
   516  	listOpts := sg.ListOpts{
   517  		TagsAny: strings.Join(tags, ","),
   518  	}
   519  
   520  	allPages, err := sg.List(conn, listOpts).AllPages(ctx)
   521  	if err != nil {
   522  		return emptySecurityGroups, err
   523  	}
   524  
   525  	allGroups, err := sg.ExtractGroups(allPages)
   526  	if err != nil {
   527  		return emptySecurityGroups, err
   528  	}
   529  	return allGroups, nil
   530  }
   531  
   532  func deleteSecurityGroups(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   533  	logger.Debug("Deleting openstack security-groups")
   534  	defer logger.Debugf("Exiting deleting openstack security-groups")
   535  
   536  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   537  	if err != nil {
   538  		logger.Error(err)
   539  		return false, nil
   540  	}
   541  
   542  	allGroups, err := getSecurityGroups(ctx, conn, filter)
   543  	if err != nil {
   544  		logger.Error(err)
   545  		return false, nil
   546  	}
   547  	numberToDelete := len(allGroups)
   548  	numberDeleted := 0
   549  	for _, group := range allGroups {
   550  		logger.Debugf("Deleting Security Group: %q", group.ID)
   551  		err = sg.Delete(ctx, conn, group.ID).ExtractErr()
   552  		if err != nil {
   553  			// Ignore the error if the security group cannot be found
   554  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   555  				// This can fail when sg is still in use by servers
   556  				// Just log the error and move on to the next security group
   557  				logger.Debugf("Deleting Security Group %q failed with error: %v", group.ID, err)
   558  				continue
   559  			}
   560  			logger.Debugf("Cannot find security group %q. It's probably already been deleted.", group.ID)
   561  		}
   562  		numberDeleted++
   563  	}
   564  	return numberDeleted == numberToDelete, nil
   565  }
   566  
   567  func updateFips(ctx context.Context, allFIPs []floatingips.FloatingIP, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) error {
   568  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   569  	if err != nil {
   570  		return err
   571  	}
   572  
   573  	for _, fip := range allFIPs {
   574  		logger.Debugf("Updating FIP %s", fip.ID)
   575  		_, err := floatingips.Update(ctx, conn, fip.ID, floatingips.UpdateOpts{}).Extract()
   576  		if err != nil {
   577  			// Ignore the error if the resource cannot be found and return with an appropriate message if it's another type of error
   578  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   579  				logger.Errorf("Updating floating IP %q for Router failed: %v", fip.ID, err)
   580  				return err
   581  			}
   582  			logger.Debugf("Cannot find floating ip %q. It's probably already been deleted.", fip.ID)
   583  		}
   584  	}
   585  	return nil
   586  }
   587  
   588  // deletePortFIPs looks up FIPs associated to the port and attempts to delete them
   589  func deletePortFIPs(ctx context.Context, portID string, opts *clientconfig.ClientOpts, logger logrus.FieldLogger) error {
   590  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   591  	if err != nil {
   592  		return err
   593  	}
   594  
   595  	fipPages, err := floatingips.List(conn, floatingips.ListOpts{PortID: portID}).AllPages(ctx)
   596  
   597  	if err != nil {
   598  		logger.Error(err)
   599  		return err
   600  	}
   601  
   602  	fips, err := floatingips.ExtractFloatingIPs(fipPages)
   603  	if err != nil {
   604  		logger.Error(err)
   605  		return err
   606  	}
   607  
   608  	for _, fip := range fips {
   609  		logger.Debugf("Deleting FIP %q", fip.ID)
   610  		err = floatingips.Delete(ctx, conn, fip.ID).ExtractErr()
   611  		if err != nil {
   612  			// Ignore the error if the FIP cannot be found
   613  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   614  				logger.Errorf("Deleting FIP %q failed: %v", fip.ID, err)
   615  				return err
   616  			}
   617  			logger.Debugf("Cannot find FIP %q. It's probably already been deleted.", fip.ID)
   618  		}
   619  	}
   620  	return nil
   621  }
   622  
   623  func getRouters(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) ([]routers.Router, error) {
   624  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   625  	if err != nil {
   626  		return nil, err
   627  	}
   628  	tags := filterTags(filter)
   629  	listOpts := routers.ListOpts{
   630  		TagsAny: strings.Join(tags, ","),
   631  	}
   632  
   633  	allPages, err := routers.List(conn, listOpts).AllPages(ctx)
   634  	if err != nil {
   635  		return nil, err
   636  	}
   637  
   638  	allRouters, err := routers.ExtractRouters(allPages)
   639  	if err != nil {
   640  		return nil, err
   641  	}
   642  	return allRouters, nil
   643  }
   644  
   645  func deleteRouters(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   646  	logger.Debug("Deleting openstack routers")
   647  	defer logger.Debugf("Exiting deleting openstack routers")
   648  
   649  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   650  	if err != nil {
   651  		logger.Error(err)
   652  		return false, nil
   653  	}
   654  
   655  	allRouters, err := getRouters(ctx, opts, filter, logger)
   656  	if err != nil {
   657  		logger.Error(err)
   658  		return false, nil
   659  	}
   660  
   661  	numberToDelete := len(allRouters)
   662  	numberDeleted := 0
   663  	for _, router := range allRouters {
   664  		fipOpts := floatingips.ListOpts{
   665  			RouterID: router.ID,
   666  		}
   667  
   668  		fipPages, err := floatingips.List(conn, fipOpts).AllPages(ctx)
   669  		if err != nil {
   670  			logger.Error(err)
   671  			return false, nil
   672  		}
   673  
   674  		allFIPs, err := floatingips.ExtractFloatingIPs(fipPages)
   675  		if err != nil {
   676  			logger.Error(err)
   677  			return false, nil
   678  		}
   679  		// If a user provisioned floating ip was used, it needs to be dissociated
   680  		// Any floating Ip's associated with routers that are going to be deleted will be dissociated
   681  		err = updateFips(ctx, allFIPs, opts, filter, logger)
   682  		if err != nil {
   683  			logger.Error(err)
   684  			continue
   685  		}
   686  		// Clean Gateway interface
   687  		updateOpts := routers.UpdateOpts{
   688  			GatewayInfo: &routers.GatewayInfo{},
   689  		}
   690  
   691  		_, err = routers.Update(ctx, conn, router.ID, updateOpts).Extract()
   692  		if err != nil {
   693  			logger.Error(err)
   694  		}
   695  
   696  		logger.Debugf("Deleting Router %q", router.ID)
   697  		err = routers.Delete(ctx, conn, router.ID).ExtractErr()
   698  		if err != nil {
   699  			// Ignore the error if the router cannot be found and return with an appropriate message if it's another type of error
   700  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   701  				// Just log the error and move on to the next router
   702  				logger.Errorf("Deleting router %q failed: %v", router.ID, err)
   703  				continue
   704  			}
   705  			logger.Debugf("Cannot find router %q. It's probably already been deleted.", router.ID)
   706  		}
   707  		numberDeleted++
   708  	}
   709  	return numberDeleted == numberToDelete, nil
   710  }
   711  
   712  func getRouterInterfaces(ctx context.Context, conn *gophercloud.ServiceClient, allNetworks []networks.Network, logger logrus.FieldLogger) ([]ports.Port, error) {
   713  	var routerPorts []ports.Port
   714  	for _, network := range allNetworks {
   715  		if len(network.Subnets) == 0 {
   716  			continue
   717  		}
   718  		subnet, err := subnets.Get(ctx, conn, network.Subnets[0]).Extract()
   719  		if err != nil {
   720  			logger.Debug(err)
   721  			return routerPorts, nil
   722  		}
   723  		if subnet.GatewayIP == "" {
   724  			continue
   725  		}
   726  		portListOpts := ports.ListOpts{
   727  			FixedIPs: []ports.FixedIPOpts{
   728  				{
   729  					SubnetID: network.Subnets[0],
   730  				},
   731  				{
   732  					IPAddress: subnet.GatewayIP,
   733  				},
   734  			},
   735  		}
   736  
   737  		allPagesPort, err := ports.List(conn, portListOpts).AllPages(ctx)
   738  		if err != nil {
   739  			logger.Error(err)
   740  			return routerPorts, nil
   741  		}
   742  
   743  		routerPorts, err = ports.ExtractPorts(allPagesPort)
   744  		if err != nil {
   745  			logger.Error(err)
   746  			return routerPorts, nil
   747  		}
   748  
   749  		if len(routerPorts) != 0 {
   750  			logger.Debugf("Found Port %q connected to Router", routerPorts[0].ID)
   751  			return routerPorts, nil
   752  		}
   753  	}
   754  	return routerPorts, nil
   755  }
   756  
   757  func clearRouterInterfaces(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   758  	logger.Debugf("Removing interfaces from router")
   759  	defer logger.Debug("Exiting removal of interfaces from router")
   760  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   761  	if err != nil {
   762  		logger.Error(err)
   763  		return false, nil
   764  	}
   765  
   766  	tags := filterTags(filter)
   767  	networkListOpts := networks.ListOpts{
   768  		Tags: strings.Join(tags, ","),
   769  	}
   770  
   771  	allNetworksPages, err := networks.List(conn, networkListOpts).AllPages(ctx)
   772  	if err != nil {
   773  		logger.Debug(err)
   774  		return false, nil
   775  	}
   776  
   777  	allNetworks, err := networks.ExtractNetworks(allNetworksPages)
   778  	if err != nil {
   779  		logger.Debug(err)
   780  		return false, nil
   781  	}
   782  
   783  	// Identify router by checking any tagged Network that has a Subnet
   784  	// with GatewayIP set
   785  	routerPorts, err := getRouterInterfaces(ctx, conn, allNetworks, logger)
   786  	if err != nil {
   787  		logger.Debug(err)
   788  		return false, nil
   789  	}
   790  
   791  	if len(routerPorts) == 0 {
   792  		return true, nil
   793  	}
   794  
   795  	routerID := routerPorts[0].DeviceID
   796  	router, err := routers.Get(ctx, conn, routerID).Extract()
   797  	if err != nil {
   798  		logger.Error(err)
   799  		return false, nil
   800  	}
   801  
   802  	removed, err := removeRouterInterfaces(ctx, conn, filter, *router, logger)
   803  	if err != nil {
   804  		logger.Debug(err)
   805  		return false, nil
   806  	}
   807  	return removed, nil
   808  }
   809  
   810  func removeRouterInterfaces(ctx context.Context, client *gophercloud.ServiceClient, filter Filter, router routers.Router, logger logrus.FieldLogger) (bool, error) {
   811  	// Get router interface ports
   812  	portListOpts := ports.ListOpts{
   813  		DeviceID: router.ID,
   814  	}
   815  	allPagesPort, err := ports.List(client, portListOpts).AllPages(ctx)
   816  	if err != nil {
   817  		logger.Error(err)
   818  		return false, fmt.Errorf("failed to get ports list: %w", err)
   819  	}
   820  	allPorts, err := ports.ExtractPorts(allPagesPort)
   821  	if err != nil {
   822  		logger.Error(err)
   823  		return false, fmt.Errorf("failed to extract ports list: %w", err)
   824  	}
   825  	tags := filterTags(filter)
   826  	SubnetlistOpts := subnets.ListOpts{
   827  		TagsAny: strings.Join(tags, ","),
   828  	}
   829  
   830  	allSubnetsPage, err := subnets.List(client, SubnetlistOpts).AllPages(ctx)
   831  	if err != nil {
   832  		logger.Debug(err)
   833  		return false, fmt.Errorf("failed to list subnets list: %w", err)
   834  	}
   835  
   836  	allSubnets, err := subnets.ExtractSubnets(allSubnetsPage)
   837  	if err != nil {
   838  		logger.Debug(err)
   839  		return false, fmt.Errorf("failed to extract subnets list: %w", err)
   840  	}
   841  
   842  	clusterTag := "openshiftClusterID=" + filter["openshiftClusterID"]
   843  	clusterRouter := isClusterRouter(clusterTag, router.Tags)
   844  
   845  	numberToDelete := len(allPorts)
   846  	numberDeleted := 0
   847  	var customInterfaces []ports.Port
   848  	// map to keep track of whether interface for subnet was already removed
   849  	removedSubnets := make(map[string]bool)
   850  	for _, port := range allPorts {
   851  		for _, IP := range port.FixedIPs {
   852  			// Skip removal if Router was not created by CNO or installer and
   853  			// interface is not handled by the Cluster
   854  			if !clusterRouter && !isClusterSubnet(allSubnets, IP.SubnetID) {
   855  				logger.Debugf("Found custom interface %q on Router %q", port.ID, router.ID)
   856  				customInterfaces = append(customInterfaces, port)
   857  				continue
   858  			}
   859  			if !removedSubnets[IP.SubnetID] {
   860  				removeOpts := routers.RemoveInterfaceOpts{
   861  					SubnetID: IP.SubnetID,
   862  				}
   863  				logger.Debugf("Removing Subnet %q from Router %q", IP.SubnetID, router.ID)
   864  				_, err := routers.RemoveInterface(ctx, client, router.ID, removeOpts).Extract()
   865  				if err != nil {
   866  					if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   867  						// This can fail when subnet is still in use
   868  						logger.Debugf("Removing Subnet %q from Router %q failed: %v", IP.SubnetID, router.ID, err)
   869  						return false, nil
   870  					}
   871  					logger.Debugf("Cannot find subnet %q. It's probably already been removed from router %q.", IP.SubnetID, router.ID)
   872  				}
   873  				removedSubnets[IP.SubnetID] = true
   874  				numberDeleted++
   875  			}
   876  		}
   877  	}
   878  	numberToDelete -= len(customInterfaces)
   879  	return numberToDelete == numberDeleted, nil
   880  }
   881  
   882  func isClusterRouter(clusterTag string, tags []string) bool {
   883  	for _, tag := range tags {
   884  		if clusterTag == tag {
   885  			return true
   886  		}
   887  	}
   888  	return false
   889  }
   890  
   891  func deleteLeftoverLoadBalancers(ctx context.Context, opts *clientconfig.ClientOpts, logger logrus.FieldLogger, networkID string) error {
   892  	conn, err := openstackdefaults.NewServiceClient(ctx, "load-balancer", opts)
   893  	if err != nil {
   894  		// Ignore the error if Octavia is not available for the cloud
   895  		var gerr *gophercloud.ErrEndpointNotFound
   896  		if errors.As(err, &gerr) {
   897  			logger.Debug("Skip load balancer deletion because Octavia endpoint is not found")
   898  			return nil
   899  		}
   900  		logger.Error(err)
   901  		return err
   902  	}
   903  
   904  	listOpts := loadbalancers.ListOpts{
   905  		VipNetworkID: networkID,
   906  	}
   907  	allPages, err := loadbalancers.List(conn, listOpts).AllPages(ctx)
   908  	if err != nil {
   909  		logger.Error(err)
   910  		return err
   911  	}
   912  
   913  	allLoadBalancers, err := loadbalancers.ExtractLoadBalancers(allPages)
   914  	if err != nil {
   915  		logger.Error(err)
   916  		return err
   917  	}
   918  	deleteOpts := loadbalancers.DeleteOpts{
   919  		Cascade: true,
   920  	}
   921  	deleted := 0
   922  	for _, loadbalancer := range allLoadBalancers {
   923  		if !strings.HasPrefix(loadbalancer.Description, "Kubernetes external service") {
   924  			logger.Debugf("Not deleting LoadBalancer %q with description %q", loadbalancer.ID, loadbalancer.Description)
   925  			continue
   926  		}
   927  		logger.Debugf("Deleting LoadBalancer %q", loadbalancer.ID)
   928  
   929  		// Cascade delete of an LB won't remove the associated FIP, we have to do it ourselves.
   930  		err := deletePortFIPs(ctx, loadbalancer.VipPortID, opts, logger)
   931  		if err != nil {
   932  			// Go to the next LB, but do not delete current one or we'll lose reference to the FIP that failed deletion.
   933  			continue
   934  		}
   935  
   936  		err = loadbalancers.Delete(ctx, conn, loadbalancer.ID, deleteOpts).ExtractErr()
   937  		if err != nil {
   938  			// Ignore the error if the load balancer cannot be found
   939  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   940  				// This can fail when the load balancer is still in use so return/retry
   941  				// Just log the error and move on to the next LB
   942  				logger.Debugf("Deleting load balancer %q failed: %v", loadbalancer.ID, err)
   943  				continue
   944  			}
   945  			logger.Debugf("Cannot find load balancer %q. It's probably already been deleted.", loadbalancer.ID)
   946  		}
   947  		deleted++
   948  	}
   949  
   950  	if deleted != len(allLoadBalancers) {
   951  		return fmt.Errorf("only deleted %d of %d load balancers", deleted, len(allLoadBalancers))
   952  	}
   953  	return nil
   954  }
   955  
   956  func isClusterSubnet(subnets []subnets.Subnet, subnetID string) bool {
   957  	for _, subnet := range subnets {
   958  		if subnet.ID == subnetID {
   959  			return true
   960  		}
   961  	}
   962  	return false
   963  }
   964  
   965  func deleteSubnets(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
   966  	logger.Debug("Deleting openstack subnets")
   967  	defer logger.Debugf("Exiting deleting openstack subnets")
   968  
   969  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
   970  	if err != nil {
   971  		logger.Error(err)
   972  		return false, nil
   973  	}
   974  	tags := filterTags(filter)
   975  	listOpts := subnets.ListOpts{
   976  		TagsAny: strings.Join(tags, ","),
   977  	}
   978  
   979  	allPages, err := subnets.List(conn, listOpts).AllPages(ctx)
   980  	if err != nil {
   981  		logger.Error(err)
   982  		return false, nil
   983  	}
   984  
   985  	allSubnets, err := subnets.ExtractSubnets(allPages)
   986  	if err != nil {
   987  		logger.Error(err)
   988  		return false, nil
   989  	}
   990  
   991  	numberToDelete := len(allSubnets)
   992  	numberDeleted := 0
   993  	for _, subnet := range allSubnets {
   994  		logger.Debugf("Deleting Subnet: %q", subnet.ID)
   995  		err = subnets.Delete(ctx, conn, subnet.ID).ExtractErr()
   996  		if err != nil {
   997  			// Ignore the error if the subnet cannot be found
   998  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
   999  				// This can fail when subnet is still in use
  1000  				// Just log the error and move on to the next subnet
  1001  				logger.Debugf("Deleting Subnet %q failed: %v", subnet.ID, err)
  1002  				continue
  1003  			}
  1004  			logger.Debugf("Cannot find subnet %q. It's probably already been deleted.", subnet.ID)
  1005  		}
  1006  		numberDeleted++
  1007  	}
  1008  	return numberDeleted == numberToDelete, nil
  1009  }
  1010  
  1011  func deleteNetworks(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1012  	logger.Debug("Deleting openstack networks")
  1013  	defer logger.Debugf("Exiting deleting openstack networks")
  1014  
  1015  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1016  	if err != nil {
  1017  		logger.Error(err)
  1018  		return false, nil
  1019  	}
  1020  	tags := filterTags(filter)
  1021  	listOpts := networks.ListOpts{
  1022  		TagsAny: strings.Join(tags, ","),
  1023  	}
  1024  
  1025  	allPages, err := networks.List(conn, listOpts).AllPages(ctx)
  1026  	if err != nil {
  1027  		logger.Error(err)
  1028  		return false, nil
  1029  	}
  1030  
  1031  	allNetworks, err := networks.ExtractNetworks(allPages)
  1032  	if err != nil {
  1033  		logger.Error(err)
  1034  		return false, nil
  1035  	}
  1036  	numberToDelete := len(allNetworks)
  1037  	numberDeleted := 0
  1038  	for _, network := range allNetworks {
  1039  		logger.Debugf("Deleting network: %q", network.ID)
  1040  		err = networks.Delete(ctx, conn, network.ID).ExtractErr()
  1041  		if err != nil {
  1042  			// Ignore the error if the network cannot be found
  1043  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1044  				// This can fail when network is still in use. Let's log an error and try to fix this.
  1045  				logger.Debugf("Deleting Network %q failed: %v", network.ID, err)
  1046  
  1047  				// First try to delete eventual leftover load balancers
  1048  				// *This has to be done before attempt to remove ports or we'll delete LB ports!*
  1049  				err := deleteLeftoverLoadBalancers(ctx, opts, logger, network.ID)
  1050  				if err != nil {
  1051  					logger.Error(err)
  1052  					// Do not attempt to delete ports on LB removal problem or we'll lose FIP associations!
  1053  					continue
  1054  				}
  1055  
  1056  				// Only then try to remove all the ports it may still contain (untagged as well).
  1057  				// *We cannot delete ports before LBs because we'll lose FIP associations!*
  1058  				_, err = deletePortsByNetwork(ctx, opts, network.ID, logger)
  1059  				if err != nil {
  1060  					logger.Error(err)
  1061  				}
  1062  				continue
  1063  			}
  1064  			logger.Debugf("Cannot find network %q. It's probably already been deleted.", network.ID)
  1065  		}
  1066  		numberDeleted++
  1067  	}
  1068  	return numberDeleted == numberToDelete, nil
  1069  }
  1070  
  1071  func deleteContainers(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1072  	logger.Debug("Deleting openstack containers")
  1073  	defer logger.Debugf("Exiting deleting openstack containers")
  1074  
  1075  	conn, err := openstackdefaults.NewServiceClient(ctx, "object-store", opts)
  1076  	if err != nil {
  1077  		// Ignore the error if Swift is not available for the cloud
  1078  		var gerr *gophercloud.ErrEndpointNotFound
  1079  		if errors.As(err, &gerr) {
  1080  			logger.Debug("Skip container deletion because Swift endpoint is not found")
  1081  			return true, nil
  1082  		}
  1083  		logger.Error(err)
  1084  		return false, nil
  1085  	}
  1086  
  1087  	allPages, err := containers.List(conn, nil).AllPages(ctx)
  1088  	if err != nil {
  1089  		// Ignore the error if the user doesn't have the swiftoperator role.
  1090  		// Depending on the configuration Swift returns different error codes:
  1091  		// 403 with Keystone and 401 with internal Swauth.
  1092  		// It means we have to catch them both.
  1093  		// More information about Swith auth: https://docs.openstack.org/swift/latest/overview_auth.html
  1094  		if gophercloud.ResponseCodeIs(err, http.StatusForbidden) {
  1095  			logger.Debug("Skip container deletion because the user doesn't have the `swiftoperator` role")
  1096  			return true, nil
  1097  		}
  1098  		if gophercloud.ResponseCodeIs(err, http.StatusUnauthorized) {
  1099  			logger.Debug("Skip container deletion because the user doesn't have the `swiftoperator` role")
  1100  			return true, nil
  1101  		}
  1102  		logger.Error(err)
  1103  		return false, nil
  1104  	}
  1105  
  1106  	allContainers, err := containers.ExtractNames(allPages)
  1107  	if err != nil {
  1108  		logger.Error(err)
  1109  		return false, nil
  1110  	}
  1111  	for _, container := range allContainers {
  1112  		metadata, err := containers.Get(ctx, conn, container, nil).ExtractMetadata()
  1113  		if err != nil {
  1114  			// Some containers that we fetched previously can already be deleted in
  1115  			// runtime. We should ignore these cases and continue to iterate through
  1116  			// the remaining containers.
  1117  			if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1118  				continue
  1119  			}
  1120  			logger.Error(err)
  1121  			return false, nil
  1122  		}
  1123  		for key, val := range filter {
  1124  			// Swift mangles the case so openshiftClusterID becomes
  1125  			// Openshiftclusterid in the X-Container-Meta- HEAD output
  1126  			titlekey := strings.Title(strings.ToLower(key))
  1127  			if metadata[titlekey] == val {
  1128  				queue := newSemaphore(3)
  1129  				errCh := make(chan error)
  1130  				err := objects.List(conn, container, nil).EachPage(ctx, func(ctx context.Context, page pagination.Page) (bool, error) {
  1131  					objectsOnPage, err := objects.ExtractNames(page)
  1132  					if err != nil {
  1133  						return false, err
  1134  					}
  1135  					queue.Add(func() {
  1136  						for len(objectsOnPage) > 0 {
  1137  							logger.Debugf("Initiating bulk deletion of %d objects in container %q", len(objectsOnPage), container)
  1138  							resp, err := objects.BulkDelete(ctx, conn, container, objectsOnPage).Extract()
  1139  							if err != nil {
  1140  								errCh <- err
  1141  								return
  1142  							}
  1143  							if len(resp.Errors) > 0 {
  1144  								// Convert resp.Errors to golang errors.
  1145  								// Each error is represented by a list of 2 strings, where the first one
  1146  								// is the object name, and the second one contains an error message.
  1147  								for _, objectError := range resp.Errors {
  1148  									errCh <- fmt.Errorf("cannot delete object %q: %s", objectError[0], objectError[1])
  1149  								}
  1150  								logger.Debugf("Terminating object deletion routine with error. Deleted %d objects out of %d.", resp.NumberDeleted, len(objectsOnPage))
  1151  							}
  1152  
  1153  							// Some object-storage instances may be set to have a limit to the LIST operation
  1154  							// that is higher to the limit to the BULK DELETE operation. On those clouds, objects
  1155  							// in the BULK DELETE call beyond the limit are silently ignored. In this loop, after
  1156  							// checking that no errors were encountered, we reduce the BULK DELETE list by the
  1157  							// number of processed objects, and send it back to the server if it's not empty.
  1158  							objectsOnPage = objectsOnPage[resp.NumberDeleted+resp.NumberNotFound:]
  1159  						}
  1160  						logger.Debugf("Terminating object deletion routine.")
  1161  					})
  1162  					return true, nil
  1163  				})
  1164  				if err != nil {
  1165  					if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1166  						logger.Errorf("Bulk deletion of container %q objects failed: %v", container, err)
  1167  						return false, nil
  1168  					}
  1169  				}
  1170  				var errs []error
  1171  				go func() {
  1172  					for err := range errCh {
  1173  						errs = append(errs, err)
  1174  					}
  1175  				}()
  1176  
  1177  				queue.Wait()
  1178  				close(errCh)
  1179  				if len(errs) > 0 {
  1180  					return false, fmt.Errorf("errors occurred during bulk deletion of the objects of container %q: %w", container, k8serrors.NewAggregate(errs))
  1181  				}
  1182  				logger.Debugf("Deleting container %q", container)
  1183  				_, err = containers.Delete(ctx, conn, container).Extract()
  1184  				if err != nil {
  1185  					// Ignore the error if the container cannot be found and return with an appropriate message if it's another type of error
  1186  					if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1187  						logger.Errorf("Deleting container %q failed: %v", container, err)
  1188  						return false, nil
  1189  					}
  1190  					logger.Debugf("Cannot find container %q. It's probably already been deleted.", container)
  1191  				}
  1192  				// If a metadata key matched, we're done so break from the loop
  1193  				break
  1194  			}
  1195  		}
  1196  	}
  1197  	return true, nil
  1198  }
  1199  
  1200  func deleteTrunks(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1201  	logger.Debug("Deleting openstack trunks")
  1202  	defer logger.Debugf("Exiting deleting openstack trunks")
  1203  
  1204  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1205  	if err != nil {
  1206  		logger.Error(err)
  1207  		return false, nil
  1208  	}
  1209  
  1210  	tags := filterTags(filter)
  1211  	listOpts := trunks.ListOpts{
  1212  		TagsAny: strings.Join(tags, ","),
  1213  	}
  1214  	allPages, err := trunks.List(conn, listOpts).AllPages(ctx)
  1215  	if err != nil {
  1216  		if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1217  			logger.Debug("Skip trunk deletion because the cloud doesn't support trunk ports")
  1218  			return true, nil
  1219  		}
  1220  		logger.Error(err)
  1221  		return false, nil
  1222  	}
  1223  
  1224  	allTrunks, err := trunks.ExtractTrunks(allPages)
  1225  	if err != nil {
  1226  		logger.Error(err)
  1227  		return false, nil
  1228  	}
  1229  	numberToDelete := len(allTrunks)
  1230  	numberDeleted := 0
  1231  	for _, trunk := range allTrunks {
  1232  		logger.Debugf("Deleting Trunk %q", trunk.ID)
  1233  		err = trunks.Delete(ctx, conn, trunk.ID).ExtractErr()
  1234  		if err != nil {
  1235  			// Ignore the error if the trunk cannot be found
  1236  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1237  				// This can fail when the trunk is still in use so return/retry
  1238  				// Just log the error and move on to the next trunk
  1239  				logger.Debugf("Deleting Trunk %q failed: %v", trunk.ID, err)
  1240  				continue
  1241  			}
  1242  			logger.Debugf("Cannot find trunk %q. It's probably already been deleted.", trunk.ID)
  1243  		}
  1244  		numberDeleted++
  1245  	}
  1246  	return numberDeleted == numberToDelete, nil
  1247  }
  1248  
  1249  func deleteAssociatedTrunk(ctx context.Context, conn *gophercloud.ServiceClient, logger logrus.FieldLogger, portID string) {
  1250  	logger.Debug("Deleting associated trunk")
  1251  	defer logger.Debugf("Exiting deleting associated trunk")
  1252  
  1253  	listOpts := trunks.ListOpts{
  1254  		PortID: portID,
  1255  	}
  1256  	allPages, err := trunks.List(conn, listOpts).AllPages(ctx)
  1257  	if err != nil {
  1258  		if gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1259  			logger.Debug("Skip trunk deletion because the cloud doesn't support trunk ports")
  1260  			return
  1261  		}
  1262  		logger.Error(err)
  1263  		return
  1264  	}
  1265  
  1266  	allTrunks, err := trunks.ExtractTrunks(allPages)
  1267  	if err != nil {
  1268  		logger.Error(err)
  1269  		return
  1270  	}
  1271  	for _, trunk := range allTrunks {
  1272  		logger.Debugf("Deleting Trunk %q", trunk.ID)
  1273  		err = trunks.Delete(ctx, conn, trunk.ID).ExtractErr()
  1274  		if err != nil {
  1275  			// Ignore the error if the trunk cannot be found
  1276  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1277  				// This can fail when the trunk is still in use so return/retry
  1278  				// Just log the error and move on to the next trunk
  1279  				logger.Debugf("Deleting Trunk %q failed: %v", trunk.ID, err)
  1280  				continue
  1281  			}
  1282  			logger.Debugf("Cannot find trunk %q. It's probably already been deleted.", trunk.ID)
  1283  		}
  1284  	}
  1285  	return
  1286  }
  1287  
  1288  func deleteLoadBalancers(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1289  	logger.Debug("Deleting openstack load balancers")
  1290  	defer logger.Debugf("Exiting deleting openstack load balancers")
  1291  
  1292  	conn, err := openstackdefaults.NewServiceClient(ctx, "load-balancer", opts)
  1293  	if err != nil {
  1294  		// Ignore the error if Octavia is not available for the cloud
  1295  		var gerr *gophercloud.ErrEndpointNotFound
  1296  		if errors.As(err, &gerr) {
  1297  			logger.Debug("Skip load balancer deletion because Octavia endpoint is not found")
  1298  			return true, nil
  1299  		}
  1300  		logger.Error(err)
  1301  		return false, nil
  1302  	}
  1303  
  1304  	newallPages, err := apiversions.List(conn).AllPages(ctx)
  1305  	if err != nil {
  1306  		logger.Errorf("Unable to list api versions: %v", err)
  1307  		return false, nil
  1308  	}
  1309  
  1310  	allAPIVersions, err := apiversions.ExtractAPIVersions(newallPages)
  1311  	if err != nil {
  1312  		logger.Errorf("Unable to extract api versions: %v", err)
  1313  		return false, nil
  1314  	}
  1315  
  1316  	var octaviaTagSupport bool
  1317  	octaviaTagSupport = false
  1318  	for _, apiVersion := range allAPIVersions {
  1319  		if apiVersion.ID >= minOctaviaVersionWithTagSupport {
  1320  			octaviaTagSupport = true
  1321  		}
  1322  	}
  1323  
  1324  	tags := filterTags(filter)
  1325  	var allLoadBalancers []loadbalancers.LoadBalancer
  1326  	if octaviaTagSupport {
  1327  		listOpts := loadbalancers.ListOpts{
  1328  			TagsAny: tags,
  1329  		}
  1330  		allPages, err := loadbalancers.List(conn, listOpts).AllPages(ctx)
  1331  		if err != nil {
  1332  			logger.Error(err)
  1333  			return false, nil
  1334  		}
  1335  
  1336  		allLoadBalancers, err = loadbalancers.ExtractLoadBalancers(allPages)
  1337  		if err != nil {
  1338  			logger.Error(err)
  1339  			return false, nil
  1340  		}
  1341  	}
  1342  
  1343  	listOpts := loadbalancers.ListOpts{
  1344  		Description: strings.Join(tags, ","),
  1345  	}
  1346  
  1347  	allPages, err := loadbalancers.List(conn, listOpts).AllPages(ctx)
  1348  	if err != nil {
  1349  		logger.Error(err)
  1350  		return false, nil
  1351  	}
  1352  
  1353  	allLoadBalancersWithTaggedDescription, err := loadbalancers.ExtractLoadBalancers(allPages)
  1354  	if err != nil {
  1355  		logger.Error(err)
  1356  		return false, nil
  1357  	}
  1358  
  1359  	allLoadBalancers = append(allLoadBalancers, allLoadBalancersWithTaggedDescription...)
  1360  	deleteOpts := loadbalancers.DeleteOpts{
  1361  		Cascade: true,
  1362  	}
  1363  	numberToDelete := len(allLoadBalancers)
  1364  	numberDeleted := 0
  1365  	for _, loadbalancer := range allLoadBalancers {
  1366  		logger.Debugf("Deleting LoadBalancer %q", loadbalancer.ID)
  1367  		err = loadbalancers.Delete(ctx, conn, loadbalancer.ID, deleteOpts).ExtractErr()
  1368  		if err != nil {
  1369  			// Ignore the error if the load balancer cannot be found
  1370  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1371  				// This can fail when the load balancer is still in use so return/retry
  1372  				// Just log the error and move on to the next port
  1373  				logger.Debugf("Deleting load balancer %q failed: %v", loadbalancer.ID, err)
  1374  				continue
  1375  			}
  1376  			logger.Debugf("Cannot find load balancer %q. It's probably already been deleted.", loadbalancer.ID)
  1377  		}
  1378  		numberDeleted++
  1379  	}
  1380  
  1381  	return numberDeleted == numberToDelete, nil
  1382  }
  1383  
  1384  func deleteVolumes(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1385  	logger.Debug("Deleting OpenStack volumes")
  1386  	defer logger.Debugf("Exiting deleting OpenStack volumes")
  1387  
  1388  	var clusterID string
  1389  	for k, v := range filter {
  1390  		if strings.ToLower(k) == "openshiftclusterid" {
  1391  			clusterID = v
  1392  			break
  1393  		}
  1394  	}
  1395  
  1396  	conn, err := openstackdefaults.NewServiceClient(ctx, "volume", opts)
  1397  	if err != nil {
  1398  		logger.Error(err)
  1399  		return false, nil
  1400  	}
  1401  
  1402  	listOpts := volumes.ListOpts{}
  1403  
  1404  	allPages, err := volumes.List(conn, listOpts).AllPages(ctx)
  1405  	if err != nil {
  1406  		logger.Error(err)
  1407  		return false, nil
  1408  	}
  1409  
  1410  	allVolumes, err := volumes.ExtractVolumes(allPages)
  1411  	if err != nil {
  1412  		logger.Error(err)
  1413  		return false, nil
  1414  	}
  1415  
  1416  	volumeIDs := []string{}
  1417  	for _, volume := range allVolumes {
  1418  		// First, we need to delete all volumes that have names with the cluster ID as a prefix.
  1419  		// They are created by the in-tree Cinder provisioner.
  1420  		if strings.HasPrefix(volume.Name, clusterID) {
  1421  			volumeIDs = append(volumeIDs, volume.ID)
  1422  		}
  1423  		// Second, we need to delete volumes created by the CSI driver. They contain their cluster ID
  1424  		// in the metadata.
  1425  		if val, ok := volume.Metadata[cinderCSIClusterIDKey]; ok && val == clusterID {
  1426  			volumeIDs = append(volumeIDs, volume.ID)
  1427  		}
  1428  	}
  1429  
  1430  	deleteOpts := volumes.DeleteOpts{
  1431  		Cascade: false,
  1432  	}
  1433  
  1434  	numberToDelete := len(volumeIDs)
  1435  	numberDeleted := 0
  1436  	for _, volumeID := range volumeIDs {
  1437  		logger.Debugf("Deleting volume %q", volumeID)
  1438  		err = volumes.Delete(ctx, conn, volumeID, deleteOpts).ExtractErr()
  1439  		if err != nil {
  1440  			// Ignore the error if the volume cannot be found
  1441  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1442  				// Just log the error and move on to the next volume
  1443  				logger.Debugf("Deleting volume %q failed: %v", volumeID, err)
  1444  				continue
  1445  			}
  1446  			logger.Debugf("Cannot find volume %q. It's probably already been deleted.", volumeID)
  1447  		}
  1448  		numberDeleted++
  1449  	}
  1450  
  1451  	return numberDeleted == numberToDelete, nil
  1452  }
  1453  
  1454  func deleteVolumeSnapshots(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1455  	logger.Debug("Deleting OpenStack volume snapshots")
  1456  	defer logger.Debugf("Exiting deleting OpenStack volume snapshots")
  1457  
  1458  	var clusterID string
  1459  	for k, v := range filter {
  1460  		if strings.ToLower(k) == "openshiftclusterid" {
  1461  			clusterID = v
  1462  			break
  1463  		}
  1464  	}
  1465  
  1466  	conn, err := openstackdefaults.NewServiceClient(ctx, "volume", opts)
  1467  	if err != nil {
  1468  		logger.Error(err)
  1469  		return false, nil
  1470  	}
  1471  
  1472  	listOpts := snapshots.ListOpts{}
  1473  
  1474  	allPages, err := snapshots.List(conn, listOpts).AllPages(ctx)
  1475  	if err != nil {
  1476  		logger.Error(err)
  1477  		return false, nil
  1478  	}
  1479  
  1480  	allSnapshots, err := snapshots.ExtractSnapshots(allPages)
  1481  	if err != nil {
  1482  		logger.Error(err)
  1483  		return false, nil
  1484  	}
  1485  
  1486  	numberToDelete := len(allSnapshots)
  1487  	numberDeleted := 0
  1488  	for _, snapshot := range allSnapshots {
  1489  		// Delete only those snapshots that contain cluster ID in the metadata
  1490  		if val, ok := snapshot.Metadata[cinderCSIClusterIDKey]; ok && val == clusterID {
  1491  			logger.Debugf("Deleting volume snapshot %q", snapshot.ID)
  1492  			err = snapshots.Delete(ctx, conn, snapshot.ID).ExtractErr()
  1493  			if err != nil {
  1494  				// Ignore the error if the server cannot be found
  1495  				if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1496  					// Just log the error and move on to the next volume snapshot
  1497  					logger.Debugf("Deleting volume snapshot %q failed: %v", snapshot.ID, err)
  1498  					continue
  1499  				}
  1500  				logger.Debugf("Cannot find volume snapshot %q. It's probably already been deleted.", snapshot.ID)
  1501  			}
  1502  		}
  1503  		numberDeleted++
  1504  	}
  1505  
  1506  	return numberDeleted == numberToDelete, nil
  1507  }
  1508  
  1509  func deleteShares(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1510  	logger.Debug("Deleting OpenStack shares")
  1511  	defer logger.Debugf("Exiting deleting OpenStack shares")
  1512  
  1513  	var clusterID string
  1514  	for k, v := range filter {
  1515  		if strings.ToLower(k) == "openshiftclusterid" {
  1516  			clusterID = v
  1517  			break
  1518  		}
  1519  	}
  1520  
  1521  	conn, err := openstackdefaults.NewServiceClient(ctx, "sharev2", opts)
  1522  	if err != nil {
  1523  		// Ignore the error if Manila is not available in the cloud
  1524  		var gerr *gophercloud.ErrEndpointNotFound
  1525  		if errors.As(err, &gerr) {
  1526  			logger.Debug("Skip share deletion because Manila endpoint is not found")
  1527  			return true, nil
  1528  		}
  1529  		logger.Error(err)
  1530  		return false, nil
  1531  	}
  1532  
  1533  	listOpts := shares.ListOpts{
  1534  		Metadata: map[string]string{manilaCSIClusterIDKey: clusterID},
  1535  	}
  1536  
  1537  	allPages, err := shares.ListDetail(conn, listOpts).AllPages(ctx)
  1538  	if err != nil {
  1539  		logger.Error(err)
  1540  		return false, nil
  1541  	}
  1542  
  1543  	allShares, err := shares.ExtractShares(allPages)
  1544  	if err != nil {
  1545  		logger.Error(err)
  1546  		return false, nil
  1547  	}
  1548  
  1549  	numberToDelete := len(allShares)
  1550  	numberDeleted := 0
  1551  	for _, share := range allShares {
  1552  		deleted, err := deleteShareSnapshots(ctx, conn, share.ID, logger)
  1553  		if err != nil {
  1554  			return false, err
  1555  		}
  1556  		if !deleted {
  1557  			return false, nil
  1558  		}
  1559  
  1560  		logger.Debugf("Deleting share %q", share.ID)
  1561  		err = shares.Delete(ctx, conn, share.ID).ExtractErr()
  1562  		if err != nil {
  1563  			// Ignore the error if the share cannot be found
  1564  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1565  				// Just log the error and move on to the next share
  1566  				logger.Debugf("Deleting share %q failed: %v", share.ID, err)
  1567  				continue
  1568  			}
  1569  			logger.Debugf("Cannot find share %q. It's probably already been deleted.", share.ID)
  1570  		}
  1571  		numberDeleted++
  1572  	}
  1573  
  1574  	return numberDeleted == numberToDelete, nil
  1575  }
  1576  
  1577  func deleteShareSnapshots(ctx context.Context, conn *gophercloud.ServiceClient, shareID string, logger logrus.FieldLogger) (bool, error) {
  1578  	logger.Debugf("Deleting OpenStack snapshots for share %v", shareID)
  1579  	defer logger.Debugf("Exiting deleting OpenStack snapshots for share %v", shareID)
  1580  
  1581  	listOpts := sharesnapshots.ListOpts{
  1582  		ShareID: shareID,
  1583  	}
  1584  
  1585  	allPages, err := sharesnapshots.ListDetail(conn, listOpts).AllPages(ctx)
  1586  	if err != nil {
  1587  		logger.Error(err)
  1588  		return false, nil
  1589  	}
  1590  
  1591  	allSnapshots, err := sharesnapshots.ExtractSnapshots(allPages)
  1592  	if err != nil {
  1593  		logger.Error(err)
  1594  		return false, nil
  1595  	}
  1596  
  1597  	numberToDelete := len(allSnapshots)
  1598  	numberDeleted := 0
  1599  	for _, snapshot := range allSnapshots {
  1600  		logger.Debugf("Deleting share snapshot %q", snapshot.ID)
  1601  		err = sharesnapshots.Delete(ctx, conn, snapshot.ID).ExtractErr()
  1602  		if err != nil {
  1603  			// Ignore the error if the share snapshot cannot be found
  1604  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1605  				// Just log the error and move on to the next share snapshot
  1606  				logger.Debugf("Deleting share snapshot %q failed: %v", snapshot.ID, err)
  1607  				continue
  1608  			}
  1609  			logger.Debugf("Cannot find share snapshot %q. It's probably already been deleted.", snapshot.ID)
  1610  		}
  1611  		numberDeleted++
  1612  	}
  1613  
  1614  	return numberDeleted == numberToDelete, nil
  1615  }
  1616  
  1617  func deleteFloatingIPs(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1618  	logger.Debug("Deleting openstack floating ips")
  1619  	defer logger.Debugf("Exiting deleting openstack floating ips")
  1620  
  1621  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1622  	if err != nil {
  1623  		logger.Error(err)
  1624  		return false, nil
  1625  	}
  1626  	tags := filterTags(filter)
  1627  	listOpts := floatingips.ListOpts{
  1628  		TagsAny: strings.Join(tags, ","),
  1629  	}
  1630  
  1631  	allPages, err := floatingips.List(conn, listOpts).AllPages(ctx)
  1632  	if err != nil {
  1633  		logger.Error(err)
  1634  		return false, nil
  1635  	}
  1636  
  1637  	allFloatingIPs, err := floatingips.ExtractFloatingIPs(allPages)
  1638  	if err != nil {
  1639  		logger.Error(err)
  1640  		return false, nil
  1641  	}
  1642  
  1643  	numberToDelete := len(allFloatingIPs)
  1644  	numberDeleted := 0
  1645  	for _, floatingIP := range allFloatingIPs {
  1646  		logger.Debugf("Deleting Floating IP %q", floatingIP.ID)
  1647  		err = floatingips.Delete(ctx, conn, floatingIP.ID).ExtractErr()
  1648  		if err != nil {
  1649  			// Ignore the error if the floating ip cannot be found
  1650  			if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1651  				// Just log the error and move on to the next floating IP
  1652  				logger.Debugf("Deleting floating ip %q failed: %v", floatingIP.ID, err)
  1653  				continue
  1654  			}
  1655  			logger.Debugf("Cannot find floating ip %q. It's probably already been deleted.", floatingIP.ID)
  1656  		}
  1657  		numberDeleted++
  1658  	}
  1659  	return numberDeleted == numberToDelete, nil
  1660  }
  1661  
  1662  func deleteImages(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1663  	logger.Debug("Deleting openstack base image")
  1664  	defer logger.Debugf("Exiting deleting openstack base image")
  1665  
  1666  	conn, err := openstackdefaults.NewServiceClient(ctx, "image", opts)
  1667  	if err != nil {
  1668  		logger.Error(err)
  1669  		return false, nil
  1670  	}
  1671  
  1672  	listOpts := images.ListOpts{
  1673  		Tags: filterTags(filter),
  1674  	}
  1675  
  1676  	allPages, err := images.List(conn, listOpts).AllPages(ctx)
  1677  	if err != nil {
  1678  		logger.Error(err)
  1679  		return false, nil
  1680  	}
  1681  
  1682  	allImages, err := images.ExtractImages(allPages)
  1683  	if err != nil {
  1684  		logger.Error(err)
  1685  		return false, nil
  1686  	}
  1687  
  1688  	numberToDelete := len(allImages)
  1689  	numberDeleted := 0
  1690  	for _, image := range allImages {
  1691  		logger.Debugf("Deleting image: %+v", image.ID)
  1692  		err := images.Delete(ctx, conn, image.ID).ExtractErr()
  1693  		if err != nil {
  1694  			// This can fail if the image is still in use by other VMs
  1695  			// Just log the error and move on to the next image
  1696  			logger.Debugf("Deleting Image failed: %v", err)
  1697  			continue
  1698  		}
  1699  		numberDeleted++
  1700  	}
  1701  	return numberDeleted == numberToDelete, nil
  1702  }
  1703  
  1704  func untagRunner(ctx context.Context, opts *clientconfig.ClientOpts, infraID string, logger logrus.FieldLogger) error {
  1705  	backoffSettings := wait.Backoff{
  1706  		Duration: time.Second * 10,
  1707  		Steps:    25,
  1708  	}
  1709  
  1710  	err := wait.ExponentialBackoff(backoffSettings, func() (bool, error) {
  1711  		return untagPrimaryNetwork(ctx, opts, infraID, logger)
  1712  	})
  1713  	if err != nil {
  1714  		if err == wait.ErrWaitTimeout {
  1715  			return err
  1716  		}
  1717  		return fmt.Errorf("unrecoverable error: %w", err)
  1718  	}
  1719  
  1720  	return nil
  1721  }
  1722  
  1723  func deleteRouterRunner(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) error {
  1724  	backoffSettings := wait.Backoff{
  1725  		Duration: time.Second * 15,
  1726  		Factor:   1.3,
  1727  		Steps:    25,
  1728  	}
  1729  
  1730  	err := wait.ExponentialBackoff(backoffSettings, func() (bool, error) {
  1731  		return deleteRouters(ctx, opts, filter, logger)
  1732  	})
  1733  	if err != nil {
  1734  		if err == wait.ErrWaitTimeout {
  1735  			return err
  1736  		}
  1737  		return fmt.Errorf("unrecoverable error: %w", err)
  1738  	}
  1739  
  1740  	return nil
  1741  }
  1742  
  1743  // untagNetwork removes the tag from the primary cluster network based on unfra id
  1744  func untagPrimaryNetwork(ctx context.Context, opts *clientconfig.ClientOpts, infraID string, logger logrus.FieldLogger) (bool, error) {
  1745  	networkTag := infraID + "-primaryClusterNetwork"
  1746  
  1747  	logger.Debugf("Removing tag %v from openstack networks", networkTag)
  1748  	defer logger.Debug("Exiting untagging openstack networks")
  1749  
  1750  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1751  	if err != nil {
  1752  		logger.Debug(err)
  1753  		return false, nil
  1754  	}
  1755  
  1756  	listOpts := networks.ListOpts{
  1757  		Tags: networkTag,
  1758  	}
  1759  
  1760  	allPages, err := networks.List(conn, listOpts).AllPages(ctx)
  1761  	if err != nil {
  1762  		logger.Debug(err)
  1763  		return false, nil
  1764  	}
  1765  
  1766  	allNetworks, err := networks.ExtractNetworks(allPages)
  1767  	if err != nil {
  1768  		logger.Debug(err)
  1769  		return false, nil
  1770  	}
  1771  
  1772  	if len(allNetworks) > 1 {
  1773  		return false, fmt.Errorf("more than one network with tag %s", networkTag)
  1774  	}
  1775  
  1776  	if len(allNetworks) == 0 {
  1777  		// The network has already been deleted.
  1778  		return true, nil
  1779  	}
  1780  
  1781  	err = attributestags.Delete(ctx, conn, "networks", allNetworks[0].ID, networkTag).ExtractErr()
  1782  	if err != nil {
  1783  		return false, nil
  1784  	}
  1785  
  1786  	return true, nil
  1787  }
  1788  
  1789  // validateCloud checks that the target cloud fulfills the minimum requirements
  1790  // for destroy to function.
  1791  func validateCloud(ctx context.Context, opts *clientconfig.ClientOpts, logger logrus.FieldLogger) error {
  1792  	logger.Debug("Validating the cloud")
  1793  
  1794  	// A lack of support for network tagging can lead the Installer to
  1795  	// delete unmanaged resources.
  1796  	//
  1797  	// See https://bugzilla.redhat.com/show_bug.cgi?id=2013877
  1798  	logger.Debug("Validating network extensions")
  1799  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1800  	if err != nil {
  1801  		return fmt.Errorf("failed to build the network client: %w", err)
  1802  	}
  1803  
  1804  	availableExtensions, err := networkextensions.Get(ctx, conn)
  1805  	if err != nil {
  1806  		return fmt.Errorf("failed to fetch network extensions: %w", err)
  1807  	}
  1808  
  1809  	return networkextensions.Validate(availableExtensions)
  1810  }
  1811  
  1812  // cleanClusterSgs removes the installer security groups from the user provided Port.
  1813  func cleanClusterSgs(providedPortSGs []string, clusterSGs []sg.SecGroup) []string {
  1814  	var sgs []string
  1815  	for _, providedPortSG := range providedPortSGs {
  1816  		if !isClusterSG(providedPortSG, clusterSGs) {
  1817  			sgs = append(sgs, providedPortSG)
  1818  		}
  1819  	}
  1820  	return sgs
  1821  }
  1822  
  1823  func isClusterSG(providedPortSG string, clusterSGs []sg.SecGroup) bool {
  1824  	for _, clusterSG := range clusterSGs {
  1825  		if providedPortSG == clusterSG.ID {
  1826  			return true
  1827  		}
  1828  	}
  1829  	return false
  1830  }
  1831  
  1832  func cleanVIPsPorts(ctx context.Context, opts *clientconfig.ClientOpts, filter Filter, logger logrus.FieldLogger) (bool, error) {
  1833  	logger.Debug("Cleaning provided Ports for API and Ingress VIPs")
  1834  	defer logger.Debugf("Exiting clean of provided Ports for API and Ingress VIPs")
  1835  	conn, err := openstackdefaults.NewServiceClient(ctx, "network", opts)
  1836  	if err != nil {
  1837  		logger.Error(err)
  1838  		return false, nil
  1839  	}
  1840  
  1841  	tag := filter["openshiftClusterID"] + openstackdefaults.DualStackVIPsPortTag
  1842  	PortlistOpts := ports.ListOpts{
  1843  		TagsAny: tag,
  1844  	}
  1845  	allPages, err := ports.List(conn, PortlistOpts).AllPages(ctx)
  1846  	if err != nil {
  1847  		logger.Error(err)
  1848  		return false, nil
  1849  	}
  1850  
  1851  	allPorts, err := ports.ExtractPorts(allPages)
  1852  	if err != nil {
  1853  		logger.Error(err)
  1854  		return false, nil
  1855  	}
  1856  
  1857  	numberToClean := len(allPorts)
  1858  	numberCleaned := 0
  1859  
  1860  	// Updating user provided API and Ingress Ports
  1861  	if len(allPorts) > 0 {
  1862  		clusterSGs, err := getSecurityGroups(ctx, conn, filter)
  1863  		if err != nil {
  1864  			logger.Error(err)
  1865  			return false, nil
  1866  		}
  1867  		fipByPort, err := getFIPsByPort(ctx, conn, logger)
  1868  		if err != nil {
  1869  			logger.Error(err)
  1870  			return false, nil
  1871  		}
  1872  		for _, port := range allPorts {
  1873  			logger.Debugf("Updating security groups for Port: %q", port.ID)
  1874  			sgs := cleanClusterSgs(port.SecurityGroups, clusterSGs)
  1875  			_, err := ports.Update(ctx, conn, port.ID, ports.UpdateOpts{SecurityGroups: &sgs}).Extract()
  1876  			if err != nil {
  1877  				return false, nil
  1878  			}
  1879  			if fip, ok := fipByPort[port.ID]; ok {
  1880  				logger.Debugf("Dissociating Floating IP %q", fip.ID)
  1881  				_, err := floatingips.Update(ctx, conn, fip.ID, floatingips.UpdateOpts{}).Extract()
  1882  				if err != nil {
  1883  					// Ignore the error if the floating ip cannot be found and return with an appropriate message if it's another type of error
  1884  					if !gophercloud.ResponseCodeIs(err, http.StatusNotFound) {
  1885  						return false, nil
  1886  					}
  1887  					logger.Debugf("Cannot find floating ip %q. It's probably already been deleted.", fip.ID)
  1888  				}
  1889  			}
  1890  
  1891  			logger.Debugf("Deleting tag for Port: %q", port.ID)
  1892  			err = attributestags.Delete(ctx, conn, "ports", port.ID, tag).ExtractErr()
  1893  			if err != nil {
  1894  				return false, nil
  1895  			}
  1896  			numberCleaned++
  1897  		}
  1898  	}
  1899  	return numberCleaned == numberToClean, nil
  1900  }