e6823dcb587585c0d4cbf71b41e7e0df5befef8e
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 cryptography
.hazmat
.backends
import default_backend
as crypto_default_backend
23 from cryptography
.hazmat
.primitives
import serialization
as crypto_serialization
24 from cryptography
.hazmat
.primitives
.asymmetric
import rsa
25 from google
.oauth2
import service_account
26 import googleapiclient
.discovery
27 from osm_ro_plugin
import vimconn
29 __author__
= "Sergio Gallardo Ruiz"
30 __date__
= "$11-aug-2021 08:30:00$"
33 if getenv("OSMRO_PDB_DEBUG"):
42 class vimconnector(vimconn
.VimConnector
):
44 # Translate Google Cloud provisioning state to OSM provision state
45 # The first three ones are the transitional status once a user initiated action has been requested
46 # Once the operation is complete, it will transition into the states Succeeded or Failed
47 # https://cloud.google.com/compute/docs/instances/instance-life-cycle
48 provision_state2osm
= {
49 "PROVISIONING": "BUILD",
53 # Translate azure power state to OSM provision state
57 "STOPPING": "INACTIVE",
58 "SUSPENDING": "INACTIVE",
59 "SUSPENDED": "INACTIVE",
60 "TERMINATED": "INACTIVE",
63 # If a net or subnet is tried to be deleted and it has an associated resource, the net is marked "to be deleted"
64 # (incluid it's name in the following list). When the instance is deleted, its associated net will be deleted if
65 # they are present in that list
66 nets_to_be_deleted
= []
83 Constructor of VIM. Raise an exception is some needed parameter is missing, but it must not do any connectivity
84 checking against the VIM
85 Using common constructor parameters.
86 In this case: config must include the following parameters:
87 subscription_id: assigned subscription identifier
88 region_name: current region for network
89 config may also include the following parameter:
90 flavors_pattern: pattern that will be used to select a range of vm sizes, for example
91 "^((?!Standard_B).)*$" will filter out Standard_B range that is cheap but is very overused
92 "^Standard_B" will select a serie B maybe for test environment
94 vimconn
.VimConnector
.__init
__(
109 # Variable that indicates if client must be reloaded or initialized
110 self
.reload_client
= False
114 log_format_simple
= (
115 "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s"
117 log_format_complete
= "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(funcName)s(): %(message)s"
118 log_formatter_simple
= logging
.Formatter(
119 log_format_simple
, datefmt
="%Y-%m-%dT%H:%M:%S"
121 self
.handler
= logging
.StreamHandler()
122 self
.handler
.setFormatter(log_formatter_simple
)
124 self
.logger
= logging
.getLogger("ro.vim.gcp")
125 self
.logger
.addHandler(self
.handler
)
127 self
.logger
.setLevel(getattr(logging
, log_level
))
129 if self
.logger
.getEffectiveLevel() == logging
.DEBUG
:
130 log_formatter
= logging
.Formatter(
131 log_format_complete
, datefmt
="%Y-%m-%dT%H:%M:%S"
133 self
.handler
.setFormatter(log_formatter
)
135 self
.logger
.debug("Google Cloud connection init")
137 self
.project
= tenant_id
or tenant_name
139 # REGION - Google Cloud considers regions and zones. A specific region can have more than one zone
140 # (for instance: region us-west1 with the zones us-west1-a, us-west1-b and us-west1-c)
141 # So the region name specified in the config will be considered as a specific zone for GC and
142 # the region will be calculated from that without the preffix.
143 if "region_name" in config
:
144 self
.zone
= config
.get("region_name")
145 self
.region
= self
.zone
.rsplit("-", 1)[0]
147 raise vimconn
.VimConnException(
148 "Google Cloud region_name is not specified at config"
152 self
.logger
.debug("Config: %s", config
)
153 scopes
= ["https://www.googleapis.com/auth/cloud-platform"]
154 self
.credentials
= None
155 if "credentials" in config
:
156 self
.logger
.debug("Setting credentials")
157 # Settings Google Cloud credentials dict
158 credentials_body
= config
["credentials"]
159 # self.logger.debug("Credentials filtered: %s", credentials_body)
160 credentials
= service_account
.Credentials
.from_service_account_info(
163 if "sa_file" in config
:
164 credentials
= service_account
.Credentials
.from_service_account_file(
165 config
.get("sa_file"), scopes
=scopes
167 self
.logger
.debug("Credentials: %s", credentials
)
168 # Construct a Resource for interacting with an API.
169 self
.credentials
= credentials
171 self
.conn_compute
= googleapiclient
.discovery
.build(
172 "compute", "v1", credentials
=credentials
174 except Exception as e
:
175 self
._format
_vimconn
_exception
(e
)
177 raise vimconn
.VimConnException(
178 "It is not possible to init GCP with no credentials"
181 def _reload_connection(self
):
183 Called before any operation, checks python Google Cloud clientsself.reload_client
185 if self
.reload_client
:
186 self
.logger
.debug("reloading google cloud client")
189 # Set to client created
190 self
.conn_compute
= googleapiclient
.discovery
.build("compute", "v1")
191 except Exception as e
:
192 self
._format
_vimconn
_exception
(e
)
194 def _format_vimconn_exception(self
, e
):
196 Transforms a generic exception to a vimConnException
198 self
.logger
.error("Google Cloud plugin error: {}".format(e
))
199 if isinstance(e
, vimconn
.VimConnException
):
202 # In case of generic error recreate client
203 self
.reload_client
= True
204 raise vimconn
.VimConnException(type(e
).__name
__ + ": " + str(e
))
206 def _wait_for_global_operation(self
, operation
):
208 Waits for the end of the specific operation
209 :operation: operation name
212 self
.logger
.debug("Waiting for operation %s", operation
)
216 self
.conn_compute
.globalOperations()
217 .get(project
=self
.project
, operation
=operation
)
221 if result
["status"] == "DONE":
222 if "error" in result
:
223 raise vimconn
.VimConnException(result
["error"])
228 def _wait_for_zone_operation(self
, operation
):
230 Waits for the end of the specific operation
231 :operation: operation name
234 self
.logger
.debug("Waiting for operation %s", operation
)
238 self
.conn_compute
.zoneOperations()
239 .get(project
=self
.project
, operation
=operation
, zone
=self
.zone
)
243 if result
["status"] == "DONE":
244 if "error" in result
:
245 raise vimconn
.VimConnException(result
["error"])
250 def _wait_for_region_operation(self
, operation
):
252 Waits for the end of the specific operation
253 :operation: operation name
256 self
.logger
.debug("Waiting for operation %s", operation
)
260 self
.conn_compute
.regionOperations()
261 .get(project
=self
.project
, operation
=operation
, region
=self
.region
)
265 if result
["status"] == "DONE":
266 if "error" in result
:
267 raise vimconn
.VimConnException(result
["error"])
278 provider_network_profile
=None,
281 Adds a network to VIM
282 :param net_name: name of the network
283 :param net_type: not used for Google Cloud networks
284 :param ip_profile: not used for Google Cloud networks
285 :param shared: Not allowed for Google Cloud Connector
286 :param provider_network_profile: (optional)
288 contains {segmentation-id: vlan, provider-network: vim_netowrk}
289 :return: a tuple with the network identifier and created_items, or raises an exception on error
290 created_items can be None or a dictionary where this method can include key-values that will be passed to
291 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
292 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
297 "new_network begin: net_name %s net_type %s ip_profile %s shared %s provider_network_profile %s",
302 provider_network_profile
,
304 net_name
= self
._check
_vm
_name
(net_name
)
305 net_name
= self
._randomize
_name
(net_name
)
306 self
.logger
.debug("create network name %s, ip_profile %s", net_name
, ip_profile
)
310 self
.logger
.debug("creating network_name: {}".format(net_name
))
312 network
= "projects/{}/global/networks/default".format(self
.project
)
314 if ip_profile
is not None:
315 if "subnet_address" in ip_profile
:
316 subnet_address
= ip_profile
["subnet_address"]
318 "name": str(net_name
),
319 "description": net_name
,
321 "ipCidrRange": subnet_address
,
322 # The network is created in AUTO mode (one subnet per region is created)
323 # "autoCreateSubnetworks": True,
324 "autoCreateSubnetworks": False,
328 self
.conn_compute
.networks()
329 .insert(project
=self
.project
, body
=network_body
)
332 self
._wait
_for
_global
_operation
(operation
["name"])
333 self
.logger
.debug("created network_name: {}".format(net_name
))
335 # Adding firewall rules to allow the traffic in the network:
336 self
._create
_firewall
_rules
(net_name
)
338 # create subnetwork, even if there is no profile
343 if not ip_profile
.get("subnet_address"):
344 # Fake subnet is required
345 subnet_rand
= random
.randint(0, 255)
346 ip_profile
["subnet_address"] = "192.168.{}.0/24".format(subnet_rand
)
348 subnet_name
= net_name
+ "-subnet"
349 subnet_id
= self
._new
_subnet
(
350 subnet_name
, ip_profile
, operation
["targetLink"]
353 self
.logger
.debug("new_network Return: subnet_id: %s", subnet_id
)
355 except Exception as e
:
356 self
._format
_vimconn
_exception
(e
)
358 def _new_subnet(self
, subnet_name
, ip_profile
, network
):
360 Adds a tenant network to VIM. It creates a new subnet at existing base vnet
361 :param net_name: subnet name
363 subnet-address: if it is not provided a subnet/24 in the default vnet is created,
364 otherwise it creates a subnet in the indicated address
365 :return: a tuple with the network identifier and created_items, or raises an exception on error
368 "_new_subnet begin: subnet_name %s ip_profile %s network %s",
374 "create subnet name %s, ip_profile %s", subnet_name
, ip_profile
379 self
.logger
.debug("creating subnet_name: {}".format(subnet_name
))
382 "name": str(subnet_name
),
383 "description": subnet_name
,
385 "ipCidrRange": ip_profile
["subnet_address"],
389 self
.conn_compute
.subnetworks()
391 project
=self
.project
,
393 body
=subnetwork_body
,
397 self
._wait
_for
_region
_operation
(operation
["name"])
399 self
.logger
.debug("created subnet_name: {}".format(subnet_name
))
402 "_new_subnet Return: (%s,%s)",
403 "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
),
406 return "regions/%s/subnetworks/%s" % (self
.region
, subnet_name
), None
407 except Exception as e
:
408 self
._format
_vimconn
_exception
(e
)
410 def get_network_list(self
, filter_dict
={}):
411 """Obtain tenant networks of VIM
415 shared: boolean, not implemented in GC
416 tenant_id: tenant, not used in GC, all networks same tenants
417 admin_state_up: boolean, not implemented in GC
418 status: 'ACTIVE', not implemented in GC #
419 Returns the network list of dictionaries
421 self
.logger
.debug("get_network_list begin: filter_dict %s", filter_dict
)
423 "Getting network (subnetwork) from VIM filter: {}".format(str(filter_dict
))
428 if self
.reload_client
:
429 self
._reload
_connection
()
433 request
= self
.conn_compute
.subnetworks().list(
434 project
=self
.project
, region
=self
.region
437 while request
is not None:
438 response
= request
.execute()
439 self
.logger
.debug("Network list: %s", response
)
440 for net
in response
["items"]:
441 self
.logger
.debug("Network in list: {}".format(str(net
["name"])))
442 if filter_dict
is not None:
443 if "name" in filter_dict
.keys():
445 filter_dict
["name"] == net
["name"]
446 or filter_dict
["name"] == net
["selfLink"]
448 self
.logger
.debug("Network found: %s", net
["name"])
451 "id": str(net
["selfLink"]),
452 "name": str(net
["name"]),
453 "network": str(net
["network"]),
459 "id": str(net
["selfLink"]),
460 "name": str(net
["name"]),
461 "network": str(net
["network"]),
464 request
= self
.conn_compute
.subnetworks().list_next(
465 previous_request
=request
, previous_response
=response
468 self
.logger
.debug("get_network_list Return: net_list %s", net_list
)
471 except Exception as e
:
472 self
.logger
.error("Error in get_network_list()", exc_info
=True)
473 raise vimconn
.VimConnException(e
)
475 def get_network(self
, net_id
):
476 self
.logger
.debug("get_network begin: net_id %s", net_id
)
477 # res_name = self._get_resource_name_from_resource_id(net_id)
478 self
._reload
_connection
()
480 self
.logger
.debug("Get network: %s", net_id
)
481 filter_dict
= {"name": net_id
}
482 network_list
= self
.get_network_list(filter_dict
)
483 self
.logger
.debug("Network list: %s", network_list
)
488 self
.logger
.debug("get_network Return: network_list[0] %s", network_list
[0])
489 return network_list
[0]
491 def delete_network(self
, net_id
, created_items
=None):
493 Removes a tenant network from VIM and its associated elements
494 :param net_id: VIM identifier of the network, provided by method new_network
495 :param created_items: dictionary with extra items to be deleted. provided by method new_network
496 Returns the network identifier or raises an exception upon error or when network is not found
500 "delete_network begin: net_id %s created_items %s",
504 self
.logger
.debug("Deleting network: {}".format(str(net_id
)))
508 net_name
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
510 # Check associated VMs
511 self
.conn_compute
.instances().list(
512 project
=self
.project
, zone
=self
.zone
515 net_id
= self
.delete_subnet(net_name
, created_items
)
517 self
.logger
.debug("delete_network Return: net_id %s", net_id
)
520 except Exception as e
:
521 self
.logger
.error("Error in delete_network()", exc_info
=True)
522 raise vimconn
.VimConnException(e
)
524 def delete_subnet(self
, net_id
, created_items
=None):
526 Removes a tenant network from VIM and its associated elements
527 :param net_id: VIM identifier of the network, provided by method new_network
528 :param created_items: dictionary with extra items to be deleted. provided by method new_network
529 Returns the network identifier or raises an exception upon error or when network is not found
533 "delete_subnet begin: net_id %s created_items %s",
537 self
.logger
.debug("Deleting subnetwork: {}".format(str(net_id
)))
540 # If the network has no more subnets, it will be deleted too
541 net_info
= self
.get_network(net_id
)
542 # If the subnet is in use by another resource, the deletion will
543 # be retried N times before abort the operation
544 created_items
= created_items
or {}
545 created_items
[net_id
] = False
549 self
.conn_compute
.subnetworks()
551 project
=self
.project
,
557 self
._wait
_for
_region
_operation
(operation
["name"])
558 if net_id
in self
.nets_to_be_deleted
:
559 self
.nets_to_be_deleted
.remove(net_id
)
560 except Exception as e
:
562 e
.args
[0]["status"] == "400"
563 ): # Resource in use, so the net is marked to be deleted
564 self
.logger
.debug("Subnet still in use")
565 self
.nets_to_be_deleted
.append(net_id
)
567 raise vimconn
.VimConnException(e
)
569 self
.logger
.debug("nets_to_be_deleted: %s", self
.nets_to_be_deleted
)
571 # If the network has no more subnets, it will be deleted too
572 # if "network" in net_info and net_id not in self.nets_to_be_deleted:
573 if "network" in net_info
:
574 network_name
= self
._get
_resource
_name
_from
_resource
_id
(
579 # Deletion of the associated firewall rules:
580 self
._delete
_firewall
_rules
(network_name
)
583 self
.conn_compute
.networks()
585 project
=self
.project
,
586 network
=network_name
,
590 self
._wait
_for
_global
_operation
(operation
["name"])
591 except Exception as e
:
592 self
.logger
.debug("error deleting associated network %s", e
)
594 self
.logger
.debug("delete_subnet Return: net_id %s", net_id
)
597 except Exception as e
:
598 self
.logger
.error("Error in delete_network()", exc_info
=True)
599 raise vimconn
.VimConnException(e
)
601 def new_flavor(self
, flavor_data
):
603 It is not allowed to create new flavors (machine types) in Google Cloud, must always use an existing one
605 raise vimconn
.VimConnNotImplemented(
606 "It is not possible to create new flavors in Google Cloud"
609 def new_tenant(self
, tenant_name
, tenant_description
):
611 It is not allowed to create new tenants in Google Cloud
613 raise vimconn
.VimConnNotImplemented(
614 "It is not possible to create a TENANT in Google Cloud"
617 def get_flavor(self
, flavor_id
):
619 Obtains the flavor_data from the flavor_id/machine type id
621 self
.logger
.debug("get_flavor begin: flavor_id %s", flavor_id
)
625 self
.conn_compute
.machineTypes()
626 .get(project
=self
.project
, zone
=self
.zone
, machineType
=flavor_id
)
629 flavor_data
= response
630 self
.logger
.debug("Machine type data: %s", flavor_data
)
634 "id": flavor_data
["id"],
636 "id_complete": flavor_data
["selfLink"],
637 "ram": flavor_data
["memoryMb"],
638 "vcpus": flavor_data
["guestCpus"],
639 "disk": flavor_data
["maximumPersistentDisksSizeGb"],
642 self
.logger
.debug("get_flavor Return: flavor %s", flavor
)
645 raise vimconn
.VimConnNotFoundException(
646 "flavor '{}' not found".format(flavor_id
)
648 except Exception as e
:
649 self
._format
_vimconn
_exception
(e
)
651 # Google Cloud VM names can not have some special characters
652 def _check_vm_name(self
, vm_name
):
654 Checks vm name, in case the vm has not allowed characters they are removed, not error raised
655 Only lowercase and hyphens are allowed
657 chars_not_allowed_list
= "~!@#$%^&*()=+_[]{}|;:<>/?."
659 # First: the VM name max length is 64 characters
660 vm_name_aux
= vm_name
[:62]
662 # Second: replace not allowed characters
663 for elem
in chars_not_allowed_list
:
664 # Check if string is in the main string
665 if elem
in vm_name_aux
:
666 # self.logger.debug("Dentro del IF")
668 vm_name_aux
= vm_name_aux
.replace(elem
, "-")
670 return vm_name_aux
.lower()
672 def get_flavor_id_from_data(self
, flavor_dict
):
673 self
.logger
.debug("get_flavor_id_from_data begin: flavor_dict %s", flavor_dict
)
674 filter_dict
= flavor_dict
or {}
678 self
.conn_compute
.machineTypes()
679 .list(project
=self
.project
, zone
=self
.zone
)
682 machine_types_list
= response
["items"]
683 # self.logger.debug("List of machine types: %s", machine_types_list)
685 cpus
= filter_dict
.get("vcpus") or 0
686 memMB
= filter_dict
.get("ram") or 0
687 # Workaround (it should be 0)
688 numberInterfaces
= len(filter_dict
.get("interfaces", [])) or 4
691 filtered_machines
= []
692 for machine_type
in machine_types_list
:
694 machine_type
["guestCpus"] >= cpus
695 and machine_type
["memoryMb"] >= memMB
696 # In Google Cloud the number of virtual network interfaces scales with
697 # the number of virtual CPUs with a minimum of 2 and a maximum of 8:
698 # https://cloud.google.com/vpc/docs/create-use-multiple-interfaces#max-interfaces
699 and machine_type
["guestCpus"] >= numberInterfaces
701 filtered_machines
.append(machine_type
)
703 # self.logger.debug("Filtered machines: %s", filtered_machines)
706 listedFilteredMachines
= sorted(
710 float(k
["memoryMb"]),
711 int(k
["maximumPersistentDisksSizeGb"]),
715 # self.logger.debug("Sorted filtered machines: %s", listedFilteredMachines)
717 if listedFilteredMachines
:
719 "get_flavor_id_from_data Return: listedFilteredMachines[0][name] %s",
720 listedFilteredMachines
[0]["name"],
722 return listedFilteredMachines
[0]["name"]
724 raise vimconn
.VimConnNotFoundException(
725 "Cannot find any flavor matching '{}'".format(str(flavor_dict
))
728 except Exception as e
:
729 self
._format
_vimconn
_exception
(e
)
731 def delete_flavor(self
, flavor_id
):
732 raise vimconn
.VimConnNotImplemented(
733 "It is not possible to delete a flavor in Google Cloud"
736 def delete_tenant(self
, tenant_id
):
737 raise vimconn
.VimConnNotImplemented(
738 "It is not possible to delete a TENANT in Google Cloud"
741 def new_image(self
, image_dict
):
743 This function comes from the early days when we though the image could be embedded in the package.
744 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
746 raise vimconn
.VimConnNotImplemented("Not implemented")
748 def get_image_id_from_path(self
, path
):
750 This function comes from the early days when we though the image could be embedded in the package.
751 Unless OSM manages VM images E2E from NBI to RO, this function does not make sense to be implemented.
753 raise vimconn
.VimConnNotImplemented("Not implemented")
755 def get_image_list(self
, filter_dict
={}):
756 """Obtain tenant images from VIM
758 name: image name with the format: image project:image family:image version
759 If some part of the name is provide ex: publisher:offer it will search all availables skus and version
760 for the provided publisher and offer
761 id: image uuid, currently not supported for azure
762 Returns the image list of dictionaries:
763 [{<the fields at Filter_dict plus some VIM specific>}, ...]
766 self
.logger
.debug("get_image_list begin: filter_dict %s", filter_dict
)
770 # Get image id from parameter image_id:
771 # <image Project>:image-family:<family> => Latest version of the family
772 # <image Project>:image:<image> => Specific image
773 # <image Project>:<image> => Specific image
775 image_info
= filter_dict
["name"].split(":")
776 image_project
= image_info
[0]
777 if len(image_info
) == 2:
779 image_item
= image_info
[1]
780 if len(image_info
) == 3:
781 image_type
= image_info
[1]
782 image_item
= image_info
[2]
784 raise vimconn
.VimConnNotFoundException("Wrong format for image")
787 if image_type
== "image-family":
789 self
.conn_compute
.images()
790 .getFromFamily(project
=image_project
, family
=image_item
)
793 elif image_type
== "image":
795 self
.conn_compute
.images()
796 .get(project
=image_project
, image
=image_item
)
800 raise vimconn
.VimConnNotFoundException("Wrong format for image")
803 "id": "projects/%s/global/images/%s"
804 % (image_project
, image_response
["name"]),
806 [image_project
, image_item
, image_response
["name"]]
811 self
.logger
.debug("get_image_list Return: image_list %s", image_list
)
814 except Exception as e
:
815 self
._format
_vimconn
_exception
(e
)
817 def delete_inuse_nic(self
, nic_name
):
818 raise vimconn
.VimConnNotImplemented("Not necessary")
820 def delete_image(self
, image_id
):
821 raise vimconn
.VimConnNotImplemented("Not implemented")
823 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
824 """Send and action over a VM instance from VIM
825 Returns the vm_id if the action was successfully sent to the VIM
827 raise vimconn
.VimConnNotImplemented("Not necessary")
829 def _randomize_name(self
, name
):
830 """Adds a random string to allow requests with the same VM name
831 Returns the name with an additional random string (if the total size is bigger
832 than 62 the original name will be truncated)
841 + "".join(random_choice("0123456789abcdef") for _
in range(12))
843 self
.conn_compute
.instances().get(
844 project
=self
.project
, zone
=self
.zone
, instance
=random_name
846 # If no exception is arisen, the random name exists for an instance,
847 # so a new random name must be generated
849 except Exception as e
:
850 if e
.args
[0]["status"] == "404":
851 self
.logger
.debug("New random name: %s", random_name
)
855 "Exception generating random name (%s) for the instance", name
857 self
._format
_vimconn
_exception
(e
)
866 image_id
=None, # <image project>:(image|image-family):<image/family id>
868 affinity_group_list
=None,
872 availability_zone_index
=None,
873 availability_zone_list
=None,
876 "new_vminstance begin: name: %s, image_id: %s, flavor_id: %s, net_list: %s, cloud_config: %s, "
877 "disk_list: %s, availability_zone_index: %s, availability_zone_list: %s",
884 availability_zone_index
,
885 availability_zone_list
,
888 if self
.reload_client
:
889 self
._reload
_connection
()
891 # Validate input data is valid
892 # # First of all, the name must be adapted because Google Cloud only allows names consist of
893 # lowercase letters (a-z), numbers and hyphens (?:[a-z](?:[-a-z0-9]{0,61}[a-z0-9])?)
894 vm_name
= self
._check
_vm
_name
(name
)
895 vm_name
= self
._randomize
_name
(vm_name
)
898 # At least one network must be provided
900 raise vimconn
.VimConnException(
901 "At least one net must be provided to create a new VM"
906 metadata
= self
._build
_metadata
(vm_name
, cloud_config
)
908 # Building network interfaces list
909 network_interfaces
= []
912 if not net
.get("net_id"):
913 if not net
.get("name"):
918 ] = "regions/%s/subnetworks/" % self
.region
+ net
.get("name")
920 net_iface
["subnetwork"] = net
.get("net_id")
921 # In order to get an external IP address, the key "accessConfigs" must be used
922 # in the interace. It has to be of type "ONE_TO_ONE_NAT" and name "External NAT"
923 if net
.get("floating_ip", False) or (
924 net
["use"] == "mgmt" and self
.config
.get("use_floating_ip")
926 net_iface
["accessConfigs"] = [
927 {"type": "ONE_TO_ONE_NAT", "name": "External NAT"}
930 network_interfaces
.append(net_iface
)
932 self
.logger
.debug("Network interfaces: %s", network_interfaces
)
934 self
.logger
.debug("Source image: %s", image_id
)
938 "machineType": self
.get_flavor(flavor_id
)["id_complete"],
939 # Specify the boot disk and the image to use as a source.
944 "initializeParams": {
945 "sourceImage": image_id
,
949 # Specify the network interfaces
950 "networkInterfaces": network_interfaces
,
951 "metadata": metadata
,
955 self
.conn_compute
.instances()
956 .insert(project
=self
.project
, zone
=self
.zone
, body
=vm_parameters
)
959 self
._wait
_for
_zone
_operation
(response
["name"])
961 # The created instance info is obtained to get the name of the generated network interfaces (nic0, nic1...)
963 self
.conn_compute
.instances()
964 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_name
)
967 self
.logger
.debug("instance get: %s", response
)
968 vm_id
= response
["name"]
970 # The generated network interfaces in the instance are include in net_list:
971 for _
, net
in enumerate(net_list
):
972 for net_ifaces
in response
["networkInterfaces"]:
975 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
979 network_id
= self
._get
_resource
_name
_from
_resource
_id
(
982 if network_id
== self
._get
_resource
_name
_from
_resource
_id
(
983 net_ifaces
["subnetwork"]
985 net
["vim_id"] = net_ifaces
["name"]
988 "new_vminstance Return: (name %s, created_items %s)",
992 return vm_name
, created_items
994 except Exception as e
:
995 # Rollback vm creacion
996 if vm_id
is not None:
998 self
.logger
.debug("exception creating vm try to rollback")
999 self
.delete_vminstance(vm_id
, created_items
)
1000 except Exception as e2
:
1001 self
.logger
.error("new_vminstance rollback fail {}".format(e2
))
1005 "Exception creating new vminstance: %s", e
, exc_info
=True
1007 self
._format
_vimconn
_exception
(e
)
1009 def _build_metadata(self
, vm_name
, cloud_config
):
1012 metadata
["items"] = []
1014 # if there is a cloud-init load it
1016 self
.logger
.debug("cloud config: %s", cloud_config
)
1017 _
, userdata
= self
._create
_user
_data
(cloud_config
)
1018 metadata
["items"].append({"key": "user-data", "value": userdata
})
1020 # either password of ssh-keys are required
1021 # we will always use ssh-keys, in case it is not available we will generate it
1023 if cloud_config and cloud_config.get("key-pairs"):
1026 if cloud_config.get("key-pairs"):
1027 if isinstance(cloud_config["key-pairs"], list):
1028 # Transform the format "<key> <user@host>" into "<user>:<key>"
1030 for key in cloud_config.get("key-pairs"):
1031 key_data = key_data + key + "\n"
1037 # If there is no ssh key in cloud config, a new key is generated:
1038 _, key_data = self._generate_keys()
1041 "value": "" + key_data
1043 self.logger.debug("generated keys: %s", key_data)
1045 metadata["items"].append(key_pairs)
1047 self
.logger
.debug("metadata: %s", metadata
)
1051 def _generate_keys(self
):
1052 """Method used to generate a pair of private/public keys.
1053 This method is used because to create a vm in Azure we always need a key or a password
1054 In some cases we may have a password in a cloud-init file but it may not be available
1056 key
= rsa
.generate_private_key(
1057 backend
=crypto_default_backend(), public_exponent
=65537, key_size
=2048
1059 private_key
= key
.private_bytes(
1060 crypto_serialization
.Encoding
.PEM
,
1061 crypto_serialization
.PrivateFormat
.PKCS8
,
1062 crypto_serialization
.NoEncryption(),
1064 public_key
= key
.public_key().public_bytes(
1065 crypto_serialization
.Encoding
.OpenSSH
,
1066 crypto_serialization
.PublicFormat
.OpenSSH
,
1068 private_key
= private_key
.decode("utf8")
1069 # Change first line because Paramiko needs a explicit start with 'BEGIN RSA PRIVATE KEY'
1070 i
= private_key
.find("\n")
1071 private_key
= "-----BEGIN RSA PRIVATE KEY-----" + private_key
[i
:]
1072 public_key
= public_key
.decode("utf8")
1074 return private_key
, public_key
1076 def _get_unused_vm_name(self
, vm_name
):
1078 Checks the vm name and in case it is used adds a suffix to the name to allow creation
1082 self
.conn_compute
.instances()
1083 .list(project
=self
.project
, zone
=self
.zone
)
1086 # Filter to vms starting with the indicated name
1087 vms
= list(filter(lambda vm
: (vm
.name
.startswith(vm_name
)), all_vms
))
1088 vm_names
= [str(vm
.name
) for vm
in vms
]
1090 # get the name with the first not used suffix
1092 # name = subnet_name + "-" + str(name_suffix)
1093 name
= vm_name
# first subnet created will have no prefix
1095 while name
in vm_names
:
1097 name
= vm_name
+ "-" + str(name_suffix
)
1101 def get_vminstance(self
, vm_id
):
1103 Obtaing the vm instance data from v_id
1105 self
.logger
.debug("get_vminstance begin: vm_id %s", vm_id
)
1106 self
._reload
_connection
()
1110 self
.conn_compute
.instances()
1111 .get(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1114 # vm = response["source"]
1115 except Exception as e
:
1116 self
._format
_vimconn
_exception
(e
)
1118 self
.logger
.debug("get_vminstance Return: response %s", response
)
1121 def delete_vminstance(self
, vm_id
, created_items
=None):
1122 """Deletes a vm instance from the vim."""
1124 "delete_vminstance begin: vm_id %s created_items %s",
1128 if self
.reload_client
:
1129 self
._reload
_connection
()
1131 created_items
= created_items
or {}
1133 vm
= self
.get_vminstance(vm_id
)
1136 self
.conn_compute
.instances()
1137 .delete(project
=self
.project
, zone
=self
.zone
, instance
=vm_id
)
1140 self
._wait
_for
_zone
_operation
(operation
["name"])
1142 # The associated subnets must be checked if they are marked to be deleted
1143 for netIface
in vm
["networkInterfaces"]:
1145 self
._get
_resource
_name
_from
_resource
_id
(netIface
["subnetwork"])
1146 in self
.nets_to_be_deleted
1148 self
._get
_resource
_name
_from
_resource
_id
(
1149 self
.delete_network(netIface
["subnetwork"])
1152 self
.logger
.debug("delete_vminstance end")
1154 except Exception as e
:
1155 # The VM can be deleted previously during network deletion
1156 if e
.args
[0]["status"] == "404":
1157 self
.logger
.debug("The VM doesn't exist or has been deleted")
1159 self
._format
_vimconn
_exception
(e
)
1161 def _get_net_name_from_resource_id(self
, resource_id
):
1163 net_name
= str(resource_id
.split("/")[-1])
1167 raise vimconn
.VimConnException(
1168 "Unable to get google cloud net_name from invalid resource_id format '{}'".format(
1173 def _get_resource_name_from_resource_id(self
, resource_id
):
1175 Obtains resource_name from the google cloud complete identifier: resource_name will always be last item
1178 "_get_resource_name_from_resource_id begin: resource_id %s",
1182 resource
= str(resource_id
.split("/")[-1])
1185 "_get_resource_name_from_resource_id Return: resource %s",
1189 except Exception as e
:
1190 raise vimconn
.VimConnException(
1191 "Unable to get resource name from resource_id '{}' Error: '{}'".format(
1196 def refresh_nets_status(self
, net_list
):
1197 """Get the status of the networks
1198 Params: the list of network identifiers
1199 Returns a dictionary with:
1200 net_id: #VIM id of this network
1201 status: #Mandatory. Text with one of:
1202 # DELETED (not found at vim)
1203 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1204 # OTHER (Vim reported other status not understood)
1205 # ERROR (VIM indicates an ERROR status)
1206 # ACTIVE, INACTIVE, DOWN (admin down),
1207 # BUILD (on building process)
1209 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1210 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1212 self
.logger
.debug("refresh_nets_status begin: net_list %s", net_list
)
1214 self
._reload
_connection
()
1216 for net_id
in net_list
:
1218 resName
= self
._get
_resource
_name
_from
_resource
_id
(net_id
)
1221 self
.conn_compute
.subnetworks()
1222 .get(project
=self
.project
, region
=self
.region
, subnetwork
=resName
)
1225 self
.logger
.debug("get subnetwork: %s", net
)
1227 out_nets
[net_id
] = {
1228 "status": "ACTIVE", # Google Cloud does not provide the status in subnetworks getting
1229 "vim_info": str(net
),
1231 except vimconn
.VimConnNotFoundException
as e
:
1233 "VimConnNotFoundException %s when searching subnet", e
1235 out_nets
[net_id
] = {
1236 "status": "DELETED",
1237 "error_msg": str(e
),
1239 except Exception as e
:
1241 "Exception %s when searching subnet", e
, exc_info
=True
1243 out_nets
[net_id
] = {
1244 "status": "VIM_ERROR",
1245 "error_msg": str(e
),
1248 self
.logger
.debug("refresh_nets_status Return: out_nets %s", out_nets
)
1251 def refresh_vms_status(self
, vm_list
):
1252 """Get the status of the virtual machines and their interfaces/ports
1253 Params: the list of VM identifiers
1254 Returns a dictionary with:
1255 vm_id: # VIM id of this Virtual Machine
1256 status: # Mandatory. Text with one of:
1257 # DELETED (not found at vim)
1258 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1259 # OTHER (Vim reported other status not understood)
1260 # ERROR (VIM indicates an ERROR status)
1261 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1262 # BUILD (on building process), ERROR
1263 # ACTIVE:NoMgmtIP (Active but none of its interfaces has an IP address
1264 # (ACTIVE:NoMgmtIP is not returned for Azure)
1266 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1267 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1268 interfaces: list with interface info. Each item a dictionary with:
1269 vim_interface_id - The ID of the interface
1270 mac_address - The MAC address of the interface.
1271 ip_address - The IP address of the interface within the subnet.
1273 self
.logger
.debug("refresh_vms_status begin: vm_list %s", vm_list
)
1275 self
._reload
_connection
()
1277 search_vm_list
= vm_list
or {}
1279 for vm_id
in search_vm_list
:
1282 res_name
= self
._get
_resource
_name
_from
_resource
_id
(vm_id
)
1285 self
.conn_compute
.instances()
1286 .get(project
=self
.project
, zone
=self
.zone
, instance
=res_name
)
1290 out_vm
["vim_info"] = str(vm
["name"])
1291 out_vm
["status"] = self
.provision_state2osm
.get(vm
["status"], "OTHER")
1293 # In Google Cloud the there is no difference between provision or power status,
1294 # so if provision status method doesn't return a specific state (OTHER), the
1295 # power method is called
1296 if out_vm
["status"] == "OTHER":
1297 out_vm
["status"] = self
.power_state2osm
.get(vm
["status"], "OTHER")
1299 network_interfaces
= vm
["networkInterfaces"]
1300 out_vm
["interfaces"] = self
._get
_vm
_interfaces
_status
(
1301 vm_id
, network_interfaces
1303 except Exception as e
:
1304 self
.logger
.error("Exception %s refreshing vm_status", e
, exc_info
=True)
1305 out_vm
["status"] = "VIM_ERROR"
1306 out_vm
["error_msg"] = str(e
)
1307 out_vm
["vim_info"] = None
1309 out_vms
[vm_id
] = out_vm
1311 self
.logger
.debug("refresh_vms_status Return: out_vms %s", out_vms
)
1314 def _get_vm_interfaces_status(self
, vm_id
, interfaces
):
1316 Gets the interfaces detail for a vm
1317 :param interfaces: List of interfaces.
1318 :return: Dictionary with list of interfaces including, vim_interface_id, mac_address and ip_address
1321 "_get_vm_interfaces_status begin: vm_id %s interfaces %s",
1327 for network_interface
in interfaces
:
1329 interface_dict
["vim_interface_id"] = network_interface
["name"]
1332 ips
.append(network_interface
["networkIP"])
1333 interface_dict
["ip_address"] = ";".join(ips
)
1334 interface_list
.append(interface_dict
)
1337 "_get_vm_interfaces_status Return: interface_list %s",
1340 return interface_list
1341 except Exception as e
:
1343 "Exception %s obtaining interface data for vm: %s",
1348 self
._format
_vimconn
_exception
(e
)
1350 def _get_default_admin_user(self
, image_id
):
1351 if "ubuntu" in image_id
.lower():
1354 return self
._default
_admin
_user
1356 def _create_firewall_rules(self
, network
):
1358 Creates the necessary firewall rules to allow the traffic in the network
1359 (https://cloud.google.com/vpc/docs/firewalls)
1361 :return: a list with the names of the firewall rules
1363 self
.logger
.debug("_create_firewall_rules begin: network %s", network
)
1367 # Adding firewall rule to allow http:
1368 self
.logger
.debug("creating firewall rule to allow http")
1369 firewall_rule_body
= {
1370 "name": "fw-rule-http-" + network
,
1371 "network": "global/networks/" + network
,
1372 "allowed": [{"IPProtocol": "tcp", "ports": ["80"]}],
1374 self
.conn_compute
.firewalls().insert(
1375 project
=self
.project
, body
=firewall_rule_body
1378 # Adding firewall rule to allow ssh:
1379 self
.logger
.debug("creating firewall rule to allow ssh")
1380 firewall_rule_body
= {
1381 "name": "fw-rule-ssh-" + network
,
1382 "network": "global/networks/" + network
,
1383 "allowed": [{"IPProtocol": "tcp", "ports": ["22"]}],
1385 self
.conn_compute
.firewalls().insert(
1386 project
=self
.project
, body
=firewall_rule_body
1389 # Adding firewall rule to allow ping:
1390 self
.logger
.debug("creating firewall rule to allow ping")
1391 firewall_rule_body
= {
1392 "name": "fw-rule-icmp-" + network
,
1393 "network": "global/networks/" + network
,
1394 "allowed": [{"IPProtocol": "icmp"}],
1396 self
.conn_compute
.firewalls().insert(
1397 project
=self
.project
, body
=firewall_rule_body
1400 # Adding firewall rule to allow internal:
1401 self
.logger
.debug("creating firewall rule to allow internal")
1402 firewall_rule_body
= {
1403 "name": "fw-rule-internal-" + network
,
1404 "network": "global/networks/" + network
,
1406 {"IPProtocol": "tcp", "ports": ["0-65535"]},
1407 {"IPProtocol": "udp", "ports": ["0-65535"]},
1408 {"IPProtocol": "icmp"},
1411 self
.conn_compute
.firewalls().insert(
1412 project
=self
.project
, body
=firewall_rule_body
1415 # Adding firewall rule to allow microk8s:
1416 self
.logger
.debug("creating firewall rule to allow microk8s")
1417 firewall_rule_body
= {
1418 "name": "fw-rule-microk8s-" + network
,
1419 "network": "global/networks/" + network
,
1420 "allowed": [{"IPProtocol": "tcp", "ports": ["16443"]}],
1422 self
.conn_compute
.firewalls().insert(
1423 project
=self
.project
, body
=firewall_rule_body
1426 # Adding firewall rule to allow rdp:
1427 self
.logger
.debug("creating firewall rule to allow rdp")
1428 firewall_rule_body
= {
1429 "name": "fw-rule-rdp-" + network
,
1430 "network": "global/networks/" + network
,
1431 "allowed": [{"IPProtocol": "tcp", "ports": ["3389"]}],
1433 self
.conn_compute
.firewalls().insert(
1434 project
=self
.project
, body
=firewall_rule_body
1437 # Adding firewall rule to allow osm:
1438 self
.logger
.debug("creating firewall rule to allow osm")
1439 firewall_rule_body
= {
1440 "name": "fw-rule-osm-" + network
,
1441 "network": "global/networks/" + network
,
1442 "allowed": [{"IPProtocol": "tcp", "ports": ["9001", "9999"]}],
1444 self
.conn_compute
.firewalls().insert(
1445 project
=self
.project
, body
=firewall_rule_body
1449 "_create_firewall_rules Return: list_rules %s", rules_list
1452 except Exception as e
:
1454 "Unable to create google cloud firewall rules for network '{}'".format(
1458 self
._format
_vimconn
_exception
(e
)
1460 def _delete_firewall_rules(self
, network
):
1462 Deletes the associated firewall rules to the network
1464 :return: a list with the names of the firewall rules
1466 self
.logger
.debug("_delete_firewall_rules begin: network %s", network
)
1471 self
.conn_compute
.firewalls().list(project
=self
.project
).execute()
1473 for item
in rules_list
["items"]:
1474 if network
== self
._get
_resource
_name
_from
_resource
_id
(item
["network"]):
1475 self
.conn_compute
.firewalls().delete(
1476 project
=self
.project
, firewall
=item
["name"]
1479 self
.logger
.debug("_delete_firewall_rules Return: list_rules %s", 0)
1481 except Exception as e
:
1483 "Unable to delete google cloud firewall rules for network '{}'".format(
1487 self
._format
_vimconn
_exception
(e
)
1489 def migrate_instance(self
, vm_id
, compute_host
=None):
1493 vm_id: ID of an instance
1494 compute_host: Host to migrate the vdu to
1496 # TODO: Add support for migration
1497 raise vimconn
.VimConnNotImplemented("Not implemented")