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
17 from osm_ro_plugin
import vimconn
21 from random
import choice
as random_choice
24 from google
.api_core
.exceptions
import NotFound
25 import googleapiclient
.discovery
26 from google
.oauth2
import service_account
28 from cryptography
.hazmat
.primitives
import serialization
as crypto_serialization
29 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
30 from cryptography
.hazmat
.backends
import default_backend
as crypto_default_backend
34 __author__
= "Sergio Gallardo Ruiz"
35 __date__
= "$11-aug-2021 08:30:00$"
38 if getenv("OSMRO_PDB_DEBUG"):
47 class vimconnector(vimconn
.VimConnector
):
49 # Translate Google Cloud provisioning state to OSM provision state
50 # The first three ones are the transitional status once a user initiated action has been requested
51 # Once the operation is complete, it will transition into the states Succeeded or Failed
52 # https://cloud.google.com/compute/docs/instances/instance-life-cycle
53 provision_state2osm
= {
54 "PROVISIONING": "BUILD",
58 # Translate azure power state to OSM provision state
62 "STOPPING": "INACTIVE",
63 "SUSPENDING": "INACTIVE",
64 "SUSPENDED": "INACTIVE",
65 "TERMINATED": "INACTIVE",
68 # If a net or subnet is tried to be deleted and it has an associated resource, the net is marked "to be deleted"
69 # (incluid it's name in the following list). When the instance is deleted, its associated net will be deleted if
70 # they are present in that list
71 nets_to_be_deleted
= []
88 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
89 checking against the VIM
90 Using common constructor parameters.
91 In this case: config must include the following parameters:
92 subscription_id: assigned subscription identifier
93 region_name: current region for network
94 config may also include the following parameter:
95 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
96 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
97 "^Standard_B" will select a serie B maybe for test environment
99 vimconn
.VimConnector
.__init
__(
114 # Variable that indicates if client must be reloaded or initialized
115 self
.reload_client
= False
119 log_format_simple
= (
120 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
122 log_format_complete
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
123 log_formatter_simple
= logging
.Formatter(
124 log_format_simple
, datefmt
="%Y-%m-%dT%H:%M:%S"
126 self
.handler
= logging
.StreamHandler()
127 self
.handler
.setFormatter(log_formatter_simple
)
129 self
.logger
= logging
.getLogger("ro.vim.gcp")
130 self
.logger
.addHandler(self
.handler
)
132 self
.logger
.setLevel(getattr(logging
, log_level
))
134 if self
.logger
.getEffectiveLevel() == logging
.DEBUG
:
135 log_formatter
= logging
.Formatter(
136 log_format_complete
, datefmt
="%Y-%m-%dT%H:%M:%S"
138 self
.handler
.setFormatter(log_formatter
)
140 self
.logger
.debug("Google Cloud connection init")
142 self
.project
= tenant_id
or tenant_name
144 # REGION - Google Cloud considers regions and zones. A specific region can have more than one zone
145 # (for instance: region us-west1 with the zones us-west1-a, us-west1-b and us-west1-c)
146 # So the region name specified in the config will be considered as a specific zone for GC and
147 # the region will be calculated from that without the preffix.
148 if "region_name" in config
:
149 self
.zone
= config
.get("region_name")
150 self
.region
= self
.zone
.rsplit("-", 1)[0]
152 raise vimconn
.VimConnException(
153 "Google Cloud region_name is not specified at config"
157 self
.logger
.debug("Config: %s", config
)
158 scopes
= ["https://www.googleapis.com/auth/cloud-platform"]
159 self
.credentials
= None
161 "credentials" in config
163 self
.logger
.debug("Setting credentials")
164 # Settings Google Cloud credentials dict
165 credentials_body
= config
["credentials"]
166 # self.logger.debug("Credentials filtered: %s", credentials_body)
167 credentials
= service_account
.Credentials
.from_service_account_info(
170 if "sa_file" in config
:
171 credentials
= service_account
.Credentials
.from_service_account_file(
172 config
.get("sa_file"), scopes
=scopes
174 self
.logger
.debug("Credentials: %s", credentials
)
175 # Construct a Resource for interacting with an API.
176 self
.credentials
= credentials
178 self
.conn_compute
= googleapiclient
.discovery
.build(
179 "compute", "v1", credentials
=credentials
181 except Exception as e
:
182 self
._format
_vimconn
_exception
(e
)
184 raise vimconn
.VimConnException(
185 "It is not possible to init GCP with no credentials"
188 def _reload_connection(self
):
190 Called before any operation, checks python Google Cloud clientsself.reload_client
192 if self
.reload_client
:
193 self
.logger
.debug("reloading google cloud client")
196 # Set to client created
197 self
.conn_compute
= googleapiclient
.discovery
.build("compute", "v1")
198 except Exception as e
:
199 self
._format
_vimconn
_exception
(e
)
201 def _format_vimconn_exception(self
, e
):
203 Transforms a generic exception to a vimConnException
205 self
.logger
.error("Google Cloud plugin error: {}".format(e
))
206 if isinstance(e
, vimconn
.VimConnException
):
209 # In case of generic error recreate client
210 self
.reload_client
= True
211 raise vimconn
.VimConnException(type(e
).__name
__ + ": " + str(e
))
213 def _wait_for_global_operation(self
, operation
):
215 Waits for the end of the specific operation
216 :operation: operation name
219 self
.logger
.debug("Waiting for operation %s", operation
)
223 self
.conn_compute
.globalOperations()
224 .get(project
=self
.project
, operation
=operation
)
228 if result
["status"] == "DONE":
229 if "error" in result
:
230 raise vimconn
.VimConnException(result
["error"])
235 def _wait_for_zone_operation(self
, operation
):
237 Waits for the end of the specific operation
238 :operation: operation name
241 self
.logger
.debug("Waiting for operation %s", operation
)
245 self
.conn_compute
.zoneOperations()
246 .get(project
=self
.project
, operation
=operation
, zone
=self
.zone
)
250 if result
["status"] == "DONE":
251 if "error" in result
:
252 raise vimconn
.VimConnException(result
["error"])
257 def _wait_for_region_operation(self
, operation
):
259 Waits for the end of the specific operation
260 :operation: operation name
263 self
.logger
.debug("Waiting for operation %s", operation
)
267 self
.conn_compute
.regionOperations()
268 .get(project
=self
.project
, operation
=operation
, region
=self
.region
)
272 if result
["status"] == "DONE":
273 if "error" in result
:
274 raise vimconn
.VimConnException(result
["error"])
285 provider_network_profile
=None,
288 Adds a network to VIM
289 :param net_name: name of the network
290 :param net_type: not used for Google Cloud networks
291 :param ip_profile: not used for Google Cloud networks
292 :param shared: Not allowed for Google Cloud Connector
293 :param provider_network_profile: (optional)
295 contains {segmentation-id: vlan, provider-network: vim_netowrk}
296 :return: a tuple with the network identifier and created_items, or raises an exception on error
297 created_items can be None or a dictionary where this method can include key-values that will be passed to
298 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
299 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
304 "new_network begin: net_name %s net_type %s ip_profile %s shared %s provider_network_profile %s",
309 provider_network_profile
,
311 net_name
= self
._check
_vm
_name
(net_name
)
312 net_name
= self
._randomize
_name
(net_name
)
313 self
.logger
.debug("create network name %s, ip_profile %s", net_name
, ip_profile
)
317 self
.logger
.debug("creating network_name: {}".format(net_name
))
319 network
= "projects/{}/global/networks/default".format(self
.project
)
321 if ip_profile
is not None:
322 if "subnet_address" in ip_profile
:
323 subnet_address
= ip_profile
["subnet_address"]
325 "name": str(net_name
),
326 "description": net_name
,
328 "ipCidrRange": subnet_address
,
329 # "autoCreateSubnetworks": True, # The network is created in AUTO mode (one subnet per region is created)
330 "autoCreateSubnetworks": False,
334 self
.conn_compute
.networks()
335 .insert(project
=self
.project
, body
=network_body
)
338 self
._wait
_for
_global
_operation
(operation
["name"])
339 self
.logger
.debug("created network_name: {}".format(net_name
))
341 # Adding firewall rules to allow the traffic in the network:
342 rules_list
= self
._create
_firewall
_rules
(net_name
)
344 # create subnetwork, even if there is no profile
349 if not ip_profile
.get("subnet_address"):
350 # Fake subnet is required
351 subnet_rand
= random
.randint(0, 255)
352 ip_profile
["subnet_address"] = "192.168.{}.0/24".format(subnet_rand
)
354 subnet_name
= net_name
+ "-subnet"
355 subnet_id
= self
._new
_subnet
(
356 subnet_name
, ip_profile
, operation
["targetLink"]
359 self
.logger
.debug("new_network Return: subnet_id: %s", subnet_id
)
361 except Exception as e
:
362 self
._format
_vimconn
_exception
(e
)
364 def _new_subnet(self
, subnet_name
, ip_profile
, network
):
366 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
367 :param net_name: subnet name
369 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
370 otherwise it creates a subnet in the indicated address
371 :return: a tuple with the network identifier and created_items, or raises an exception on error
374 "_new_subnet begin: subnet_name %s ip_profile %s network %s",
380 "create subnet name %s, ip_profile %s", subnet_name
, ip_profile
385 self
.logger
.debug("creating subnet_name: {}".format(subnet_name
))
388 "name": str(subnet_name
),
389 "description": subnet_name
,
391 "ipCidrRange": ip_profile
["subnet_address"],
395 self
.conn_compute
.subnetworks()
397 project
=self
.project
,
399 body
=subnetwork_body
,
403 self
._wait
_for
_region
_operation
(operation
["name"])
405 self
.logger
.debug("created subnet_name: {}".format(subnet_name
))
408 "_new_subnet Return: (%s,%s)",
409 "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
),
412 return "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
), None
413 except Exception as e
:
414 self
._format
_vimconn
_exception
(e
)
416 def get_network_list(self
, filter_dict
={}):
417 """Obtain tenant networks of VIM
421 shared: boolean, not implemented in GC
422 tenant_id: tenant, not used in GC, all networks same tenants
423 admin_state_up: boolean, not implemented in GC
424 status: 'ACTIVE', not implemented in GC #
425 Returns the network list of dictionaries
427 self
.logger
.debug("get_network_list begin: filter_dict %s", filter_dict
)
429 "Getting network (subnetwork) from VIM filter: {}".format(str(filter_dict
))
434 if self
.reload_client
:
435 self
._reload
_connection
()
439 request
= self
.conn_compute
.subnetworks().list(
440 project
=self
.project
, region
=self
.region
443 while request
is not None:
444 response
= request
.execute()
445 self
.logger
.debug("Network list: %s", response
)
446 for net
in response
["items"]:
447 self
.logger
.debug("Network in list: {}".format(str(net
["name"])))
448 if filter_dict
is not None:
449 if "name" in filter_dict
.keys():
451 filter_dict
["name"] == net
["name"]
452 or filter_dict
["name"] == net
["selfLink"]
454 self
.logger
.debug("Network found: %s", net
["name"])
457 "id": str(net
["selfLink"]),
458 "name": str(net
["name"]),
459 "network": str(net
["network"]),
465 "id": str(net
["selfLink"]),
466 "name": str(net
["name"]),
467 "network": str(net
["network"]),
470 request
= self
.conn_compute
.subnetworks().list_next(
471 previous_request
=request
, previous_response
=response
474 self
.logger
.debug("get_network_list Return: net_list %s", net_list
)
477 except Exception as e
:
478 self
.logger
.error("Error in get_network_list()", exc_info
=True)
479 raise vimconn
.VimConnException(e
)
481 def get_network(self
, net_id
):
482 self
.logger
.debug("get_network begin: net_id %s", net_id
)
483 # res_name = self._get_resource_name_from_resource_id(net_id)
484 self
._reload
_connection
()
486 self
.logger
.debug("Get network: %s", net_id
)
487 filter_dict
= {"name": net_id
}
488 network_list
= self
.get_network_list(filter_dict
)
489 self
.logger
.debug("Network list: %s", network_list
)
495 "get_network Return: network_list[0] %s", network_list
[0]
497 return network_list
[0]
499 def delete_network(self
, net_id
, created_items
=None):
501 Removes a tenant network from VIM and its associated elements
502 :param net_id: VIM identifier of the network, provided by method new_network
503 :param created_items: dictionary with extra items to be deleted. provided by method new_network
504 Returns the network identifier or raises an exception upon error or when network is not found
508 "delete_network begin: net_id %s created_items %s",
512 self
.logger
.debug("Deleting network: {}".format(str(net_id
)))
516 net_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
518 # Check associated VMs
520 self
.conn_compute
.instances()
521 .list(project
=self
.project
, zone
=self
.zone
)
525 net_id
= self
.delete_subnet(net_name
, created_items
)
527 self
.logger
.debug("delete_network Return: net_id %s", net_id
)
530 except Exception as e
:
531 self
.logger
.error("Error in delete_network()", exc_info
=True)
532 raise vimconn
.VimConnException(e
)
534 def delete_subnet(self
, net_id
, created_items
=None):
536 Removes a tenant network from VIM and its associated elements
537 :param net_id: VIM identifier of the network, provided by method new_network
538 :param created_items: dictionary with extra items to be deleted. provided by method new_network
539 Returns the network identifier or raises an exception upon error or when network is not found
543 "delete_subnet begin: net_id %s created_items %s",
547 self
.logger
.debug("Deleting subnetwork: {}".format(str(net_id
)))
550 # If the network has no more subnets, it will be deleted too
551 net_info
= self
.get_network(net_id
)
552 # If the subnet is in use by another resource, the deletion will be retried N times before abort the operation
553 created_items
= created_items
or {}
554 created_items
[net_id
] = False
558 self
.conn_compute
.subnetworks()
560 project
=self
.project
,
566 self
._wait
_for
_region
_operation
(operation
["name"])
567 if net_id
in self
.nets_to_be_deleted
:
568 self
.nets_to_be_deleted
.remove(net_id
)
569 except Exception as e
:
571 e
.args
[0]["status"] == "400"
572 ): # Resource in use, so the net is marked to be deleted
573 self
.logger
.debug("Subnet still in use")
574 self
.nets_to_be_deleted
.append(net_id
)
576 raise vimconn
.VimConnException(e
)
578 self
.logger
.debug("nets_to_be_deleted: %s", self
.nets_to_be_deleted
)
580 # If the network has no more subnets, it will be deleted too
581 # if "network" in net_info and net_id not in self.nets_to_be_deleted:
582 if "network" in net_info
:
583 network_name
= self
._get
_resource
_name
_from
_resource
_id
(
588 # Deletion of the associated firewall rules:
589 rules_list
= self
._delete
_firewall
_rules
(network_name
)
592 self
.conn_compute
.networks()
594 project
=self
.project
,
595 network
=network_name
,
599 self
._wait
_for
_global
_operation
(operation
["name"])
600 except Exception as e
:
601 self
.logger
.debug("error deleting associated network %s", e
)
603 self
.logger
.debug("delete_subnet Return: net_id %s", net_id
)
606 except Exception as e
:
607 self
.logger
.error("Error in delete_network()", exc_info
=True)
608 raise vimconn
.VimConnException(e
)
610 def new_flavor(self
, flavor_data
):
612 It is not allowed to create new flavors (machine types) in Google Cloud, must always use an existing one
614 raise vimconn
.VimConnNotImplemented(
615 "It is not possible to create new flavors in Google Cloud"
618 def new_tenant(self
, tenant_name
, tenant_description
):
620 It is not allowed to create new tenants in Google Cloud
622 raise vimconn
.VimConnNotImplemented(
623 "It is not possible to create a TENANT in Google Cloud"
626 def get_flavor(self
, flavor_id
):
628 Obtains the flavor_data from the flavor_id/machine type id
630 self
.logger
.debug("get_flavor begin: flavor_id %s", flavor_id
)
634 self
.conn_compute
.machineTypes()
635 .get(project
=self
.project
, zone
=self
.zone
, machineType
=flavor_id
)
638 flavor_data
= response
639 self
.logger
.debug("Machine type data: %s", flavor_data
)
643 "id": flavor_data
["id"],
645 "id_complete": flavor_data
["selfLink"],
646 "ram": flavor_data
["memoryMb"],
647 "vcpus": flavor_data
["guestCpus"],
648 "disk": flavor_data
["maximumPersistentDisksSizeGb"],
651 self
.logger
.debug("get_flavor Return: flavor %s", flavor
)
654 raise vimconn
.VimConnNotFoundException(
655 "flavor '{}' not found".format(flavor_id
)
657 except Exception as e
:
658 self
._format
_vimconn
_exception
(e
)
660 # Google Cloud VM names can not have some special characters
661 def _check_vm_name(self
, vm_name
):
663 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
664 Only lowercase and hyphens are allowed
666 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
668 # First: the VM name max length is 64 characters
669 vm_name_aux
= vm_name
[:62]
671 # Second: replace not allowed characters
672 for elem
in chars_not_allowed_list
:
673 # Check if string is in the main string
674 if elem
in vm_name_aux
:
675 # self.logger.debug("Dentro del IF")
677 vm_name_aux
= vm_name_aux
.replace(elem
, "-")
679 return vm_name_aux
.lower()
681 def get_flavor_id_from_data(self
, flavor_dict
):
683 "get_flavor_id_from_data begin: flavor_dict %s", flavor_dict
685 filter_dict
= flavor_dict
or {}
689 self
.conn_compute
.machineTypes()
690 .list(project
=self
.project
, zone
=self
.zone
)
693 machine_types_list
= response
["items"]
694 # self.logger.debug("List of machine types: %s", machine_types_list)
696 cpus
= filter_dict
.get("vcpus") or 0
697 memMB
= filter_dict
.get("ram") or 0
698 numberInterfaces
= len(filter_dict
.get("interfaces", [])) or 4 # Workaround (it should be 0)
701 filtered_machines
= []
702 for machine_type
in machine_types_list
:
704 machine_type
["guestCpus"] >= cpus
705 and machine_type
["memoryMb"] >= memMB
706 # In Google Cloud the number of virtual network interfaces scales with
707 # the number of virtual CPUs with a minimum of 2 and a maximum of 8:
708 # https://cloud.google.com/vpc/docs/create-use-multiple-interfaces#max-interfaces
709 and machine_type
["guestCpus"] >= numberInterfaces
711 filtered_machines
.append(machine_type
)
713 # self.logger.debug("Filtered machines: %s", filtered_machines)
716 listedFilteredMachines
= sorted(
720 float(k
["memoryMb"]),
721 int(k
["maximumPersistentDisksSizeGb"]),
725 # self.logger.debug("Sorted filtered machines: %s", listedFilteredMachines)
727 if listedFilteredMachines
:
729 "get_flavor_id_from_data Return: listedFilteredMachines[0][name] %s",
730 listedFilteredMachines
[0]["name"],
732 return listedFilteredMachines
[0]["name"]
734 raise vimconn
.VimConnNotFoundException(
735 "Cannot find any flavor matching '{}'".format(str(flavor_dict
))
738 except Exception as e
:
739 self
._format
_vimconn
_exception
(e
)
741 def delete_flavor(self
, flavor_id
):
742 raise vimconn
.VimConnNotImplemented(
743 "It is not possible to delete a flavor in Google Cloud"
746 def delete_tenant(self
, tenant_id
):
747 raise vimconn
.VimConnNotImplemented(
748 "It is not possible to delete a TENANT in Google Cloud"
751 def new_image(self
, image_dict
):
753 This function comes from the early days when we though the image could be embedded in the package.
754 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
756 raise vimconn
.VimConnNotImplemented("Not implemented")
758 def get_image_id_from_path(self
, path
):
760 This function comes from the early days when we though the image could be embedded in the package.
761 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
763 raise vimconn
.VimConnNotImplemented("Not implemented")
765 def get_image_list(self
, filter_dict
={}):
766 """Obtain tenant images from VIM
768 name: image name with the format: image project:image family:image version
769 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
770 for the provided publisher and offer
771 id: image uuid, currently not supported for azure
772 Returns the image list of dictionaries:
773 [{<the fields at Filter_dict plus some VIM specific>}, ...]
776 self
.logger
.debug("get_image_list begin: filter_dict %s", filter_dict
)
780 # Get image id from parameter image_id:
781 # <image Project>:image-family:<family> => Latest version of the family
782 # <image Project>:image:<image> => Specific image
783 # <image Project>:<image> => Specific image
785 image_info
= filter_dict
["name"].split(":")
786 image_project
= image_info
[0]
787 if len(image_info
) == 2:
789 image_item
= image_info
[1]
790 if len(image_info
) == 3:
791 image_type
= image_info
[1]
792 image_item
= image_info
[2]
794 raise vimconn
.VimConnNotFoundException("Wrong format for image")
797 if image_type
== "image-family":
799 self
.conn_compute
.images()
800 .getFromFamily(project
=image_project
, family
=image_item
)
803 elif image_type
== "image":
805 self
.conn_compute
.images()
806 .get(project
=image_project
, image
=image_item
)
810 raise vimconn
.VimConnNotFoundException("Wrong format for image")
813 "id": "projects/%s/global/images/%s"
814 % (image_project
, image_response
["name"]),
816 [image_project
, image_item
, image_response
["name"]]
821 self
.logger
.debug("get_image_list Return: image_list %s", image_list
)
824 except Exception as e
:
825 self
._format
_vimconn
_exception
(e
)
827 def delete_inuse_nic(self
, nic_name
):
828 raise vimconn
.VimConnNotImplemented("Not necessary")
830 def delete_image(self
, image_id
):
831 raise vimconn
.VimConnNotImplemented("Not implemented")
833 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
834 """Send and action over a VM instance from VIM
835 Returns the vm_id if the action was successfully sent to the VIM
837 raise vimconn
.VimConnNotImplemented("Not necessary")
839 def _randomize_name(self
, name
):
840 """Adds a random string to allow requests with the same VM name
841 Returns the name with an additional random string (if the total size is bigger
842 than 62 the original name will be truncated)
851 + "".join(random_choice("0123456789abcdef") for _
in range(12))
854 self
.conn_compute
.instances()
855 .get(project
=self
.project
, zone
=self
.zone
, instance
=random_name
)
858 # If no exception is arisen, the random name exists for an instance, so a new random name must be generated
860 except Exception as e
:
861 if e
.args
[0]["status"] == "404":
862 self
.logger
.debug("New random name: %s", random_name
)
865 self
.logger
.error("Exception generating random name (%s) for the instance", name
)
866 self
._format
_vimconn
_exception
(e
)
875 image_id
=None, # <image project>:(image|image-family):<image/family id>
880 availability_zone_index
=None,
881 availability_zone_list
=None,
884 "new_vminstance begin: name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
885 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
892 availability_zone_index
,
893 availability_zone_list
,
896 if self
.reload_client
:
897 self
._reload
_connection
()
899 # Validate input data is valid
900 # # First of all, the name must be adapted because Google Cloud only allows names consist of
901 # lowercase letters (a-z), numbers and hyphens (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
902 vm_name
= self
._check
_vm
_name
(name
)
903 vm_name
= self
._randomize
_name
(vm_name
)
906 # At least one network must be provided
908 raise vimconn
.VimConnException(
909 "At least one net must be provided to create a new VM"
914 metadata
= self
._build
_metadata
(vm_name
, cloud_config
)
916 # Building network interfaces list
917 network_interfaces
= []
920 if not net
.get("net_id"):
921 if not net
.get("name"):
926 ] = "regions/%s/subnetworks/" % self
.region
+ net
.get("name")
928 net_iface
["subnetwork"] = net
.get("net_id")
929 # In order to get an external IP address, the key "accessConfigs" must be used
930 # in the interace. It has to be of type "ONE_TO_ONE_NAT" and name "External NAT"
931 if net
.get("floating_ip", False) or (net
["use"] == "mgmt" and self
.config
.get("use_floating_ip")):
932 net_iface
["accessConfigs"] = [
933 {"type": "ONE_TO_ONE_NAT", "name": "External NAT"}
936 network_interfaces
.append(net_iface
)
938 self
.logger
.debug("Network interfaces: %s", network_interfaces
)
940 self
.logger
.debug("Source image: %s", image_id
)
944 "machineType": self
.get_flavor(flavor_id
)["id_complete"],
945 # Specify the boot disk and the image to use as a source.
950 "initializeParams": {
951 "sourceImage": image_id
,
955 # Specify the network interfaces
956 "networkInterfaces": network_interfaces
,
957 "metadata": metadata
,
961 self
.conn_compute
.instances()
962 .insert(project
=self
.project
, zone
=self
.zone
, body
=vm_parameters
)
965 self
._wait
_for
_zone
_operation
(response
["name"])
967 # The created instance info is obtained to get the name of the generated network interfaces (nic0, nic1...)
969 self
.conn_compute
.instances()
970 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_name
)
973 self
.logger
.debug("instance get: %s", response
)
974 vm_id
= response
["name"]
976 # The generated network interfaces in the instance are include in net_list:
977 for _
, net
in enumerate(net_list
):
978 for net_ifaces
in response
["networkInterfaces"]:
981 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
985 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
988 if network_id
== self
._get
_resource
_name
_from
_resource
_id
(
989 net_ifaces
["subnetwork"]
991 net
["vim_id"] = net_ifaces
["name"]
994 "new_vminstance Return: (name %s, created_items %s)",
998 return vm_name
, created_items
1000 except Exception as e
:
1001 # Rollback vm creacion
1002 if vm_id
is not None:
1004 self
.logger
.debug("exception creating vm try to rollback")
1005 self
.delete_vminstance(vm_id
, created_items
)
1006 except Exception as e2
:
1007 self
.logger
.error("new_vminstance rollback fail {}".format(e2
))
1010 self
.logger
.debug("Exception creating new vminstance: %s", e
, exc_info
=True)
1011 self
._format
_vimconn
_exception
(e
)
1014 def _build_metadata(self
, vm_name
, cloud_config
):
1018 metadata
["items"] = []
1021 # if there is a cloud-init load it
1023 self
.logger
.debug("cloud config: %s", cloud_config
)
1024 _
, userdata
= self
._create
_user
_data
(cloud_config
)
1025 metadata
["items"].append(
1026 {"key": "user-data", "value": userdata
}
1029 # either password of ssh-keys are required
1030 # we will always use ssh-keys, in case it is not available we will generate it
1032 if cloud_config and cloud_config.get("key-pairs"):
1035 if cloud_config.get("key-pairs"):
1036 if isinstance(cloud_config["key-pairs"], list):
1037 # Transform the format "<key> <user@host>" into "<user>:<key>"
1039 for key in cloud_config.get("key-pairs"):
1040 key_data = key_data + key + "\n"
1046 # If there is no ssh key in cloud config, a new key is generated:
1047 _, key_data = self._generate_keys()
1050 "value": "" + key_data
1052 self.logger.debug("generated keys: %s", key_data)
1054 metadata["items"].append(key_pairs)
1056 self
.logger
.debug("metadata: %s", metadata
)
1061 def _generate_keys(self
):
1062 """Method used to generate a pair of private/public keys.
1063 This method is used because to create a vm in Azure we always need a key or a password
1064 In some cases we may have a password in a cloud-init file but it may not be available
1066 key
= rsa
.generate_private_key(
1067 backend
=crypto_default_backend(), public_exponent
=65537, key_size
=2048
1069 private_key
= key
.private_bytes(
1070 crypto_serialization
.Encoding
.PEM
,
1071 crypto_serialization
.PrivateFormat
.PKCS8
,
1072 crypto_serialization
.NoEncryption(),
1074 public_key
= key
.public_key().public_bytes(
1075 crypto_serialization
.Encoding
.OpenSSH
,
1076 crypto_serialization
.PublicFormat
.OpenSSH
,
1078 private_key
= private_key
.decode("utf8")
1079 # Change first line because Paramiko needs a explicit start with 'BEGIN RSA PRIVATE KEY'
1080 i
= private_key
.find("\n")
1081 private_key
= "-----BEGIN RSA PRIVATE KEY-----" + private_key
[i
:]
1082 public_key
= public_key
.decode("utf8")
1084 return private_key
, public_key
1087 def _get_unused_vm_name(self
, vm_name
):
1089 Checks the vm name and in case it is used adds a suffix to the name to allow creation
1093 self
.conn_compute
.instances()
1094 .list(project
=self
.project
, zone
=self
.zone
)
1097 # Filter to vms starting with the indicated name
1098 vms
= list(filter(lambda vm
: (vm
.name
.startswith(vm_name
)), all_vms
))
1099 vm_names
= [str(vm
.name
) for vm
in vms
]
1101 # get the name with the first not used suffix
1103 # name = subnet_name + "-" + str(name_suffix)
1104 name
= vm_name
# first subnet created will have no prefix
1106 while name
in vm_names
:
1108 name
= vm_name
+ "-" + str(name_suffix
)
1112 def get_vminstance(self
, vm_id
):
1114 Obtaing the vm instance data from v_id
1116 self
.logger
.debug("get_vminstance begin: vm_id %s", vm_id
)
1117 self
._reload
_connection
()
1121 self
.conn_compute
.instances()
1122 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1125 # vm = response["source"]
1126 except Exception as e
:
1127 self
._format
_vimconn
_exception
(e
)
1129 self
.logger
.debug("get_vminstance Return: response %s", response
)
1132 def delete_vminstance(self
, vm_id
, created_items
=None):
1133 """Deletes a vm instance from the vim."""
1135 "delete_vminstance begin: vm_id %s created_items %s",
1139 if self
.reload_client
:
1140 self
._reload
_connection
()
1142 created_items
= created_items
or {}
1144 vm
= self
.get_vminstance(vm_id
)
1147 self
.conn_compute
.instances()
1148 .delete(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1151 self
._wait
_for
_zone
_operation
(operation
["name"])
1153 # The associated subnets must be checked if they are marked to be deleted
1154 for netIface
in vm
["networkInterfaces"]:
1156 self
._get
_resource
_name
_from
_resource
_id
(netIface
["subnetwork"])
1157 in self
.nets_to_be_deleted
1159 net_id
= self
._get
_resource
_name
_from
_resource
_id
(
1160 self
.delete_network(netIface
["subnetwork"])
1163 self
.logger
.debug("delete_vminstance end")
1165 except Exception as e
:
1166 # The VM can be deleted previously during network deletion
1167 if e
.args
[0]["status"] == "404":
1168 self
.logger
.debug("The VM doesn't exist or has been deleted")
1170 self
._format
_vimconn
_exception
(e
)
1172 def _get_net_name_from_resource_id(self
, resource_id
):
1174 net_name
= str(resource_id
.split("/")[-1])
1178 raise vimconn
.VimConnException(
1179 "Unable to get google cloud net_name from invalid resource_id format '{}'".format(
1184 def _get_resource_name_from_resource_id(self
, resource_id
):
1186 Obtains resource_name from the google cloud complete identifier: resource_name will always be last item
1189 "_get_resource_name_from_resource_id begin: resource_id %s",
1193 resource
= str(resource_id
.split("/")[-1])
1196 "_get_resource_name_from_resource_id Return: resource %s",
1200 except Exception as e
:
1201 raise vimconn
.VimConnException(
1202 "Unable to get resource name from resource_id '{}' Error: '{}'".format(
1207 def refresh_nets_status(self
, net_list
):
1208 """Get the status of the networks
1209 Params: the list of network identifiers
1210 Returns a dictionary with:
1211 net_id: #VIM id of this network
1212 status: #Mandatory. Text with one of:
1213 # DELETED (not found at vim)
1214 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1215 # OTHER (Vim reported other status not understood)
1216 # ERROR (VIM indicates an ERROR status)
1217 # ACTIVE, INACTIVE, DOWN (admin down),
1218 # BUILD (on building process)
1220 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1221 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1223 self
.logger
.debug("refresh_nets_status begin: net_list %s", net_list
)
1225 self
._reload
_connection
()
1227 for net_id
in net_list
:
1229 netName
= self
._get
_net
_name
_from
_resource
_id
(net_id
)
1230 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1233 self
.conn_compute
.subnetworks()
1234 .get(project
=self
.project
, region
=self
.region
, subnetwork
=resName
)
1237 self
.logger
.debug("get subnetwork: %s", net
)
1239 out_nets
[net_id
] = {
1240 "status": "ACTIVE", # Google Cloud does not provide the status in subnetworks getting
1241 "vim_info": str(net
),
1243 except vimconn
.VimConnNotFoundException
as e
:
1245 "VimConnNotFoundException %s when searching subnet", e
1247 out_nets
[net_id
] = {
1248 "status": "DELETED",
1249 "error_msg": str(e
),
1251 except Exception as e
:
1253 "Exception %s when searching subnet", e
, exc_info
=True
1255 out_nets
[net_id
] = {
1256 "status": "VIM_ERROR",
1257 "error_msg": str(e
),
1260 self
.logger
.debug("refresh_nets_status Return: out_nets %s", out_nets
)
1263 def refresh_vms_status(self
, vm_list
):
1264 """Get the status of the virtual machines and their interfaces/ports
1265 Params: the list of VM identifiers
1266 Returns a dictionary with:
1267 vm_id: # VIM id of this Virtual Machine
1268 status: # Mandatory. Text with one of:
1269 # DELETED (not found at vim)
1270 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1271 # OTHER (Vim reported other status not understood)
1272 # ERROR (VIM indicates an ERROR status)
1273 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1274 # BUILD (on building process), ERROR
1275 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1276 # (ACTIVE:NoMgmtIP is not returned for Azure)
1278 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1279 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1280 interfaces: list with interface info. Each item a dictionary with:
1281 vim_interface_id - The ID of the interface
1282 mac_address - The MAC address of the interface.
1283 ip_address - The IP address of the interface within the subnet.
1285 self
.logger
.debug("refresh_vms_status begin: vm_list %s", vm_list
)
1287 self
._reload
_connection
()
1289 search_vm_list
= vm_list
or {}
1291 for vm_id
in search_vm_list
:
1294 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1297 self
.conn_compute
.instances()
1298 .get(project
=self
.project
, zone
=self
.zone
, instance
=res_name
)
1302 out_vm
["vim_info"] = str(vm
["name"])
1303 out_vm
["status"] = self
.provision_state2osm
.get(vm
["status"], "OTHER")
1305 # In Google Cloud the there is no difference between provision or power status,
1306 # so if provision status method doesn't return a specific state (OTHER), the
1307 # power method is called
1308 if out_vm
["status"] == "OTHER":
1309 out_vm
["status"] = self
.power_state2osm
.get(vm
["status"], "OTHER")
1311 network_interfaces
= vm
["networkInterfaces"]
1312 out_vm
["interfaces"] = self
._get
_vm
_interfaces
_status
(
1313 vm_id
, network_interfaces
1315 except Exception as e
:
1316 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1317 out_vm
["status"] = "VIM_ERROR"
1318 out_vm
["error_msg"] = str(e
)
1319 out_vm
["vim_info"] = None
1321 out_vms
[vm_id
] = out_vm
1323 self
.logger
.debug("refresh_vms_status Return: out_vms %s", out_vms
)
1326 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1328 Gets the interfaces detail for a vm
1329 :param interfaces: List of interfaces.
1330 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1333 "_get_vm_interfaces_status begin: vm_id %s interfaces %s",
1339 for network_interface
in interfaces
:
1341 nic_name
= network_interface
["name"]
1342 interface_dict
["vim_interface_id"] = network_interface
["name"]
1345 ips
.append(network_interface
["networkIP"])
1346 interface_dict
["ip_address"] = ";".join(ips
)
1347 interface_list
.append(interface_dict
)
1350 "_get_vm_interfaces_status Return: interface_list %s",
1353 return interface_list
1354 except Exception as e
:
1356 "Exception %s obtaining interface data for vm: %s",
1361 self
._format
_vimconn
_exception
(e
)
1363 def _get_default_admin_user(self
, image_id
):
1364 if "ubuntu" in image_id
.lower():
1367 return self
._default
_admin
_user
1369 def _create_firewall_rules(self
, network
):
1371 Creates the necessary firewall rules to allow the traffic in the network
1372 (https://cloud.google.com/vpc/docs/firewalls)
1374 :return: a list with the names of the firewall rules
1376 self
.logger
.debug("_create_firewall_rules begin: network %s", network
)
1380 # Adding firewall rule to allow http:
1381 self
.logger
.debug("creating firewall rule to allow http")
1382 firewall_rule_body
= {
1383 "name": "fw-rule-http-" + network
,
1384 "network": "global/networks/" + network
,
1385 "allowed": [{"IPProtocol": "tcp", "ports": ["80"]}],
1387 operation_firewall
= (
1388 self
.conn_compute
.firewalls()
1389 .insert(project
=self
.project
, body
=firewall_rule_body
)
1393 # Adding firewall rule to allow ssh:
1394 self
.logger
.debug("creating firewall rule to allow ssh")
1395 firewall_rule_body
= {
1396 "name": "fw-rule-ssh-" + network
,
1397 "network": "global/networks/" + network
,
1398 "allowed": [{"IPProtocol": "tcp", "ports": ["22"]}],
1400 operation_firewall
= (
1401 self
.conn_compute
.firewalls()
1402 .insert(project
=self
.project
, body
=firewall_rule_body
)
1406 # Adding firewall rule to allow ping:
1407 self
.logger
.debug("creating firewall rule to allow ping")
1408 firewall_rule_body
= {
1409 "name": "fw-rule-icmp-" + network
,
1410 "network": "global/networks/" + network
,
1411 "allowed": [{"IPProtocol": "icmp"}],
1413 operation_firewall
= (
1414 self
.conn_compute
.firewalls()
1415 .insert(project
=self
.project
, body
=firewall_rule_body
)
1419 # Adding firewall rule to allow internal:
1420 self
.logger
.debug("creating firewall rule to allow internal")
1421 firewall_rule_body
= {
1422 "name": "fw-rule-internal-" + network
,
1423 "network": "global/networks/" + network
,
1425 {"IPProtocol": "tcp", "ports": ["0-65535"]},
1426 {"IPProtocol": "udp", "ports": ["0-65535"]},
1427 {"IPProtocol": "icmp"},
1430 operation_firewall
= (
1431 self
.conn_compute
.firewalls()
1432 .insert(project
=self
.project
, body
=firewall_rule_body
)
1436 # Adding firewall rule to allow microk8s:
1437 self
.logger
.debug("creating firewall rule to allow microk8s")
1438 firewall_rule_body
= {
1439 "name": "fw-rule-microk8s-" + network
,
1440 "network": "global/networks/" + network
,
1441 "allowed": [{"IPProtocol": "tcp", "ports": ["16443"]}],
1443 operation_firewall
= (
1444 self
.conn_compute
.firewalls()
1445 .insert(project
=self
.project
, body
=firewall_rule_body
)
1449 # Adding firewall rule to allow rdp:
1450 self
.logger
.debug("creating firewall rule to allow rdp")
1451 firewall_rule_body
= {
1452 "name": "fw-rule-rdp-" + network
,
1453 "network": "global/networks/" + network
,
1454 "allowed": [{"IPProtocol": "tcp", "ports": ["3389"]}],
1456 operation_firewall
= (
1457 self
.conn_compute
.firewalls()
1458 .insert(project
=self
.project
, body
=firewall_rule_body
)
1462 # Adding firewall rule to allow osm:
1463 self
.logger
.debug("creating firewall rule to allow osm")
1464 firewall_rule_body
= {
1465 "name": "fw-rule-osm-" + network
,
1466 "network": "global/networks/" + network
,
1467 "allowed": [{"IPProtocol": "tcp", "ports": ["9001", "9999"]}],
1469 operation_firewall
= (
1470 self
.conn_compute
.firewalls()
1471 .insert(project
=self
.project
, body
=firewall_rule_body
)
1476 "_create_firewall_rules Return: list_rules %s", rules_list
1479 except Exception as e
:
1481 "Unable to create google cloud firewall rules for network '{}'".format(
1485 self
._format
_vimconn
_exception
(e
)
1487 def _delete_firewall_rules(self
, network
):
1489 Deletes the associated firewall rules to the network
1491 :return: a list with the names of the firewall rules
1493 self
.logger
.debug("_delete_firewall_rules begin: network %s", network
)
1498 self
.conn_compute
.firewalls().list(project
=self
.project
).execute()
1500 for item
in rules_list
["items"]:
1501 if network
== self
._get
_resource
_name
_from
_resource
_id
(item
["network"]):
1502 operation_firewall
= (
1503 self
.conn_compute
.firewalls()
1504 .delete(project
=self
.project
, firewall
=item
["name"])
1508 self
.logger
.debug("_delete_firewall_rules Return: list_rules %s", 0)
1510 except Exception as e
:
1512 "Unable to delete google cloud firewall rules for network '{}'".format(
1516 self
._format
_vimconn
_exception
(e
)