5b58c1a5b1c7fa4a635d4f211e155b92650776dd
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(
187 "compute", "v1", credentials
=self
.credentials
189 except Exception as e
:
190 self
._format
_vimconn
_exception
(e
)
192 def _format_vimconn_exception(self
, e
):
194 Transforms a generic exception to a vimConnException
196 self
.logger
.error("Google Cloud plugin error: {}".format(e
))
197 if isinstance(e
, vimconn
.VimConnException
):
200 # In case of generic error recreate client
201 self
.reload_client
= True
202 raise vimconn
.VimConnException(type(e
).__name
__ + ": " + str(e
))
204 def _wait_for_global_operation(self
, operation
):
206 Waits for the end of the specific operation
207 :operation: operation name
210 self
.logger
.debug("Waiting for operation %s", operation
)
214 self
.conn_compute
.globalOperations()
215 .get(project
=self
.project
, operation
=operation
)
219 if result
["status"] == "DONE":
220 if "error" in result
:
221 raise vimconn
.VimConnException(result
["error"])
226 def _wait_for_zone_operation(self
, operation
):
228 Waits for the end of the specific operation
229 :operation: operation name
232 self
.logger
.debug("Waiting for operation %s", operation
)
236 self
.conn_compute
.zoneOperations()
237 .get(project
=self
.project
, operation
=operation
, zone
=self
.zone
)
241 if result
["status"] == "DONE":
242 if "error" in result
:
243 raise vimconn
.VimConnException(result
["error"])
248 def _wait_for_region_operation(self
, operation
):
250 Waits for the end of the specific operation
251 :operation: operation name
254 self
.logger
.debug("Waiting for operation %s", operation
)
258 self
.conn_compute
.regionOperations()
259 .get(project
=self
.project
, operation
=operation
, region
=self
.region
)
263 if result
["status"] == "DONE":
264 if "error" in result
:
265 raise vimconn
.VimConnException(result
["error"])
276 provider_network_profile
=None,
279 Adds a network to VIM
280 :param net_name: name of the network
281 :param net_type: not used for Google Cloud networks
282 :param ip_profile: not used for Google Cloud networks
283 :param shared: Not allowed for Google Cloud Connector
284 :param provider_network_profile: (optional)
286 contains {segmentation-id: vlan, provider-network: vim_netowrk}
287 :return: a tuple with the network identifier and created_items, or raises an exception on error
288 created_items can be None or a dictionary where this method can include key-values that will be passed to
289 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
290 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
295 "new_network begin: net_name %s net_type %s ip_profile %s shared %s provider_network_profile %s",
300 provider_network_profile
,
302 net_name
= self
._check
_vm
_name
(net_name
)
303 net_name
= self
._randomize
_name
(net_name
)
304 self
.logger
.debug("create network name %s, ip_profile %s", net_name
, ip_profile
)
307 self
.logger
.debug("creating network_name: {}".format(net_name
))
309 network
= "projects/{}/global/networks/default".format(self
.project
)
311 if ip_profile
is not None:
312 if "subnet_address" in ip_profile
:
313 subnet_address
= ip_profile
["subnet_address"]
315 "name": str(net_name
),
316 "description": net_name
,
318 "ipCidrRange": subnet_address
,
319 # The network is created in AUTO mode (one subnet per region is created)
320 # "autoCreateSubnetworks": True,
321 "autoCreateSubnetworks": False,
325 self
.conn_compute
.networks()
326 .insert(project
=self
.project
, body
=network_body
)
329 self
._wait
_for
_global
_operation
(operation
["name"])
330 self
.logger
.debug("created network_name: {}".format(net_name
))
332 # Adding firewall rules to allow the traffic in the network:
333 self
._create
_firewall
_rules
(net_name
)
335 # create subnetwork, even if there is no profile
340 if not ip_profile
.get("subnet_address"):
341 # Fake subnet is required
342 subnet_rand
= random
.randint(0, 255)
343 ip_profile
["subnet_address"] = "192.168.{}.0/24".format(subnet_rand
)
345 subnet_name
= net_name
+ "-subnet"
346 subnet_id
= self
._new
_subnet
(
347 subnet_name
, ip_profile
, operation
["targetLink"]
350 self
.logger
.debug("new_network Return: subnet_id: %s", subnet_id
)
352 except Exception as e
:
353 self
._format
_vimconn
_exception
(e
)
355 def _new_subnet(self
, subnet_name
, ip_profile
, network
):
357 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
358 :param net_name: subnet name
360 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
361 otherwise it creates a subnet in the indicated address
362 :return: a tuple with the network identifier and created_items, or raises an exception on error
365 "_new_subnet begin: subnet_name %s ip_profile %s network %s",
371 "create subnet name %s, ip_profile %s", subnet_name
, ip_profile
375 self
.logger
.debug("creating subnet_name: {}".format(subnet_name
))
378 "name": str(subnet_name
),
379 "description": subnet_name
,
381 "ipCidrRange": ip_profile
["subnet_address"],
385 self
.conn_compute
.subnetworks()
387 project
=self
.project
,
389 body
=subnetwork_body
,
393 self
._wait
_for
_region
_operation
(operation
["name"])
395 self
.logger
.debug("created subnet_name: {}".format(subnet_name
))
398 "_new_subnet Return: (%s,%s)",
399 "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
),
402 return "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
), None
403 except Exception as e
:
404 self
._format
_vimconn
_exception
(e
)
406 def get_network_list(self
, filter_dict
={}):
407 """Obtain tenant networks of VIM
411 shared: boolean, not implemented in GC
412 tenant_id: tenant, not used in GC, all networks same tenants
413 admin_state_up: boolean, not implemented in GC
414 status: 'ACTIVE', not implemented in GC #
415 Returns the network list of dictionaries
417 self
.logger
.debug("get_network_list begin: filter_dict %s", filter_dict
)
419 "Getting network (subnetwork) from VIM filter: {}".format(str(filter_dict
))
423 if self
.reload_client
:
424 self
._reload
_connection
()
428 request
= self
.conn_compute
.subnetworks().list(
429 project
=self
.project
, region
=self
.region
432 while request
is not None:
433 response
= request
.execute()
434 self
.logger
.debug("Network list: %s", response
)
435 for net
in response
["items"]:
436 self
.logger
.debug("Network in list: {}".format(str(net
["name"])))
437 if filter_dict
is not None:
438 if "name" in filter_dict
.keys():
440 filter_dict
["name"] == net
["name"]
441 or filter_dict
["name"] == net
["selfLink"]
443 self
.logger
.debug("Network found: %s", net
["name"])
446 "id": str(net
["selfLink"]),
447 "name": str(net
["name"]),
448 "network": str(net
["network"]),
454 "id": str(net
["selfLink"]),
455 "name": str(net
["name"]),
456 "network": str(net
["network"]),
459 request
= self
.conn_compute
.subnetworks().list_next(
460 previous_request
=request
, previous_response
=response
463 self
.logger
.debug("get_network_list Return: net_list %s", net_list
)
466 except Exception as e
:
467 self
.logger
.error("Error in get_network_list()", exc_info
=True)
468 raise vimconn
.VimConnException(e
)
470 def get_network(self
, net_id
):
471 self
.logger
.debug("get_network begin: net_id %s", net_id
)
472 # res_name = self._get_resource_name_from_resource_id(net_id)
473 self
._reload
_connection
()
475 self
.logger
.debug("Get network: %s", net_id
)
476 filter_dict
= {"name": net_id
}
477 network_list
= self
.get_network_list(filter_dict
)
478 self
.logger
.debug("Network list: %s", network_list
)
483 self
.logger
.debug("get_network Return: network_list[0] %s", network_list
[0])
484 return network_list
[0]
486 def delete_network(self
, net_id
, created_items
=None):
488 Removes a tenant network from VIM and its associated elements
489 :param net_id: VIM identifier of the network, provided by method new_network
490 :param created_items: dictionary with extra items to be deleted. provided by method new_network
491 Returns the network identifier or raises an exception upon error or when network is not found
495 "delete_network begin: net_id %s created_items %s",
499 self
.logger
.debug("Deleting network: {}".format(str(net_id
)))
502 net_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
504 # Check associated VMs
505 self
.conn_compute
.instances().list(
506 project
=self
.project
, zone
=self
.zone
509 net_id
= self
.delete_subnet(net_name
, created_items
)
511 self
.logger
.debug("delete_network Return: net_id %s", net_id
)
514 except Exception as e
:
515 self
.logger
.error("Error in delete_network()", exc_info
=True)
516 raise vimconn
.VimConnException(e
)
518 def delete_subnet(self
, net_id
, created_items
=None):
520 Removes a tenant network from VIM and its associated elements
521 :param net_id: VIM identifier of the network, provided by method new_network
522 :param created_items: dictionary with extra items to be deleted. provided by method new_network
523 Returns the network identifier or raises an exception upon error or when network is not found
527 "delete_subnet begin: net_id %s created_items %s",
531 self
.logger
.debug("Deleting subnetwork: {}".format(str(net_id
)))
534 # If the network has no more subnets, it will be deleted too
535 net_info
= self
.get_network(net_id
)
536 # If the subnet is in use by another resource, the deletion will
537 # be retried N times before abort the operation
538 created_items
= created_items
or {}
539 created_items
[net_id
] = False
543 self
.conn_compute
.subnetworks()
545 project
=self
.project
,
551 self
._wait
_for
_region
_operation
(operation
["name"])
552 if net_id
in self
.nets_to_be_deleted
:
553 self
.nets_to_be_deleted
.remove(net_id
)
554 except Exception as e
:
556 e
.args
[0]["status"] == "400"
557 ): # Resource in use, so the net is marked to be deleted
558 self
.logger
.debug("Subnet still in use")
559 self
.nets_to_be_deleted
.append(net_id
)
561 raise vimconn
.VimConnException(e
)
563 self
.logger
.debug("nets_to_be_deleted: %s", self
.nets_to_be_deleted
)
565 # If the network has no more subnets, it will be deleted too
566 # if "network" in net_info and net_id not in self.nets_to_be_deleted:
567 if "network" in net_info
:
568 network_name
= self
._get
_resource
_name
_from
_resource
_id
(
573 # Deletion of the associated firewall rules:
574 self
._delete
_firewall
_rules
(network_name
)
577 self
.conn_compute
.networks()
579 project
=self
.project
,
580 network
=network_name
,
584 self
._wait
_for
_global
_operation
(operation
["name"])
585 except Exception as e
:
586 self
.logger
.debug("error deleting associated network %s", e
)
588 self
.logger
.debug("delete_subnet Return: net_id %s", net_id
)
591 except Exception as e
:
592 self
.logger
.error("Error in delete_network()", exc_info
=True)
593 raise vimconn
.VimConnException(e
)
595 def new_flavor(self
, flavor_data
):
597 It is not allowed to create new flavors (machine types) in Google Cloud, must always use an existing one
599 raise vimconn
.VimConnNotImplemented(
600 "It is not possible to create new flavors in Google Cloud"
603 def new_tenant(self
, tenant_name
, tenant_description
):
605 It is not allowed to create new tenants in Google Cloud
607 raise vimconn
.VimConnNotImplemented(
608 "It is not possible to create a TENANT in Google Cloud"
611 def get_flavor(self
, flavor_id
):
613 Obtains the flavor_data from the flavor_id/machine type id
615 self
.logger
.debug("get_flavor begin: flavor_id %s", flavor_id
)
619 self
.conn_compute
.machineTypes()
620 .get(project
=self
.project
, zone
=self
.zone
, machineType
=flavor_id
)
623 flavor_data
= response
624 self
.logger
.debug("Machine type data: %s", flavor_data
)
628 "id": flavor_data
["id"],
630 "id_complete": flavor_data
["selfLink"],
631 "ram": flavor_data
["memoryMb"],
632 "vcpus": flavor_data
["guestCpus"],
633 "disk": flavor_data
["maximumPersistentDisksSizeGb"],
636 self
.logger
.debug("get_flavor Return: flavor %s", flavor
)
639 raise vimconn
.VimConnNotFoundException(
640 "flavor '{}' not found".format(flavor_id
)
642 except Exception as e
:
643 self
._format
_vimconn
_exception
(e
)
645 # Google Cloud VM names can not have some special characters
646 def _check_vm_name(self
, vm_name
):
648 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
649 Only lowercase and hyphens are allowed
651 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
653 # First: the VM name max length is 64 characters
654 vm_name_aux
= vm_name
[:62]
656 # Second: replace not allowed characters
657 for elem
in chars_not_allowed_list
:
658 # Check if string is in the main string
659 if elem
in vm_name_aux
:
660 # self.logger.debug("Dentro del IF")
662 vm_name_aux
= vm_name_aux
.replace(elem
, "-")
664 return vm_name_aux
.lower()
666 def get_flavor_id_from_data(self
, flavor_dict
):
667 self
.logger
.debug("get_flavor_id_from_data begin: flavor_dict %s", flavor_dict
)
668 filter_dict
= flavor_dict
or {}
672 self
.conn_compute
.machineTypes()
673 .list(project
=self
.project
, zone
=self
.zone
)
676 machine_types_list
= response
["items"]
677 # self.logger.debug("List of machine types: %s", machine_types_list)
679 cpus
= filter_dict
.get("vcpus") or 0
680 memMB
= filter_dict
.get("ram") or 0
681 # Workaround (it should be 0)
682 numberInterfaces
= len(filter_dict
.get("interfaces", [])) or 4
685 filtered_machines
= []
686 for machine_type
in machine_types_list
:
688 machine_type
["guestCpus"] >= cpus
689 and machine_type
["memoryMb"] >= memMB
690 # In Google Cloud the number of virtual network interfaces scales with
691 # the number of virtual CPUs with a minimum of 2 and a maximum of 8:
692 # https://cloud.google.com/vpc/docs/create-use-multiple-interfaces#max-interfaces
693 and machine_type
["guestCpus"] >= numberInterfaces
695 filtered_machines
.append(machine_type
)
697 # self.logger.debug("Filtered machines: %s", filtered_machines)
700 listedFilteredMachines
= sorted(
704 float(k
["memoryMb"]),
705 int(k
["maximumPersistentDisksSizeGb"]),
709 # self.logger.debug("Sorted filtered machines: %s", listedFilteredMachines)
711 if listedFilteredMachines
:
713 "get_flavor_id_from_data Return: listedFilteredMachines[0][name] %s",
714 listedFilteredMachines
[0]["name"],
716 return listedFilteredMachines
[0]["name"]
718 raise vimconn
.VimConnNotFoundException(
719 "Cannot find any flavor matching '{}'".format(str(flavor_dict
))
722 except Exception as e
:
723 self
._format
_vimconn
_exception
(e
)
725 def delete_flavor(self
, flavor_id
):
726 raise vimconn
.VimConnNotImplemented(
727 "It is not possible to delete a flavor in Google Cloud"
730 def delete_tenant(self
, tenant_id
):
731 raise vimconn
.VimConnNotImplemented(
732 "It is not possible to delete a TENANT in Google Cloud"
735 def new_image(self
, image_dict
):
737 This function comes from the early days when we though the image could be embedded in the package.
738 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
740 raise vimconn
.VimConnNotImplemented("Not implemented")
742 def get_image_id_from_path(self
, path
):
744 This function comes from the early days when we though the image could be embedded in the package.
745 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
747 raise vimconn
.VimConnNotImplemented("Not implemented")
749 def get_image_list(self
, filter_dict
={}):
750 """Obtain tenant images from VIM
752 name: image name with the format: image project:image family:image version
753 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
754 for the provided publisher and offer
755 id: image uuid, currently not supported for azure
756 Returns the image list of dictionaries:
757 [{<the fields at Filter_dict plus some VIM specific>}, ...]
760 self
.logger
.debug("get_image_list begin: filter_dict %s", filter_dict
)
764 # Get image id from parameter image_id:
765 # <image Project>:image-family:<family> => Latest version of the family
766 # <image Project>:image:<image> => Specific image
767 # <image Project>:<image> => Specific image
769 image_info
= filter_dict
["name"].split(":")
770 image_project
= image_info
[0]
771 if len(image_info
) == 2:
773 image_item
= image_info
[1]
774 if len(image_info
) == 3:
775 image_type
= image_info
[1]
776 image_item
= image_info
[2]
778 raise vimconn
.VimConnNotFoundException("Wrong format for image")
781 if image_type
== "image-family":
783 self
.conn_compute
.images()
784 .getFromFamily(project
=image_project
, family
=image_item
)
787 elif image_type
== "image":
789 self
.conn_compute
.images()
790 .get(project
=image_project
, image
=image_item
)
794 raise vimconn
.VimConnNotFoundException("Wrong format for image")
797 "id": "projects/%s/global/images/%s"
798 % (image_project
, image_response
["name"]),
800 [image_project
, image_item
, image_response
["name"]]
805 self
.logger
.debug("get_image_list Return: image_list %s", image_list
)
808 except Exception as e
:
809 self
._format
_vimconn
_exception
(e
)
811 def delete_image(self
, image_id
):
812 raise vimconn
.VimConnNotImplemented("Not implemented")
814 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
815 """Send and action over a VM instance from VIM
816 Returns the vm_id if the action was successfully sent to the VIM
818 raise vimconn
.VimConnNotImplemented("Not necessary")
820 def _randomize_name(self
, name
):
821 """Adds a random string to allow requests with the same VM name
822 Returns the name with an additional random string (if the total size is bigger
823 than 62 the original name will be truncated)
832 + "".join(random_choice("0123456789abcdef") for _
in range(12))
834 self
.conn_compute
.instances().get(
835 project
=self
.project
, zone
=self
.zone
, instance
=random_name
837 # If no exception is arisen, the random name exists for an instance,
838 # so a new random name must be generated
840 except Exception as e
:
841 if e
.args
[0]["status"] == "404":
842 self
.logger
.debug("New random name: %s", random_name
)
846 "Exception generating random name (%s) for the instance", name
848 self
._format
_vimconn
_exception
(e
)
857 image_id
=None, # <image project>:(image|image-family):<image/family id>
859 affinity_group_list
=None,
863 availability_zone_index
=None,
864 availability_zone_list
=None,
867 "new_vminstance begin: name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
868 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
875 availability_zone_index
,
876 availability_zone_list
,
879 if self
.reload_client
:
880 self
._reload
_connection
()
882 # Validate input data is valid
883 # # First of all, the name must be adapted because Google Cloud only allows names consist of
884 # lowercase letters (a-z), numbers and hyphens (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
885 vm_name
= self
._check
_vm
_name
(name
)
886 vm_name
= self
._randomize
_name
(vm_name
)
889 # At least one network must be provided
891 raise vimconn
.VimConnException(
892 "At least one net must be provided to create a new VM"
897 metadata
= self
._build
_metadata
(vm_name
, cloud_config
)
899 # Building network interfaces list
900 network_interfaces
= []
903 if not net
.get("net_id"):
904 if not net
.get("name"):
909 ] = "regions/%s/subnetworks/" % self
.region
+ net
.get("name")
911 net_iface
["subnetwork"] = net
.get("net_id")
912 if net
.get("ip_address"):
913 net_iface
["networkIP"] = net
.get("ip_address")
915 # In order to get an external IP address, the key "accessConfigs" must be used
916 # in the interace. It has to be of type "ONE_TO_ONE_NAT" and name "External NAT"
917 if net
.get("floating_ip", False) or (
918 net
["use"] == "mgmt" and self
.config
.get("use_floating_ip")
920 net_iface
["accessConfigs"] = [
921 {"type": "ONE_TO_ONE_NAT", "name": "External NAT"}
924 network_interfaces
.append(net_iface
)
926 self
.logger
.debug("Network interfaces: %s", network_interfaces
)
928 self
.logger
.debug("Source image: %s", image_id
)
932 "machineType": self
.get_flavor(flavor_id
)["id_complete"],
933 # Specify the boot disk and the image to use as a source.
938 "initializeParams": {
939 "sourceImage": image_id
,
943 # Specify the network interfaces
944 "networkInterfaces": network_interfaces
,
945 "metadata": metadata
,
949 self
.conn_compute
.instances()
950 .insert(project
=self
.project
, zone
=self
.zone
, body
=vm_parameters
)
953 self
._wait
_for
_zone
_operation
(response
["name"])
955 # The created instance info is obtained to get the name of the generated network interfaces (nic0, nic1...)
957 self
.conn_compute
.instances()
958 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_name
)
961 self
.logger
.debug("instance get: %s", response
)
962 vm_id
= response
["name"]
964 # The generated network interfaces in the instance are include in net_list:
965 for _
, net
in enumerate(net_list
):
966 for net_ifaces
in response
["networkInterfaces"]:
969 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
973 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
976 if network_id
== self
._get
_resource
_name
_from
_resource
_id
(
977 net_ifaces
["subnetwork"]
979 net
["vim_id"] = net_ifaces
["name"]
982 "new_vminstance Return: (name %s, created_items %s)",
986 return vm_name
, created_items
988 except Exception as e
:
989 # Rollback vm creacion
990 if vm_id
is not None:
992 self
.logger
.debug("exception creating vm try to rollback")
993 self
.delete_vminstance(vm_id
, created_items
)
994 except Exception as e2
:
995 self
.logger
.error("new_vminstance rollback fail {}".format(e2
))
999 "Exception creating new vminstance: %s", e
, exc_info
=True
1001 self
._format
_vimconn
_exception
(e
)
1003 def _build_metadata(self
, vm_name
, cloud_config
):
1006 metadata
["items"] = []
1008 # if there is a cloud-init load it
1010 self
.logger
.debug("cloud config: %s", cloud_config
)
1011 _
, userdata
= self
._create
_user
_data
(cloud_config
)
1012 metadata
["items"].append({"key": "user-data", "value": userdata
})
1014 # either password of ssh-keys are required
1015 # we will always use ssh-keys, in case it is not available we will generate it
1016 self
.logger
.debug("metadata: %s", metadata
)
1020 def get_vminstance(self
, vm_id
):
1022 Obtaing the vm instance data from v_id
1024 self
.logger
.debug("get_vminstance begin: vm_id %s", vm_id
)
1025 self
._reload
_connection
()
1029 self
.conn_compute
.instances()
1030 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1033 # vm = response["source"]
1034 except Exception as e
:
1035 self
._format
_vimconn
_exception
(e
)
1037 self
.logger
.debug("get_vminstance Return: response %s", response
)
1040 def delete_vminstance(self
, vm_id
, created_items
=None, volumes_to_hold
=None):
1041 """Deletes a vm instance from the vim."""
1043 "delete_vminstance begin: vm_id %s created_items %s",
1047 if self
.reload_client
:
1048 self
._reload
_connection
()
1050 created_items
= created_items
or {}
1052 vm
= self
.get_vminstance(vm_id
)
1055 self
.conn_compute
.instances()
1056 .delete(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1059 self
._wait
_for
_zone
_operation
(operation
["name"])
1061 # The associated subnets must be checked if they are marked to be deleted
1062 for netIface
in vm
["networkInterfaces"]:
1064 self
._get
_resource
_name
_from
_resource
_id
(netIface
["subnetwork"])
1065 in self
.nets_to_be_deleted
1067 self
._get
_resource
_name
_from
_resource
_id
(
1068 self
.delete_network(netIface
["subnetwork"])
1071 self
.logger
.debug("delete_vminstance end")
1073 except Exception as e
:
1074 # The VM can be deleted previously during network deletion
1075 if e
.args
[0]["status"] == "404":
1076 self
.logger
.debug("The VM doesn't exist or has been deleted")
1078 self
._format
_vimconn
_exception
(e
)
1080 def _get_resource_name_from_resource_id(self
, resource_id
):
1082 Obtains resource_name from the google cloud complete identifier: resource_name will always be last item
1085 "_get_resource_name_from_resource_id begin: resource_id %s",
1089 resource
= str(resource_id
.split("/")[-1])
1092 "_get_resource_name_from_resource_id Return: resource %s",
1096 except Exception as e
:
1097 raise vimconn
.VimConnException(
1098 "Unable to get resource name from resource_id '{}' Error: '{}'".format(
1103 def _get_id_from_image(self
, image
):
1105 Obtains image_id from the google cloud complete image identifier: image_id will be the last five items
1107 self
.logger
.debug(f
"_get_id_from_image begin: image {image}")
1109 image_id
= "/".join(image
.split("/")[-5:])
1110 self
.logger
.debug(f
"_get_id_from_image Return: image_id {image_id}")
1112 except Exception as e
:
1113 raise vimconn
.VimConnException(
1114 f
"Unable to get image_id from image '{image}' Error: '{e}'"
1117 def refresh_nets_status(self
, net_list
):
1118 """Get the status of the networks
1119 Params: the list of network identifiers
1120 Returns a dictionary with:
1121 net_id: #VIM id of this network
1122 status: #Mandatory. Text with one of:
1123 # DELETED (not found at vim)
1124 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1125 # OTHER (Vim reported other status not understood)
1126 # ERROR (VIM indicates an ERROR status)
1127 # ACTIVE, INACTIVE, DOWN (admin down),
1128 # BUILD (on building process)
1130 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1131 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1133 self
.logger
.debug("refresh_nets_status begin: net_list %s", net_list
)
1135 self
._reload
_connection
()
1137 for net_id
in net_list
:
1139 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1142 self
.conn_compute
.subnetworks()
1143 .get(project
=self
.project
, region
=self
.region
, subnetwork
=resName
)
1146 self
.logger
.debug("get subnetwork: %s", net
)
1148 out_nets
[net_id
] = {
1149 "status": "ACTIVE", # Google Cloud does not provide the status in subnetworks getting
1150 "vim_info": str(net
),
1152 except vimconn
.VimConnNotFoundException
as e
:
1154 "VimConnNotFoundException %s when searching subnet", e
1156 out_nets
[net_id
] = {
1157 "status": "DELETED",
1158 "error_msg": str(e
),
1160 except Exception as e
:
1162 "Exception %s when searching subnet", e
, exc_info
=True
1164 out_nets
[net_id
] = {
1165 "status": "VIM_ERROR",
1166 "error_msg": str(e
),
1169 self
.logger
.debug("refresh_nets_status Return: out_nets %s", out_nets
)
1172 def refresh_vms_status(self
, vm_list
):
1173 """Get the status of the virtual machines and their interfaces/ports
1174 Params: the list of VM identifiers
1175 Returns a dictionary with:
1176 vm_id: # VIM id of this Virtual Machine
1177 status: # Mandatory. Text with one of:
1178 # DELETED (not found at vim)
1179 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1180 # OTHER (Vim reported other status not understood)
1181 # ERROR (VIM indicates an ERROR status)
1182 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1183 # BUILD (on building process), ERROR
1184 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1185 # (ACTIVE:NoMgmtIP is not returned for Azure)
1187 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1188 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1189 interfaces: list with interface info. Each item a dictionary with:
1190 vim_interface_id - The ID of the interface
1191 mac_address - The MAC address of the interface.
1192 ip_address - The IP address of the interface within the subnet.
1194 self
.logger
.debug("refresh_vms_status begin: vm_list %s", vm_list
)
1196 self
._reload
_connection
()
1198 search_vm_list
= vm_list
or {}
1200 for vm_id
in search_vm_list
:
1203 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1206 self
.conn_compute
.instances()
1207 .get(project
=self
.project
, zone
=self
.zone
, instance
=res_name
)
1211 disk_source
= vm
["disks"][0]["source"]
1212 self
.logger
.debug("getting disk information")
1214 self
.conn_compute
.disks()
1216 project
=self
.project
,
1218 disk
=self
._get
_resource
_name
_from
_resource
_id
(disk_source
),
1223 if disk
is not None:
1224 self
.logger
.debug(f
"disk: {disk}")
1226 "id": self
._get
_id
_from
_image
(disk
["sourceImage"]),
1227 "source": disk_source
,
1233 "creationTimestamp": vm
["creationTimestamp"],
1234 "lastStartTimestamp": vm
["lastStartTimestamp"],
1237 "cpuPlatform": vm
["cpuPlatform"],
1238 "zone": self
._get
_resource
_name
_from
_resource
_id
(vm
["zone"]),
1239 "machineType": vm
["machineType"],
1241 "id": self
._get
_resource
_name
_from
_resource
_id
(
1247 out_vm
["vim_info"] = str(vim_info
)
1248 out_vm
["status"] = self
.provision_state2osm
.get(vm
["status"], "OTHER")
1250 # In Google Cloud the there is no difference between provision or power status,
1251 # so if provision status method doesn't return a specific state (OTHER), the
1252 # power method is called
1253 if out_vm
["status"] == "OTHER":
1254 out_vm
["status"] = self
.power_state2osm
.get(vm
["status"], "OTHER")
1256 network_interfaces
= vm
["networkInterfaces"]
1257 out_vm
["interfaces"] = self
._get
_vm
_interfaces
_status
(
1258 vm_id
, network_interfaces
1260 except Exception as e
:
1261 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1262 out_vm
["status"] = "VIM_ERROR"
1263 out_vm
["error_msg"] = str(e
)
1264 out_vm
["vim_info"] = None
1266 out_vms
[vm_id
] = out_vm
1268 self
.logger
.debug("refresh_vms_status Return: out_vms %s", out_vms
)
1271 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1273 Gets the interfaces detail for a vm
1274 :param interfaces: List of interfaces.
1275 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1278 "_get_vm_interfaces_status begin: vm_id %s interfaces %s",
1284 for network_interface
in interfaces
:
1286 interface_dict
["vim_interface_id"] = network_interface
["name"]
1287 interface_dict
["vim_net_id"] = network_interface
["subnetwork"]
1290 ips
.append(network_interface
["networkIP"])
1291 interface_dict
["ip_address"] = ";".join(ips
)
1292 interface_list
.append(interface_dict
)
1295 "_get_vm_interfaces_status Return: interface_list %s",
1298 return interface_list
1299 except Exception as e
:
1301 "Exception %s obtaining interface data for vm: %s",
1306 self
._format
_vimconn
_exception
(e
)
1308 def _create_firewall_rules(self
, network
):
1310 Creates the necessary firewall rules to allow the traffic in the network
1311 (https://cloud.google.com/vpc/docs/firewalls)
1313 :return: a list with the names of the firewall rules
1315 self
.logger
.debug("_create_firewall_rules begin: network %s", network
)
1319 # Adding firewall rule to allow http:
1320 self
.logger
.debug("creating firewall rule to allow http")
1321 firewall_rule_body
= {
1322 "name": "fw-rule-http-" + network
,
1323 "network": "global/networks/" + network
,
1324 "allowed": [{"IPProtocol": "tcp", "ports": ["80"]}],
1326 self
.conn_compute
.firewalls().insert(
1327 project
=self
.project
, body
=firewall_rule_body
1330 # Adding firewall rule to allow ssh:
1331 self
.logger
.debug("creating firewall rule to allow ssh")
1332 firewall_rule_body
= {
1333 "name": "fw-rule-ssh-" + network
,
1334 "network": "global/networks/" + network
,
1335 "allowed": [{"IPProtocol": "tcp", "ports": ["22"]}],
1337 self
.conn_compute
.firewalls().insert(
1338 project
=self
.project
, body
=firewall_rule_body
1341 # Adding firewall rule to allow ping:
1342 self
.logger
.debug("creating firewall rule to allow ping")
1343 firewall_rule_body
= {
1344 "name": "fw-rule-icmp-" + network
,
1345 "network": "global/networks/" + network
,
1346 "allowed": [{"IPProtocol": "icmp"}],
1348 self
.conn_compute
.firewalls().insert(
1349 project
=self
.project
, body
=firewall_rule_body
1352 # Adding firewall rule to allow internal:
1353 self
.logger
.debug("creating firewall rule to allow internal")
1354 firewall_rule_body
= {
1355 "name": "fw-rule-internal-" + network
,
1356 "network": "global/networks/" + network
,
1358 {"IPProtocol": "tcp", "ports": ["0-65535"]},
1359 {"IPProtocol": "udp", "ports": ["0-65535"]},
1360 {"IPProtocol": "icmp"},
1363 self
.conn_compute
.firewalls().insert(
1364 project
=self
.project
, body
=firewall_rule_body
1367 # Adding firewall rule to allow microk8s:
1368 self
.logger
.debug("creating firewall rule to allow microk8s")
1369 firewall_rule_body
= {
1370 "name": "fw-rule-microk8s-" + network
,
1371 "network": "global/networks/" + network
,
1372 "allowed": [{"IPProtocol": "tcp", "ports": ["16443"]}],
1374 self
.conn_compute
.firewalls().insert(
1375 project
=self
.project
, body
=firewall_rule_body
1378 # Adding firewall rule to allow rdp:
1379 self
.logger
.debug("creating firewall rule to allow rdp")
1380 firewall_rule_body
= {
1381 "name": "fw-rule-rdp-" + network
,
1382 "network": "global/networks/" + network
,
1383 "allowed": [{"IPProtocol": "tcp", "ports": ["3389"]}],
1385 self
.conn_compute
.firewalls().insert(
1386 project
=self
.project
, body
=firewall_rule_body
1389 # Adding firewall rule to allow osm:
1390 self
.logger
.debug("creating firewall rule to allow osm")
1391 firewall_rule_body
= {
1392 "name": "fw-rule-osm-" + network
,
1393 "network": "global/networks/" + network
,
1394 "allowed": [{"IPProtocol": "tcp", "ports": ["9001", "9999"]}],
1396 self
.conn_compute
.firewalls().insert(
1397 project
=self
.project
, body
=firewall_rule_body
1401 "_create_firewall_rules Return: list_rules %s", rules_list
1404 except Exception as e
:
1406 "Unable to create google cloud firewall rules for network '{}'".format(
1410 self
._format
_vimconn
_exception
(e
)
1412 def _delete_firewall_rules(self
, network
):
1414 Deletes the associated firewall rules to the network
1416 :return: a list with the names of the firewall rules
1418 self
.logger
.debug("_delete_firewall_rules begin: network %s", network
)
1423 self
.conn_compute
.firewalls().list(project
=self
.project
).execute()
1425 for item
in rules_list
["items"]:
1426 if network
== self
._get
_resource
_name
_from
_resource
_id
(item
["network"]):
1427 self
.conn_compute
.firewalls().delete(
1428 project
=self
.project
, firewall
=item
["name"]
1431 self
.logger
.debug("_delete_firewall_rules Return: list_rules %s", 0)
1433 except Exception as e
:
1435 "Unable to delete google cloud firewall rules for network '{}'".format(
1439 self
._format
_vimconn
_exception
(e
)
1441 def migrate_instance(self
, vm_id
, compute_host
=None):
1445 vm_id: ID of an instance
1446 compute_host: Host to migrate the vdu to
1448 # TODO: Add support for migration
1449 raise vimconn
.VimConnNotImplemented("Not implemented")
1451 def resize_instance(self
, vm_id
, flavor_id
=None):
1455 vm_id: ID of an instance
1456 flavor_id: flavor_id to resize the vdu to
1458 # TODO: Add support for resize
1459 raise vimconn
.VimConnNotImplemented("Not implemented")