# -*- coding: utf-8 -*- # # Copyright 2022 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Cloud Bare Metal Solution client.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals import collections import re from apitools.base.py import exceptions as apitools_exceptions from apitools.base.py import list_pager from googlecloudsdk.api_lib.util import apis from googlecloudsdk.api_lib.util import exceptions as apilib_exceptions from googlecloudsdk.command_lib.bms import util import six _DEFAULT_API_VERSION = 'v2' _V1_API_VERSION = 'v1' _GLOBAL_REGION = 'global' _REGIONAL_IAM_REGEX = re.compile( "PERMISSION_DENIED: Permission (.+) denied on 'projects/(.+?)/.*" ) IpRangeReservation = collections.namedtuple( 'IpRangeReservation', ['start_address', 'end_address', 'note'] ) def _ParseError(error): """Returns a best-effort error message created from an API client error.""" if isinstance(error, apitools_exceptions.HttpError): parsed_error = apilib_exceptions.HttpException( error, error_format='{message}' ) error_message = parsed_error.message else: error_message = six.text_type(error) return error_message def _CollapseRegionalIAMErrors(errors): """If all errors are PERMISSION_DENIEDs, use a single global error instead.""" # TODO(b/198857865): Remove this hack once the `global` region fix is in if errors: matches = [_REGIONAL_IAM_REGEX.match(e) for e in errors] if ( all(match is not None for match in matches) and len(set(match.group(1) for match in matches)) == 1 ): errors = [ 'PERMISSION_DENIED: Permission %s denied on projects/%s' % (matches[0].group(1), matches[0].group(2)) ] return errors class BmsClient(object): """Cloud Bare Metal Solution client.""" def __init__(self, api_version=_DEFAULT_API_VERSION): self._client = apis.GetClientInstance('baremetalsolution', api_version) self._v1_client = apis.GetClientInstance( 'baremetalsolution', _V1_API_VERSION ) self._messages = apis.GetMessagesModule('baremetalsolution', api_version) self.instances_service = self._client.projects_locations_instances self.volumes_service = self._client.projects_locations_volumes self.snapshot_schedule_policies_service = ( self._client.projects_locations_snapshotSchedulePolicies ) self.snapshots_service = self._client.projects_locations_volumes_snapshots self.networks_service = self._client.projects_locations_networks self.locations_service = self._client.projects_locations self.luns_service = self._client.projects_locations_volumes_luns self.nfs_shares_service = self._client.projects_locations_nfsShares self.ssh_keys_service = self._client.projects_locations_sshKeys self.operation_service = self._client.projects_locations_operations self.os_images_service = self._client.projects_locations_osImages self.nfs_mount_permissions_str_to_message = { 'READ_ONLY': ( self.messages.AllowedClient.MountPermissionsValueValuesEnum.READ ), 'READ_WRITE': ( self.messages.AllowedClient.MountPermissionsValueValuesEnum.READ_WRITE ), } self.nfs_storage_type_str_to_message = { 'SSD': self.messages.NfsShare.StorageTypeValueValuesEnum.SSD, 'HDD': self.messages.NfsShare.StorageTypeValueValuesEnum.HDD, } @property def client(self): return self._client @property def messages(self): return self._messages def GetInstance(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsInstancesGetRequest( name=resource.RelativeName() ) ) return self.instances_service.Get(request) def ListInstances(self, parent, limit=None, page_size=None): request = ( self.messages.BaremetalsolutionProjectsLocationsInstancesListRequest( parent=parent ) ) return list_pager.YieldFromList( self.instances_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='instances', ) def UpdateInstance( self, instance_resource, labels, os_image, enable_hyperthreading, ssh_keys, kms_key_version, clear_ssh_keys, ): """Update an existing instance resource.""" updated_fields = [] if labels is not None: updated_fields.append('labels') if os_image is not None: updated_fields.append('os_image') if enable_hyperthreading is not None: updated_fields.append('hyperthreading_enabled') if ssh_keys or clear_ssh_keys: updated_fields.append('ssh_keys') if kms_key_version is not None: updated_fields.append('kms_key_version') ssh_keys_relative = [ssh_key.RelativeName() for ssh_key in ssh_keys] instance_msg = self.messages.Instance( name=instance_resource.RelativeName(), labels=labels, osImage=os_image, hyperthreadingEnabled=enable_hyperthreading, sshKeys=ssh_keys_relative, kmsKeyVersion=kms_key_version, ) request = ( self.messages.BaremetalsolutionProjectsLocationsInstancesPatchRequest( name=instance_resource.RelativeName(), instance=instance_msg, updateMask=','.join(updated_fields), ) ) return self.instances_service.Patch(request) def ReimageInstance( self, instance_resource, os_image, ssh_keys, kms_crypto_key_version ): """Reimage an existing instance.""" ssh_keys_relative = [ssh_key.RelativeName() for ssh_key in ssh_keys] request = ( self.messages.BaremetalsolutionProjectsLocationsInstancesReimageRequest( name=instance_resource.RelativeName(), reimageInstanceRequest={ 'osImage': os_image, 'sshKeys': ssh_keys_relative, 'kmsKeyVersion': kms_crypto_key_version, }, ) ) return self.instances_service.Reimage(request) def EnableHyperthreading(self, instance_resource): """Enable hyperthreading on an instance.""" request = self.messages.BaremetalsolutionProjectsLocationsInstancesEnableHyperthreadingRequest( name=instance_resource.RelativeName() ) return self.instances_service.EnableHyperthreading(request) def DisableHyperthreading(self, instance_resource): """Disable hyperthreading on an instance.""" request = self.messages.BaremetalsolutionProjectsLocationsInstancesDisableHyperthreadingRequest( name=instance_resource.RelativeName() ) return self.instances_service.DisableHyperthreading(request) def RenameInstance(self, instance_resource, new_name): """Rename an existing instance resource.""" rename_instance_request = self.messages.RenameInstanceRequest( newInstanceId=new_name ) request = ( self.messages.BaremetalsolutionProjectsLocationsInstancesRenameRequest( name=instance_resource.RelativeName(), renameInstanceRequest=rename_instance_request, ) ) return self.instances_service.Rename(request) def LoadInstanceAuthInfo(self, instance_resource): """Load instance auth info.""" request = self.messages.BaremetalsolutionProjectsLocationsInstancesLoadAuthInfoRequest( name=instance_resource.RelativeName() ) return self.instances_service.LoadAuthInfo(request) def ListSnapshotSchedulePolicies( self, project_resource, limit=None, page_size=None ): parent = 'projects/%s/locations/global' % project_resource request = self.messages.BaremetalsolutionProjectsLocationsSnapshotSchedulePoliciesListRequest( parent=parent ) return list_pager.YieldFromList( self.snapshot_schedule_policies_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='snapshotSchedulePolicies', ) def GetSnapshotSchedulePolicy(self, resource): request = self.messages.BaremetalsolutionProjectsLocationsSnapshotSchedulePoliciesGetRequest( name=resource.RelativeName() ) return self.snapshot_schedule_policies_service.Get(request) def CreateSnapshotSchedulePolicy( self, policy_resource, labels, description, schedules ): """Sends request to create a new Snapshot Schedule Policy.""" policy_id = policy_resource.Name() parent = policy_resource.Parent().RelativeName() schedule_msgs = self._ParseSnapshotSchedules(schedules) policy_msg = self.messages.SnapshotSchedulePolicy( description=description, schedules=schedule_msgs, labels=labels ) request = self.messages.BaremetalsolutionProjectsLocationsSnapshotSchedulePoliciesCreateRequest( parent=parent, snapshotSchedulePolicyId=policy_id, snapshotSchedulePolicy=policy_msg, ) return self.snapshot_schedule_policies_service.Create(request) def UpdateSnapshotSchedulePolicy( self, policy_resource, description, labels, schedules ): """Sends request to update an existing SnapshotSchedulePolicy.""" updated_fields = [] if description: updated_fields.append('description') if labels is not None: updated_fields.append('labels') schedule_msgs = self._ParseSnapshotSchedules(schedules) if schedule_msgs: updated_fields.append('schedules') update_mask = ','.join(updated_fields) policy_msg = self.messages.SnapshotSchedulePolicy( description=description, schedules=schedule_msgs, labels=labels ) request = self.messages.BaremetalsolutionProjectsLocationsSnapshotSchedulePoliciesPatchRequest( name=policy_resource.RelativeName(), snapshotSchedulePolicy=policy_msg, updateMask=update_mask, ) return self.snapshot_schedule_policies_service.Patch(request) def _ParseSnapshotSchedules(self, schedules): """Parses schedule ArgDict dicts into a list of Schedule messages.""" schedule_msgs = [] if schedules: for schedule_arg in schedules: schedule_msgs.append( self.messages.Schedule( crontabSpec=schedule_arg['crontab_spec'], retentionCount=schedule_arg['retention_count'], prefix=schedule_arg['prefix'], ) ) return schedule_msgs def DeleteSnapshotSchedulePolicy(self, resource): request = self.messages.BaremetalsolutionProjectsLocationsSnapshotSchedulePoliciesDeleteRequest( name=resource.RelativeName() ) return self.snapshot_schedule_policies_service.Delete(request) def ListVolumes(self, parent, limit=None, page_size=None): request = ( self.messages.BaremetalsolutionProjectsLocationsVolumesListRequest( parent=parent ) ) return list_pager.YieldFromList( self.volumes_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='volumes', ) def UpdateVolume( self, volume_resource, labels, snapshot_schedule_policy_resource, remove_snapshot_schedule_policy, snapshot_auto_delete, ): """Update an existing volume resource.""" updated_fields = [] policy_name = None if snapshot_schedule_policy_resource: updated_fields.append('snapshotSchedulePolicy') policy_name = snapshot_schedule_policy_resource.RelativeName() elif remove_snapshot_schedule_policy: updated_fields.append('snapshotSchedulePolicy') if labels is not None: updated_fields.append('labels') if snapshot_auto_delete: updated_fields.append('snapshotAutoDeleteBehavior') volume_msg = self.messages.Volume( name=volume_resource.RelativeName(), snapshotAutoDeleteBehavior=snapshot_auto_delete, snapshotSchedulePolicy=policy_name, labels=labels, ) request = ( self.messages.BaremetalsolutionProjectsLocationsVolumesPatchRequest( name=volume_resource.RelativeName(), volume=volume_msg, updateMask=','.join(updated_fields), ) ) return self.volumes_service.Patch(request) def GetVolume(self, resource): request = self.messages.BaremetalsolutionProjectsLocationsVolumesGetRequest( name=resource.RelativeName() ) return self.volumes_service.Get(request) def RenameVolume(self, volume_resource, new_name): """Rename an existing volume resource.""" rename_volume_request = self.messages.RenameVolumeRequest( newVolumeId=new_name ) request = ( self.messages.BaremetalsolutionProjectsLocationsVolumesRenameRequest( name=volume_resource.RelativeName(), renameVolumeRequest=rename_volume_request, ) ) return self.volumes_service.Rename(request) def ListNetworks(self, parent, limit=None, page_size=None): request = ( self.messages.BaremetalsolutionProjectsLocationsNetworksListRequest( parent=parent ) ) return list_pager.YieldFromList( self.networks_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='networks', ) def ListOSImages(self, project_resource, limit=None, page_size=None): parent = 'projects/%s/locations/global' % project_resource request = ( self.messages.BaremetalsolutionProjectsLocationsOsImagesListRequest( parent=parent ) ) return list_pager.YieldFromList( self.os_images_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='osImages', ) def GetOSImage(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsOsImagesGetRequest( name=resource.RelativeName() ) ) return self.os_images_service.Get(request) def GetNetwork(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsNetworksGetRequest( name=resource.RelativeName() ) ) return self.networks_service.Get(request) def UpdateNetwork(self, network_resource, labels, ip_reservations): """Update an existing network resource.""" updated_fields = [] ip_reservations_messages = [] if labels is not None: updated_fields.append('labels') if ip_reservations is not None: updated_fields.append('reservations') for ip_reservation in ip_reservations: ip_reservations_messages.append( self.messages.NetworkAddressReservation( startAddress=ip_reservation.start_address, endAddress=ip_reservation.end_address, note=ip_reservation.note, ) ) network_msg = self.messages.Network( name=network_resource.RelativeName(), labels=labels, reservations=ip_reservations_messages, ) request = ( self.messages.BaremetalsolutionProjectsLocationsNetworksPatchRequest( name=network_resource.RelativeName(), network=network_msg, updateMask=','.join(updated_fields), ) ) return self.networks_service.Patch(request) def RenameNetwork(self, network_resource, new_name): """Rename an existing network resource.""" rename_network_request = self.messages.RenameNetworkRequest( newNetworkId=new_name ) request = ( self.messages.BaremetalsolutionProjectsLocationsNetworksRenameRequest( name=network_resource.RelativeName(), renameNetworkRequest=rename_network_request, ) ) return self.networks_service.Rename(request) def IsClientNetwork(self, network): return network.type == self.messages.Network.TypeValueValuesEnum.CLIENT def IsPrivateNetwork(self, network): return network.type == self.messages.Network.TypeValueValuesEnum.PRIVATE def IsClientLogicalNetworkInterface(self, logical_network_interface): return ( logical_network_interface.networkType == self.messages.LogicalNetworkInterface.NetworkTypeValueValuesEnum.CLIENT ) def IsPrivateLogicalNetworkInterface(self, logical_network_interface): return ( logical_network_interface.networkType == self.messages.LogicalNetworkInterface.NetworkTypeValueValuesEnum.PRIVATE ) def ListLUNsForVolume(self, volume_resource, limit=None, page_size=None): parent = volume_resource.RelativeName() request = ( self.messages.BaremetalsolutionProjectsLocationsVolumesLunsListRequest( parent=parent ) ) return list_pager.YieldFromList( self.luns_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='luns', ) def GetLUN(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsVolumesLunsGetRequest( name=resource.RelativeName() ) ) return self.luns_service.Get(request) def ListSnapshotsForVolume(self, volume_resource, limit=None, page_size=None): parent = volume_resource.RelativeName() request = self.messages.BaremetalsolutionProjectsLocationsVolumesSnapshotsListRequest( parent=parent ) return list_pager.YieldFromList( self.snapshots_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='volumeSnapshots', ) def GetVolumeSnapshot(self, resource): request = self.messages.BaremetalsolutionProjectsLocationsVolumesSnapshotsGetRequest( name=resource.RelativeName() ) return self.snapshots_service.Get(request) def CreateVolumeSnapshot(self, resource, name, description): request = self.messages.BaremetalsolutionProjectsLocationsVolumesSnapshotsCreateRequest( parent=resource.RelativeName(), volumeSnapshot=self.messages.VolumeSnapshot( name=name, description=description ), ) return self.snapshots_service.Create(request) def DeleteVolumeSnapshot(self, resource): request = self.messages.BaremetalsolutionProjectsLocationsVolumesSnapshotsDeleteRequest( name=resource.RelativeName() ) return self.snapshots_service.Delete(request) def RestoreVolumeSnapshot(self, snapshot_name): request = self.messages.BaremetalsolutionProjectsLocationsVolumesSnapshotsRestoreVolumeSnapshotRequest( # pylint: disable=line-too-long volumeSnapshot=snapshot_name ) return self.snapshots_service.RestoreVolumeSnapshot(request) def GetNfsShare(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesGetRequest( name=resource.RelativeName() ) ) return self.nfs_shares_service.Get(request) def ListNfsShares(self, parent, limit=None, page_size=None): request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesListRequest( parent=parent ) ) return list_pager.YieldFromList( self.nfs_shares_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='nfsShares', ) def UpdateNfsShare(self, nfs_share_resource, labels, allowed_clients): """Update an existing nfs share resource.""" updated_fields = [] updated_allowed_clients = [] if labels is not None: updated_fields.append('labels') if allowed_clients is not None: updated_fields.append('allowedClients') updated_allowed_clients = allowed_clients nfs_share_msg = self.messages.NfsShare( name=nfs_share_resource.RelativeName(), labels=labels, allowedClients=updated_allowed_clients, ) request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesPatchRequest( name=nfs_share_resource.RelativeName(), nfsShare=nfs_share_msg, updateMask=','.join(updated_fields), ) ) return self.nfs_shares_service.Patch(request) def DeleteNfsShare(self, nfs_share_resource): """Delete an existing nfs share resource.""" request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesDeleteRequest( name=nfs_share_resource.RelativeName() ) ) return self.nfs_shares_service.Delete(request) def CreateNfsShare( self, nfs_share_resource, size_gib, storage_type, allowed_clients_dicts, labels, ): """Create an NFS share resource.""" allowed_clients = self.ParseAllowedClientsDicts( nfs_share_resource=nfs_share_resource, allowed_clients_dicts=allowed_clients_dicts, ) nfs_share_msg = self.messages.NfsShare( name=nfs_share_resource.RelativeName(), requestedSizeGib=size_gib, storageType=self.nfs_storage_type_str_to_message[storage_type], allowedClients=allowed_clients, labels=labels, ) request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesCreateRequest( nfsShare=nfs_share_msg, parent=nfs_share_resource.Parent().RelativeName(), ) ) return self.nfs_shares_service.Create(request) def RenameNfsShare(self, nfs_share_resource, new_name): """Rename an existing nfs share resource.""" rename_nfs_share_request = self.messages.RenameNfsShareRequest( newNfsshareId=new_name ) request = ( self.messages.BaremetalsolutionProjectsLocationsNfsSharesRenameRequest( name=nfs_share_resource.RelativeName(), renameNfsShareRequest=rename_nfs_share_request, ) ) return self.nfs_shares_service.Rename(request) def ListSshKeys(self, project_resource, limit=None, page_size=None): parent = 'projects/%s/locations/global' % project_resource request = ( self.messages.BaremetalsolutionProjectsLocationsSshKeysListRequest( parent=parent ) ) return list_pager.YieldFromList( self.ssh_keys_service, request, limit=limit, batch_size_attribute='pageSize', batch_size=page_size, field='sshKeys', ) def CreateSshKey(self, resource, public_key): """Sends request to create an SSH key.""" request = ( self.messages.BaremetalsolutionProjectsLocationsSshKeysCreateRequest( parent=resource.Parent().RelativeName(), sshKeyId=resource.Name(), sSHKey=self.messages.SSHKey(publicKey=public_key), ) ) return self.ssh_keys_service.Create(request) def DeleteSshKey(self, resource): request = ( self.messages.BaremetalsolutionProjectsLocationsSshKeysDeleteRequest( name=resource.RelativeName() ) ) return self.ssh_keys_service.Delete(request) def ParseAllowedClientsDicts(self, nfs_share_resource, allowed_clients_dicts): """Parses NFS share allowed client list of dicts.""" allowed_clients = [] for allowed_client in allowed_clients_dicts: mount_permissions = self.nfs_mount_permissions_str_to_message[ allowed_client['mount-permissions'] ] network_full_name = util.NFSNetworkFullName( nfs_share_resource=nfs_share_resource, allowed_client_dict=allowed_client, ) allowed_clients.append( self.messages.AllowedClient( network=network_full_name, allowedClientsCidr=allowed_client['cidr'], mountPermissions=mount_permissions, allowDev=allowed_client['allow-dev'], allowSuid=allowed_client['allow-suid'], noRootSquash=not allowed_client['enable-root-squash'], ) ) return allowed_clients