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 }