6532be3bc5dfe6f8de7408cd7c145c4ae28e35c0
1 # -*- coding: utf-8 -*-
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
19 from random
import choice
as random_choice
22 from google
.oauth2
import service_account
23 import googleapiclient
.discovery
24 from osm_ro_plugin
import vimconn
26 __author__
= "Sergio Gallardo Ruiz"
27 __date__
= "$11-aug-2021 08:30:00$"
30 if getenv("OSMRO_PDB_DEBUG"):
39 class vimconnector(vimconn
.VimConnector
):
40 # Translate Google Cloud provisioning state to OSM provision state
41 # The first three ones are the transitional status once a user initiated action has been requested
42 # Once the operation is complete, it will transition into the states Succeeded or Failed
43 # https://cloud.google.com/compute/docs/instances/instance-life-cycle
44 provision_state2osm
= {
45 "PROVISIONING": "BUILD",
49 # Translate azure power state to OSM provision state
53 "STOPPING": "INACTIVE",
54 "SUSPENDING": "INACTIVE",
55 "SUSPENDED": "INACTIVE",
56 "TERMINATED": "INACTIVE",
59 # If a net or subnet is tried to be deleted and it has an associated resource, the net is marked "to be deleted"
60 # (incluid it's name in the following list). When the instance is deleted, its associated net will be deleted if
61 # they are present in that list
62 nets_to_be_deleted
= []
79 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
80 checking against the VIM
81 Using common constructor parameters.
82 In this case: config must include the following parameters:
83 subscription_id: assigned subscription identifier
84 region_name: current region for network
85 config may also include the following parameter:
86 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
87 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
88 "^Standard_B" will select a serie B maybe for test environment
90 vimconn
.VimConnector
.__init
__(
105 # Variable that indicates if client must be reloaded or initialized
106 self
.reload_client
= False
110 log_format_simple
= (
111 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
113 log_format_complete
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
114 log_formatter_simple
= logging
.Formatter(
115 log_format_simple
, datefmt
="%Y-%m-%dT%H:%M:%S"
117 self
.handler
= logging
.StreamHandler()
118 self
.handler
.setFormatter(log_formatter_simple
)
120 self
.logger
= logging
.getLogger("ro.vim.gcp")
121 self
.logger
.addHandler(self
.handler
)
123 self
.logger
.setLevel(getattr(logging
, log_level
))
125 if self
.logger
.getEffectiveLevel() == logging
.DEBUG
:
126 log_formatter
= logging
.Formatter(
127 log_format_complete
, datefmt
="%Y-%m-%dT%H:%M:%S"
129 self
.handler
.setFormatter(log_formatter
)
131 self
.logger
.debug("Google Cloud connection init")
133 self
.project
= tenant_id
or tenant_name
135 # REGION - Google Cloud considers regions and zones. A specific region can have more than one zone
136 # (for instance: region us-west1 with the zones us-west1-a, us-west1-b and us-west1-c)
137 # So the region name specified in the config will be considered as a specific zone for GC and
138 # the region will be calculated from that without the preffix.
139 if "region_name" in config
:
140 self
.zone
= config
.get("region_name")
141 self
.region
= self
.zone
.rsplit("-", 1)[0]
143 raise vimconn
.VimConnException(
144 "Google Cloud region_name is not specified at config"
148 self
.logger
.debug("Config: %s", config
)
149 scopes
= ["https://www.googleapis.com/auth/cloud-platform"]
150 self
.credentials
= None
151 if "credentials" in config
:
152 self
.logger
.debug("Setting credentials")
153 # Settings Google Cloud credentials dict
154 credentials_body
= config
["credentials"]
155 # self.logger.debug("Credentials filtered: %s", credentials_body)
156 credentials
= service_account
.Credentials
.from_service_account_info(
159 if "sa_file" in config
:
160 credentials
= service_account
.Credentials
.from_service_account_file(
161 config
.get("sa_file"), scopes
=scopes
163 self
.logger
.debug("Credentials: %s", credentials
)
164 # Construct a Resource for interacting with an API.
165 self
.credentials
= credentials
167 self
.conn_compute
= googleapiclient
.discovery
.build(
168 "compute", "v1", credentials
=credentials
170 except Exception as e
:
171 self
._format
_vimconn
_exception
(e
)
173 raise vimconn
.VimConnException(
174 "It is not possible to init GCP with no credentials"
177 def _reload_connection(self
):
179 Called before any operation, checks python Google Cloud clientsself.reload_client
181 if self
.reload_client
:
182 self
.logger
.debug("reloading google cloud client")
185 # Set to client created
186 self
.conn_compute
= googleapiclient
.discovery
.build("compute", "v1")
187 except Exception as e
:
188 self
._format
_vimconn
_exception
(e
)
190 def _format_vimconn_exception(self
, e
):
192 Transforms a generic exception to a vimConnException
194 self
.logger
.error("Google Cloud plugin error: {}".format(e
))
195 if isinstance(e
, vimconn
.VimConnException
):
198 # In case of generic error recreate client
199 self
.reload_client
= True
200 raise vimconn
.VimConnException(type(e
).__name
__ + ": " + str(e
))
202 def _wait_for_global_operation(self
, operation
):
204 Waits for the end of the specific operation
205 :operation: operation name
208 self
.logger
.debug("Waiting for operation %s", operation
)
212 self
.conn_compute
.globalOperations()
213 .get(project
=self
.project
, operation
=operation
)
217 if result
["status"] == "DONE":
218 if "error" in result
:
219 raise vimconn
.VimConnException(result
["error"])
224 def _wait_for_zone_operation(self
, operation
):
226 Waits for the end of the specific operation
227 :operation: operation name
230 self
.logger
.debug("Waiting for operation %s", operation
)
234 self
.conn_compute
.zoneOperations()
235 .get(project
=self
.project
, operation
=operation
, zone
=self
.zone
)
239 if result
["status"] == "DONE":
240 if "error" in result
:
241 raise vimconn
.VimConnException(result
["error"])
246 def _wait_for_region_operation(self
, operation
):
248 Waits for the end of the specific operation
249 :operation: operation name
252 self
.logger
.debug("Waiting for operation %s", operation
)
256 self
.conn_compute
.regionOperations()
257 .get(project
=self
.project
, operation
=operation
, region
=self
.region
)
261 if result
["status"] == "DONE":
262 if "error" in result
:
263 raise vimconn
.VimConnException(result
["error"])
274 provider_network_profile
=None,
277 Adds a network to VIM
278 :param net_name: name of the network
279 :param net_type: not used for Google Cloud networks
280 :param ip_profile: not used for Google Cloud networks
281 :param shared: Not allowed for Google Cloud Connector
282 :param provider_network_profile: (optional)
284 contains {segmentation-id: vlan, provider-network: vim_netowrk}
285 :return: a tuple with the network identifier and created_items, or raises an exception on error
286 created_items can be None or a dictionary where this method can include key-values that will be passed to
287 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
288 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
293 "new_network begin: net_name %s net_type %s ip_profile %s shared %s provider_network_profile %s",
298 provider_network_profile
,
300 net_name
= self
._check
_vm
_name
(net_name
)
301 net_name
= self
._randomize
_name
(net_name
)
302 self
.logger
.debug("create network name %s, ip_profile %s", net_name
, ip_profile
)
305 self
.logger
.debug("creating network_name: {}".format(net_name
))
307 network
= "projects/{}/global/networks/default".format(self
.project
)
309 if ip_profile
is not None:
310 if "subnet_address" in ip_profile
:
311 subnet_address
= ip_profile
["subnet_address"]
313 "name": str(net_name
),
314 "description": net_name
,
316 "ipCidrRange": subnet_address
,
317 # The network is created in AUTO mode (one subnet per region is created)
318 # "autoCreateSubnetworks": True,
319 "autoCreateSubnetworks": False,
323 self
.conn_compute
.networks()
324 .insert(project
=self
.project
, body
=network_body
)
327 self
._wait
_for
_global
_operation
(operation
["name"])
328 self
.logger
.debug("created network_name: {}".format(net_name
))
330 # Adding firewall rules to allow the traffic in the network:
331 self
._create
_firewall
_rules
(net_name
)
333 # create subnetwork, even if there is no profile
338 if not ip_profile
.get("subnet_address"):
339 # Fake subnet is required
340 subnet_rand
= random
.randint(0, 255)
341 ip_profile
["subnet_address"] = "192.168.{}.0/24".format(subnet_rand
)
343 subnet_name
= net_name
+ "-subnet"
344 subnet_id
= self
._new
_subnet
(
345 subnet_name
, ip_profile
, operation
["targetLink"]
348 self
.logger
.debug("new_network Return: subnet_id: %s", subnet_id
)
350 except Exception as e
:
351 self
._format
_vimconn
_exception
(e
)
353 def _new_subnet(self
, subnet_name
, ip_profile
, network
):
355 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
356 :param net_name: subnet name
358 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
359 otherwise it creates a subnet in the indicated address
360 :return: a tuple with the network identifier and created_items, or raises an exception on error
363 "_new_subnet begin: subnet_name %s ip_profile %s network %s",
369 "create subnet name %s, ip_profile %s", subnet_name
, ip_profile
373 self
.logger
.debug("creating subnet_name: {}".format(subnet_name
))
376 "name": str(subnet_name
),
377 "description": subnet_name
,
379 "ipCidrRange": ip_profile
["subnet_address"],
383 self
.conn_compute
.subnetworks()
385 project
=self
.project
,
387 body
=subnetwork_body
,
391 self
._wait
_for
_region
_operation
(operation
["name"])
393 self
.logger
.debug("created subnet_name: {}".format(subnet_name
))
396 "_new_subnet Return: (%s,%s)",
397 "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
),
400 return "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
), None
401 except Exception as e
:
402 self
._format
_vimconn
_exception
(e
)
404 def get_network_list(self
, filter_dict
={}):
405 """Obtain tenant networks of VIM
409 shared: boolean, not implemented in GC
410 tenant_id: tenant, not used in GC, all networks same tenants
411 admin_state_up: boolean, not implemented in GC
412 status: 'ACTIVE', not implemented in GC #
413 Returns the network list of dictionaries
415 self
.logger
.debug("get_network_list begin: filter_dict %s", filter_dict
)
417 "Getting network (subnetwork) from VIM filter: {}".format(str(filter_dict
))
421 if self
.reload_client
:
422 self
._reload
_connection
()
426 request
= self
.conn_compute
.subnetworks().list(
427 project
=self
.project
, region
=self
.region
430 while request
is not None:
431 response
= request
.execute()
432 self
.logger
.debug("Network list: %s", response
)
433 for net
in response
["items"]:
434 self
.logger
.debug("Network in list: {}".format(str(net
["name"])))
435 if filter_dict
is not None:
436 if "name" in filter_dict
.keys():
438 filter_dict
["name"] == net
["name"]
439 or filter_dict
["name"] == net
["selfLink"]
441 self
.logger
.debug("Network found: %s", net
["name"])
444 "id": str(net
["selfLink"]),
445 "name": str(net
["name"]),
446 "network": str(net
["network"]),
452 "id": str(net
["selfLink"]),
453 "name": str(net
["name"]),
454 "network": str(net
["network"]),
457 request
= self
.conn_compute
.subnetworks().list_next(
458 previous_request
=request
, previous_response
=response
461 self
.logger
.debug("get_network_list Return: net_list %s", net_list
)
464 except Exception as e
:
465 self
.logger
.error("Error in get_network_list()", exc_info
=True)
466 raise vimconn
.VimConnException(e
)
468 def get_network(self
, net_id
):
469 self
.logger
.debug("get_network begin: net_id %s", net_id
)
470 # res_name = self._get_resource_name_from_resource_id(net_id)
471 self
._reload
_connection
()
473 self
.logger
.debug("Get network: %s", net_id
)
474 filter_dict
= {"name": net_id
}
475 network_list
= self
.get_network_list(filter_dict
)
476 self
.logger
.debug("Network list: %s", network_list
)
481 self
.logger
.debug("get_network Return: network_list[0] %s", network_list
[0])
482 return network_list
[0]
484 def delete_network(self
, net_id
, created_items
=None):
486 Removes a tenant network from VIM and its associated elements
487 :param net_id: VIM identifier of the network, provided by method new_network
488 :param created_items: dictionary with extra items to be deleted. provided by method new_network
489 Returns the network identifier or raises an exception upon error or when network is not found
493 "delete_network begin: net_id %s created_items %s",
497 self
.logger
.debug("Deleting network: {}".format(str(net_id
)))
500 net_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
502 # Check associated VMs
503 self
.conn_compute
.instances().list(
504 project
=self
.project
, zone
=self
.zone
507 net_id
= self
.delete_subnet(net_name
, created_items
)
509 self
.logger
.debug("delete_network Return: net_id %s", net_id
)
512 except Exception as e
:
513 self
.logger
.error("Error in delete_network()", exc_info
=True)
514 raise vimconn
.VimConnException(e
)
516 def delete_subnet(self
, net_id
, created_items
=None):
518 Removes a tenant network from VIM and its associated elements
519 :param net_id: VIM identifier of the network, provided by method new_network
520 :param created_items: dictionary with extra items to be deleted. provided by method new_network
521 Returns the network identifier or raises an exception upon error or when network is not found
525 "delete_subnet begin: net_id %s created_items %s",
529 self
.logger
.debug("Deleting subnetwork: {}".format(str(net_id
)))
532 # If the network has no more subnets, it will be deleted too
533 net_info
= self
.get_network(net_id
)
534 # If the subnet is in use by another resource, the deletion will
535 # be retried N times before abort the operation
536 created_items
= created_items
or {}
537 created_items
[net_id
] = False
541 self
.conn_compute
.subnetworks()
543 project
=self
.project
,
549 self
._wait
_for
_region
_operation
(operation
["name"])
550 if net_id
in self
.nets_to_be_deleted
:
551 self
.nets_to_be_deleted
.remove(net_id
)
552 except Exception as e
:
554 e
.args
[0]["status"] == "400"
555 ): # Resource in use, so the net is marked to be deleted
556 self
.logger
.debug("Subnet still in use")
557 self
.nets_to_be_deleted
.append(net_id
)
559 raise vimconn
.VimConnException(e
)
561 self
.logger
.debug("nets_to_be_deleted: %s", self
.nets_to_be_deleted
)
563 # If the network has no more subnets, it will be deleted too
564 # if "network" in net_info and net_id not in self.nets_to_be_deleted:
565 if "network" in net_info
:
566 network_name
= self
._get
_resource
_name
_from
_resource
_id
(
571 # Deletion of the associated firewall rules:
572 self
._delete
_firewall
_rules
(network_name
)
575 self
.conn_compute
.networks()
577 project
=self
.project
,
578 network
=network_name
,
582 self
._wait
_for
_global
_operation
(operation
["name"])
583 except Exception as e
:
584 self
.logger
.debug("error deleting associated network %s", e
)
586 self
.logger
.debug("delete_subnet Return: net_id %s", net_id
)
589 except Exception as e
:
590 self
.logger
.error("Error in delete_network()", exc_info
=True)
591 raise vimconn
.VimConnException(e
)
593 def new_flavor(self
, flavor_data
):
595 It is not allowed to create new flavors (machine types) in Google Cloud, must always use an existing one
597 raise vimconn
.VimConnNotImplemented(
598 "It is not possible to create new flavors in Google Cloud"
601 def new_tenant(self
, tenant_name
, tenant_description
):
603 It is not allowed to create new tenants in Google Cloud
605 raise vimconn
.VimConnNotImplemented(
606 "It is not possible to create a TENANT in Google Cloud"
609 def get_flavor(self
, flavor_id
):
611 Obtains the flavor_data from the flavor_id/machine type id
613 self
.logger
.debug("get_flavor begin: flavor_id %s", flavor_id
)
617 self
.conn_compute
.machineTypes()
618 .get(project
=self
.project
, zone
=self
.zone
, machineType
=flavor_id
)
621 flavor_data
= response
622 self
.logger
.debug("Machine type data: %s", flavor_data
)
626 "id": flavor_data
["id"],
628 "id_complete": flavor_data
["selfLink"],
629 "ram": flavor_data
["memoryMb"],
630 "vcpus": flavor_data
["guestCpus"],
631 "disk": flavor_data
["maximumPersistentDisksSizeGb"],
634 self
.logger
.debug("get_flavor Return: flavor %s", flavor
)
637 raise vimconn
.VimConnNotFoundException(
638 "flavor '{}' not found".format(flavor_id
)
640 except Exception as e
:
641 self
._format
_vimconn
_exception
(e
)
643 # Google Cloud VM names can not have some special characters
644 def _check_vm_name(self
, vm_name
):
646 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
647 Only lowercase and hyphens are allowed
649 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
651 # First: the VM name max length is 64 characters
652 vm_name_aux
= vm_name
[:62]
654 # Second: replace not allowed characters
655 for elem
in chars_not_allowed_list
:
656 # Check if string is in the main string
657 if elem
in vm_name_aux
:
658 # self.logger.debug("Dentro del IF")
660 vm_name_aux
= vm_name_aux
.replace(elem
, "-")
662 return vm_name_aux
.lower()
664 def get_flavor_id_from_data(self
, flavor_dict
):
665 self
.logger
.debug("get_flavor_id_from_data begin: flavor_dict %s", flavor_dict
)
666 filter_dict
= flavor_dict
or {}
670 self
.conn_compute
.machineTypes()
671 .list(project
=self
.project
, zone
=self
.zone
)
674 machine_types_list
= response
["items"]
675 # self.logger.debug("List of machine types: %s", machine_types_list)
677 cpus
= filter_dict
.get("vcpus") or 0
678 memMB
= filter_dict
.get("ram") or 0
679 # Workaround (it should be 0)
680 numberInterfaces
= len(filter_dict
.get("interfaces", [])) or 4
683 filtered_machines
= []
684 for machine_type
in machine_types_list
:
686 machine_type
["guestCpus"] >= cpus
687 and machine_type
["memoryMb"] >= memMB
688 # In Google Cloud the number of virtual network interfaces scales with
689 # the number of virtual CPUs with a minimum of 2 and a maximum of 8:
690 # https://cloud.google.com/vpc/docs/create-use-multiple-interfaces#max-interfaces
691 and machine_type
["guestCpus"] >= numberInterfaces
693 filtered_machines
.append(machine_type
)
695 # self.logger.debug("Filtered machines: %s", filtered_machines)
698 listedFilteredMachines
= sorted(
702 float(k
["memoryMb"]),
703 int(k
["maximumPersistentDisksSizeGb"]),
707 # self.logger.debug("Sorted filtered machines: %s", listedFilteredMachines)
709 if listedFilteredMachines
:
711 "get_flavor_id_from_data Return: listedFilteredMachines[0][name] %s",
712 listedFilteredMachines
[0]["name"],
714 return listedFilteredMachines
[0]["name"]
716 raise vimconn
.VimConnNotFoundException(
717 "Cannot find any flavor matching '{}'".format(str(flavor_dict
))
720 except Exception as e
:
721 self
._format
_vimconn
_exception
(e
)
723 def delete_flavor(self
, flavor_id
):
724 raise vimconn
.VimConnNotImplemented(
725 "It is not possible to delete a flavor in Google Cloud"
728 def delete_tenant(self
, tenant_id
):
729 raise vimconn
.VimConnNotImplemented(
730 "It is not possible to delete a TENANT in Google Cloud"
733 def new_image(self
, image_dict
):
735 This function comes from the early days when we though the image could be embedded in the package.
736 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
738 raise vimconn
.VimConnNotImplemented("Not implemented")
740 def get_image_id_from_path(self
, path
):
742 This function comes from the early days when we though the image could be embedded in the package.
743 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
745 raise vimconn
.VimConnNotImplemented("Not implemented")
747 def get_image_list(self
, filter_dict
={}):
748 """Obtain tenant images from VIM
750 name: image name with the format: image project:image family:image version
751 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
752 for the provided publisher and offer
753 id: image uuid, currently not supported for azure
754 Returns the image list of dictionaries:
755 [{<the fields at Filter_dict plus some VIM specific>}, ...]
758 self
.logger
.debug("get_image_list begin: filter_dict %s", filter_dict
)
762 # Get image id from parameter image_id:
763 # <image Project>:image-family:<family> => Latest version of the family
764 # <image Project>:image:<image> => Specific image
765 # <image Project>:<image> => Specific image
767 image_info
= filter_dict
["name"].split(":")
768 image_project
= image_info
[0]
769 if len(image_info
) == 2:
771 image_item
= image_info
[1]
772 if len(image_info
) == 3:
773 image_type
= image_info
[1]
774 image_item
= image_info
[2]
776 raise vimconn
.VimConnNotFoundException("Wrong format for image")
779 if image_type
== "image-family":
781 self
.conn_compute
.images()
782 .getFromFamily(project
=image_project
, family
=image_item
)
785 elif image_type
== "image":
787 self
.conn_compute
.images()
788 .get(project
=image_project
, image
=image_item
)
792 raise vimconn
.VimConnNotFoundException("Wrong format for image")
795 "id": "projects/%s/global/images/%s"
796 % (image_project
, image_response
["name"]),
798 [image_project
, image_item
, image_response
["name"]]
803 self
.logger
.debug("get_image_list Return: image_list %s", image_list
)
806 except Exception as e
:
807 self
._format
_vimconn
_exception
(e
)
809 def delete_image(self
, image_id
):
810 raise vimconn
.VimConnNotImplemented("Not implemented")
812 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
813 """Send and action over a VM instance from VIM
814 Returns the vm_id if the action was successfully sent to the VIM
816 raise vimconn
.VimConnNotImplemented("Not necessary")
818 def _randomize_name(self
, name
):
819 """Adds a random string to allow requests with the same VM name
820 Returns the name with an additional random string (if the total size is bigger
821 than 62 the original name will be truncated)
830 + "".join(random_choice("0123456789abcdef") for _
in range(12))
832 self
.conn_compute
.instances().get(
833 project
=self
.project
, zone
=self
.zone
, instance
=random_name
835 # If no exception is arisen, the random name exists for an instance,
836 # so a new random name must be generated
838 except Exception as e
:
839 if e
.args
[0]["status"] == "404":
840 self
.logger
.debug("New random name: %s", random_name
)
844 "Exception generating random name (%s) for the instance", name
846 self
._format
_vimconn
_exception
(e
)
855 image_id
=None, # <image project>:(image|image-family):<image/family id>
857 affinity_group_list
=None,
861 availability_zone_index
=None,
862 availability_zone_list
=None,
865 "new_vminstance begin: name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
866 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
873 availability_zone_index
,
874 availability_zone_list
,
877 if self
.reload_client
:
878 self
._reload
_connection
()
880 # Validate input data is valid
881 # # First of all, the name must be adapted because Google Cloud only allows names consist of
882 # lowercase letters (a-z), numbers and hyphens (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
883 vm_name
= self
._check
_vm
_name
(name
)
884 vm_name
= self
._randomize
_name
(vm_name
)
887 # At least one network must be provided
889 raise vimconn
.VimConnException(
890 "At least one net must be provided to create a new VM"
895 metadata
= self
._build
_metadata
(vm_name
, cloud_config
)
897 # Building network interfaces list
898 network_interfaces
= []
901 if not net
.get("net_id"):
902 if not net
.get("name"):
907 ] = "regions/%s/subnetworks/" % self
.region
+ net
.get("name")
909 net_iface
["subnetwork"] = net
.get("net_id")
910 if net
.get("ip_address"):
911 net_iface
["networkIP"] = net
.get("ip_address")
913 # In order to get an external IP address, the key "accessConfigs" must be used
914 # in the interace. It has to be of type "ONE_TO_ONE_NAT" and name "External NAT"
915 if net
.get("floating_ip", False) or (
916 net
["use"] == "mgmt" and self
.config
.get("use_floating_ip")
918 net_iface
["accessConfigs"] = [
919 {"type": "ONE_TO_ONE_NAT", "name": "External NAT"}
922 network_interfaces
.append(net_iface
)
924 self
.logger
.debug("Network interfaces: %s", network_interfaces
)
926 self
.logger
.debug("Source image: %s", image_id
)
930 "machineType": self
.get_flavor(flavor_id
)["id_complete"],
931 # Specify the boot disk and the image to use as a source.
936 "initializeParams": {
937 "sourceImage": image_id
,
941 # Specify the network interfaces
942 "networkInterfaces": network_interfaces
,
943 "metadata": metadata
,
947 self
.conn_compute
.instances()
948 .insert(project
=self
.project
, zone
=self
.zone
, body
=vm_parameters
)
951 self
._wait
_for
_zone
_operation
(response
["name"])
953 # The created instance info is obtained to get the name of the generated network interfaces (nic0, nic1...)
955 self
.conn_compute
.instances()
956 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_name
)
959 self
.logger
.debug("instance get: %s", response
)
960 vm_id
= response
["name"]
962 # The generated network interfaces in the instance are include in net_list:
963 for _
, net
in enumerate(net_list
):
964 for net_ifaces
in response
["networkInterfaces"]:
967 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
971 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
974 if network_id
== self
._get
_resource
_name
_from
_resource
_id
(
975 net_ifaces
["subnetwork"]
977 net
["vim_id"] = net_ifaces
["name"]
980 "new_vminstance Return: (name %s, created_items %s)",
984 return vm_name
, created_items
986 except Exception as e
:
987 # Rollback vm creacion
988 if vm_id
is not None:
990 self
.logger
.debug("exception creating vm try to rollback")
991 self
.delete_vminstance(vm_id
, created_items
)
992 except Exception as e2
:
993 self
.logger
.error("new_vminstance rollback fail {}".format(e2
))
997 "Exception creating new vminstance: %s", e
, exc_info
=True
999 self
._format
_vimconn
_exception
(e
)
1001 def _build_metadata(self
, vm_name
, cloud_config
):
1004 metadata
["items"] = []
1006 # if there is a cloud-init load it
1008 self
.logger
.debug("cloud config: %s", cloud_config
)
1009 _
, userdata
= self
._create
_user
_data
(cloud_config
)
1010 metadata
["items"].append({"key": "user-data", "value": userdata
})
1012 # either password of ssh-keys are required
1013 # we will always use ssh-keys, in case it is not available we will generate it
1014 self
.logger
.debug("metadata: %s", metadata
)
1018 def get_vminstance(self
, vm_id
):
1020 Obtaing the vm instance data from v_id
1022 self
.logger
.debug("get_vminstance begin: vm_id %s", vm_id
)
1023 self
._reload
_connection
()
1027 self
.conn_compute
.instances()
1028 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1031 # vm = response["source"]
1032 except Exception as e
:
1033 self
._format
_vimconn
_exception
(e
)
1035 self
.logger
.debug("get_vminstance Return: response %s", response
)
1038 def delete_vminstance(self
, vm_id
, created_items
=None, volumes_to_hold
=None):
1039 """Deletes a vm instance from the vim."""
1041 "delete_vminstance begin: vm_id %s created_items %s",
1045 if self
.reload_client
:
1046 self
._reload
_connection
()
1048 created_items
= created_items
or {}
1050 vm
= self
.get_vminstance(vm_id
)
1053 self
.conn_compute
.instances()
1054 .delete(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1057 self
._wait
_for
_zone
_operation
(operation
["name"])
1059 # The associated subnets must be checked if they are marked to be deleted
1060 for netIface
in vm
["networkInterfaces"]:
1062 self
._get
_resource
_name
_from
_resource
_id
(netIface
["subnetwork"])
1063 in self
.nets_to_be_deleted
1065 self
._get
_resource
_name
_from
_resource
_id
(
1066 self
.delete_network(netIface
["subnetwork"])
1069 self
.logger
.debug("delete_vminstance end")
1071 except Exception as e
:
1072 # The VM can be deleted previously during network deletion
1073 if e
.args
[0]["status"] == "404":
1074 self
.logger
.debug("The VM doesn't exist or has been deleted")
1076 self
._format
_vimconn
_exception
(e
)
1078 def _get_resource_name_from_resource_id(self
, resource_id
):
1080 Obtains resource_name from the google cloud complete identifier: resource_name will always be last item
1083 "_get_resource_name_from_resource_id begin: resource_id %s",
1087 resource
= str(resource_id
.split("/")[-1])
1090 "_get_resource_name_from_resource_id Return: resource %s",
1094 except Exception as e
:
1095 raise vimconn
.VimConnException(
1096 "Unable to get resource name from resource_id '{}' Error: '{}'".format(
1101 def _get_id_from_image(self
, image
):
1103 Obtains image_id from the google cloud complete image identifier: image_id will be the last five items
1105 self
.logger
.debug(f
"_get_id_from_image begin: image {image}")
1107 image_id
= "/".join(image
.split("/")[-5:])
1108 self
.logger
.debug(f
"_get_id_from_image Return: image_id {image_id}")
1110 except Exception as e
:
1111 raise vimconn
.VimConnException(
1112 f
"Unable to get image_id from image '{image}' Error: '{e}'"
1115 def refresh_nets_status(self
, net_list
):
1116 """Get the status of the networks
1117 Params: the list of network identifiers
1118 Returns a dictionary with:
1119 net_id: #VIM id of this network
1120 status: #Mandatory. Text with one of:
1121 # DELETED (not found at vim)
1122 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1123 # OTHER (Vim reported other status not understood)
1124 # ERROR (VIM indicates an ERROR status)
1125 # ACTIVE, INACTIVE, DOWN (admin down),
1126 # BUILD (on building process)
1128 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1129 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1131 self
.logger
.debug("refresh_nets_status begin: net_list %s", net_list
)
1133 self
._reload
_connection
()
1135 for net_id
in net_list
:
1137 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1140 self
.conn_compute
.subnetworks()
1141 .get(project
=self
.project
, region
=self
.region
, subnetwork
=resName
)
1144 self
.logger
.debug("get subnetwork: %s", net
)
1146 out_nets
[net_id
] = {
1147 "status": "ACTIVE", # Google Cloud does not provide the status in subnetworks getting
1148 "vim_info": str(net
),
1150 except vimconn
.VimConnNotFoundException
as e
:
1152 "VimConnNotFoundException %s when searching subnet", e
1154 out_nets
[net_id
] = {
1155 "status": "DELETED",
1156 "error_msg": str(e
),
1158 except Exception as e
:
1160 "Exception %s when searching subnet", e
, exc_info
=True
1162 out_nets
[net_id
] = {
1163 "status": "VIM_ERROR",
1164 "error_msg": str(e
),
1167 self
.logger
.debug("refresh_nets_status Return: out_nets %s", out_nets
)
1170 def refresh_vms_status(self
, vm_list
):
1171 """Get the status of the virtual machines and their interfaces/ports
1172 Params: the list of VM identifiers
1173 Returns a dictionary with:
1174 vm_id: # VIM id of this Virtual Machine
1175 status: # Mandatory. Text with one of:
1176 # DELETED (not found at vim)
1177 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1178 # OTHER (Vim reported other status not understood)
1179 # ERROR (VIM indicates an ERROR status)
1180 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1181 # BUILD (on building process), ERROR
1182 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1183 # (ACTIVE:NoMgmtIP is not returned for Azure)
1185 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1186 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1187 interfaces: list with interface info. Each item a dictionary with:
1188 vim_interface_id - The ID of the interface
1189 mac_address - The MAC address of the interface.
1190 ip_address - The IP address of the interface within the subnet.
1192 self
.logger
.debug("refresh_vms_status begin: vm_list %s", vm_list
)
1194 self
._reload
_connection
()
1196 search_vm_list
= vm_list
or {}
1198 for vm_id
in search_vm_list
:
1201 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1204 self
.conn_compute
.instances()
1205 .get(project
=self
.project
, zone
=self
.zone
, instance
=res_name
)
1209 disk_source
= vm
["disks"][0]["source"]
1210 self
.logger
.debug("getting disk information")
1212 self
.conn_compute
.disks()
1214 project
=self
.project
,
1216 disk
=self
._get
_resource
_name
_from
_resource
_id
(disk_source
),
1221 if disk
is not None:
1222 self
.logger
.debug(f
"disk: {disk}")
1224 "id": self
._get
_id
_from
_image
(disk
["sourceImage"]),
1225 "source": disk_source
,
1231 "creationTimestamp": vm
["creationTimestamp"],
1232 "lastStartTimestamp": vm
["lastStartTimestamp"],
1235 "cpuPlatform": vm
["cpuPlatform"],
1236 "zone": self
._get
_resource
_name
_from
_resource
_id
(vm
["zone"]),
1237 "machineType": vm
["machineType"],
1239 "id": self
._get
_resource
_name
_from
_resource
_id
(
1245 out_vm
["vim_info"] = str(vim_info
)
1246 out_vm
["status"] = self
.provision_state2osm
.get(vm
["status"], "OTHER")
1248 # In Google Cloud the there is no difference between provision or power status,
1249 # so if provision status method doesn't return a specific state (OTHER), the
1250 # power method is called
1251 if out_vm
["status"] == "OTHER":
1252 out_vm
["status"] = self
.power_state2osm
.get(vm
["status"], "OTHER")
1254 network_interfaces
= vm
["networkInterfaces"]
1255 out_vm
["interfaces"] = self
._get
_vm
_interfaces
_status
(
1256 vm_id
, network_interfaces
1258 except Exception as e
:
1259 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1260 out_vm
["status"] = "VIM_ERROR"
1261 out_vm
["error_msg"] = str(e
)
1262 out_vm
["vim_info"] = None
1264 out_vms
[vm_id
] = out_vm
1266 self
.logger
.debug("refresh_vms_status Return: out_vms %s", out_vms
)
1269 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1271 Gets the interfaces detail for a vm
1272 :param interfaces: List of interfaces.
1273 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1276 "_get_vm_interfaces_status begin: vm_id %s interfaces %s",
1282 for network_interface
in interfaces
:
1284 interface_dict
["vim_interface_id"] = network_interface
["name"]
1285 interface_dict
["vim_net_id"] = network_interface
["subnetwork"]
1288 ips
.append(network_interface
["networkIP"])
1289 interface_dict
["ip_address"] = ";".join(ips
)
1290 interface_list
.append(interface_dict
)
1293 "_get_vm_interfaces_status Return: interface_list %s",
1296 return interface_list
1297 except Exception as e
:
1299 "Exception %s obtaining interface data for vm: %s",
1304 self
._format
_vimconn
_exception
(e
)
1306 def _create_firewall_rules(self
, network
):
1308 Creates the necessary firewall rules to allow the traffic in the network
1309 (https://cloud.google.com/vpc/docs/firewalls)
1311 :return: a list with the names of the firewall rules
1313 self
.logger
.debug("_create_firewall_rules begin: network %s", network
)
1317 # Adding firewall rule to allow http:
1318 self
.logger
.debug("creating firewall rule to allow http")
1319 firewall_rule_body
= {
1320 "name": "fw-rule-http-" + network
,
1321 "network": "global/networks/" + network
,
1322 "allowed": [{"IPProtocol": "tcp", "ports": ["80"]}],
1324 self
.conn_compute
.firewalls().insert(
1325 project
=self
.project
, body
=firewall_rule_body
1328 # Adding firewall rule to allow ssh:
1329 self
.logger
.debug("creating firewall rule to allow ssh")
1330 firewall_rule_body
= {
1331 "name": "fw-rule-ssh-" + network
,
1332 "network": "global/networks/" + network
,
1333 "allowed": [{"IPProtocol": "tcp", "ports": ["22"]}],
1335 self
.conn_compute
.firewalls().insert(
1336 project
=self
.project
, body
=firewall_rule_body
1339 # Adding firewall rule to allow ping:
1340 self
.logger
.debug("creating firewall rule to allow ping")
1341 firewall_rule_body
= {
1342 "name": "fw-rule-icmp-" + network
,
1343 "network": "global/networks/" + network
,
1344 "allowed": [{"IPProtocol": "icmp"}],
1346 self
.conn_compute
.firewalls().insert(
1347 project
=self
.project
, body
=firewall_rule_body
1350 # Adding firewall rule to allow internal:
1351 self
.logger
.debug("creating firewall rule to allow internal")
1352 firewall_rule_body
= {
1353 "name": "fw-rule-internal-" + network
,
1354 "network": "global/networks/" + network
,
1356 {"IPProtocol": "tcp", "ports": ["0-65535"]},
1357 {"IPProtocol": "udp", "ports": ["0-65535"]},
1358 {"IPProtocol": "icmp"},
1361 self
.conn_compute
.firewalls().insert(
1362 project
=self
.project
, body
=firewall_rule_body
1365 # Adding firewall rule to allow microk8s:
1366 self
.logger
.debug("creating firewall rule to allow microk8s")
1367 firewall_rule_body
= {
1368 "name": "fw-rule-microk8s-" + network
,
1369 "network": "global/networks/" + network
,
1370 "allowed": [{"IPProtocol": "tcp", "ports": ["16443"]}],
1372 self
.conn_compute
.firewalls().insert(
1373 project
=self
.project
, body
=firewall_rule_body
1376 # Adding firewall rule to allow rdp:
1377 self
.logger
.debug("creating firewall rule to allow rdp")
1378 firewall_rule_body
= {
1379 "name": "fw-rule-rdp-" + network
,
1380 "network": "global/networks/" + network
,
1381 "allowed": [{"IPProtocol": "tcp", "ports": ["3389"]}],
1383 self
.conn_compute
.firewalls().insert(
1384 project
=self
.project
, body
=firewall_rule_body
1387 # Adding firewall rule to allow osm:
1388 self
.logger
.debug("creating firewall rule to allow osm")
1389 firewall_rule_body
= {
1390 "name": "fw-rule-osm-" + network
,
1391 "network": "global/networks/" + network
,
1392 "allowed": [{"IPProtocol": "tcp", "ports": ["9001", "9999"]}],
1394 self
.conn_compute
.firewalls().insert(
1395 project
=self
.project
, body
=firewall_rule_body
1399 "_create_firewall_rules Return: list_rules %s", rules_list
1402 except Exception as e
:
1404 "Unable to create google cloud firewall rules for network '{}'".format(
1408 self
._format
_vimconn
_exception
(e
)
1410 def _delete_firewall_rules(self
, network
):
1412 Deletes the associated firewall rules to the network
1414 :return: a list with the names of the firewall rules
1416 self
.logger
.debug("_delete_firewall_rules begin: network %s", network
)
1421 self
.conn_compute
.firewalls().list(project
=self
.project
).execute()
1423 for item
in rules_list
["items"]:
1424 if network
== self
._get
_resource
_name
_from
_resource
_id
(item
["network"]):
1425 self
.conn_compute
.firewalls().delete(
1426 project
=self
.project
, firewall
=item
["name"]
1429 self
.logger
.debug("_delete_firewall_rules Return: list_rules %s", 0)
1431 except Exception as e
:
1433 "Unable to delete google cloud firewall rules for network '{}'".format(
1437 self
._format
_vimconn
_exception
(e
)
1439 def migrate_instance(self
, vm_id
, compute_host
=None):
1443 vm_id: ID of an instance
1444 compute_host: Host to migrate the vdu to
1446 # TODO: Add support for migration
1447 raise vimconn
.VimConnNotImplemented("Not implemented")
1449 def resize_instance(self
, vm_id
, flavor_id
=None):
1453 vm_id: ID of an instance
1454 flavor_id: flavor_id to resize the vdu to
1456 # TODO: Add support for resize
1457 raise vimconn
.VimConnNotImplemented("Not implemented")