perfkitbenchmarker/providers/openstack/os_virtual_machine.py (333 lines of code) (raw):
# Copyright 2015 PerfKitBenchmarker Authors. 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.
"""Class to represent an OpenStack Virtual Machine.
Regions:
User defined
Machine types, or flavors:
run 'openstack flavor list'
Images:
run 'openstack image list'
"""
import json
import logging
import threading
from absl import flags
from perfkitbenchmarker import disk_strategies
from perfkitbenchmarker import errors
from perfkitbenchmarker import linux_virtual_machine
from perfkitbenchmarker import provider_info
from perfkitbenchmarker import virtual_machine
from perfkitbenchmarker import vm_util
from perfkitbenchmarker.providers.openstack import os_disk
from perfkitbenchmarker.providers.openstack import os_network
from perfkitbenchmarker.providers.openstack import utils as os_utils
NONE = 'None'
VALIDATION_ERROR_MESSAGE = '{0} {1} could not be found.'
FLAGS = flags.FLAGS
class OpenStackVirtualMachine(virtual_machine.BaseVirtualMachine):
"""Object representing an OpenStack Virtual Machine"""
CLOUD = provider_info.OPENSTACK
DEFAULT_IMAGE = None
_lock = threading.Lock() # _lock guards the following:
command_works = False
validated_resources_set = set()
uploaded_keypair_set = set()
deleted_keypair_set = set()
created_server_group_dict = {}
deleted_server_group_set = set()
floating_network_id = None
def __init__(self, vm_spec):
"""Initialize an OpenStack virtual machine.
Args:
vm_spec: virtual_machine.BaseVirtualMachineSpec object of the vm.
"""
super().__init__(vm_spec)
self.key_name = 'perfkit_key_%s' % FLAGS.run_uri
self.user_name = FLAGS.openstack_image_username
self.image = self.image or self.DEFAULT_IMAGE
# FIXME(meteorfox): Remove --openstack_public_network and
# --openstack_private_network once depreciation time has expired
self.network_name = (
FLAGS.openstack_network or FLAGS.openstack_private_network
)
self.floating_ip_pool_name = (
FLAGS.openstack_floating_ip_pool or FLAGS.openstack_public_network
)
self.id = None
self.boot_volume_id = None
self.server_group_id = None
self.floating_ip = None
self.firewall = None
self.public_network = None
self.subnet_id = None
self.post_provisioning_script = FLAGS.openstack_post_provisioning_script
@property
def group_id(self):
"""Returns the security group ID of this VM."""
return 'perfkit_sc_group'
def _CreateDependencies(self):
"""Validate and Create dependencies prior creating the VM."""
self._CheckPrerequisites()
self.firewall = os_network.OpenStackFirewall.GetFirewall()
self.public_network = os_network.OpenStackFloatingIPPool(
OpenStackVirtualMachine.floating_network_id
)
self._UploadSSHPublicKey()
source_range = self._GetInternalNetworkCIDR()
self.firewall.AllowPort(
self, os_network.MIN_PORT, os_network.MAX_PORT, source_range
)
self.firewall.AllowICMP(self) # Allowing ICMP traffic (i.e. ping)
self.AllowRemoteAccessPorts()
def _Create(self):
"""Creates an OpenStack VM instance and waits until it is ACTIVE."""
if FLAGS.openstack_boot_from_volume:
vol_name = '%s_volume' % self.name
disk_resp = os_disk.CreateBootVolume(self, vol_name, self.image)
self.boot_volume_id = disk_resp['id']
os_disk.WaitForVolumeCreation(self, self.boot_volume_id)
self._CreateInstance()
@vm_util.Retry(max_retries=4, poll_interval=2)
def _PostCreate(self):
self._SetIPAddresses()
def _Delete(self):
if self.id is None:
return
self._DeleteInstance()
if self.floating_ip:
self.public_network.release(self, self.floating_ip)
if self.server_group_id:
self._DeleteServerGroup()
if self.boot_volume_id:
os_disk.DeleteVolume(self, self.boot_volume_id)
self.boot_volume_id = None
def _DeleteDependencies(self):
"""Delete dependencies that were needed for the VM after the VM has been
deleted.
"""
self._DeleteSSHPublicKey()
def _Exists(self):
if self.id is None:
return False
show_cmd = os_utils.OpenStackCLICommand(self, 'server', 'show', self.id)
stdout, _, _ = show_cmd.Issue()
try:
resp = json.loads(stdout)
return resp
except ValueError:
return False
def _Suspend(self):
"""Suspends the vm."""
raise NotImplementedError()
def _Resume(self):
"""Resumes the VM."""
raise NotImplementedError()
def _CheckCanaryCommand(self):
if OpenStackVirtualMachine.command_works: # fast path
return
with self._lock:
if OpenStackVirtualMachine.command_works:
return
logging.info('Testing OpenStack CLI command is installed and working')
cmd = os_utils.OpenStackCLICommand(self, 'image', 'list')
stdout, stderr, _ = cmd.Issue()
if stderr:
raise errors.Config.InvalidValue(
'OpenStack CLI test command failed. Please make sure the OpenStack '
'CLI client is installed and properly configured'
)
OpenStackVirtualMachine.command_works = True
def _CheckPrerequisites(self):
"""Checks prerequisites are met otherwise aborts execution."""
self._CheckCanaryCommand()
if self.zone in self.validated_resources_set:
return # No need to check again
with self._lock:
if self.zone in self.validated_resources_set:
return
logging.info('Validating prerequisites.')
self._CheckImage()
self._CheckFlavor()
self._CheckNetworks()
self.validated_resources_set.add(self.zone)
logging.info('Prerequisites validated.')
def _CheckImage(self):
"""Tries to get image, if found continues execution otherwise aborts."""
cmd = os_utils.OpenStackCLICommand(self, 'image', 'show', self.image)
err_msg = VALIDATION_ERROR_MESSAGE.format('Image', self.image)
self._IssueCommandCheck(cmd, err_msg)
def _CheckFlavor(self):
"""Tries to get flavor, if found continues execution otherwise aborts."""
cmd = os_utils.OpenStackCLICommand(
self, 'flavor', 'show', self.machine_type
)
err_msg = VALIDATION_ERROR_MESSAGE.format('Machine type', self.machine_type)
self._IssueCommandCheck(cmd, err_msg)
def _CheckNetworks(self):
"""Tries to get network, if found continues execution otherwise aborts."""
if not self.network_name:
if self.floating_ip_pool_name:
msg = (
'Cannot associate floating-ip address from pool %s without '
'an internally routable network. Make sure '
'--openstack_network flag is set.'
)
else:
msg = (
'Cannot build instance without a network. Make sure to set '
'either just --openstack_network or both '
'--openstack_network and --openstack_floating_ip_pool flags.'
)
raise errors.Error(msg)
self._CheckNetworkExists(self.network_name)
if self.floating_ip_pool_name:
floating_network_dict = self._CheckFloatingIPNetworkExists(
self.floating_ip_pool_name
)
OpenStackVirtualMachine.floating_network_id = floating_network_dict['id']
def _CheckFloatingIPNetworkExists(self, floating_network_name_or_id):
network = self._CheckNetworkExists(floating_network_name_or_id)
if network['router:external'] not in ('External', True):
raise errors.Config.InvalidValue(
'Network "%s" is not External' % self.floating_ip_pool_name
)
return network
def _CheckNetworkExists(self, network_name_or_id):
cmd = os_utils.OpenStackCLICommand(
self, 'network', 'show', network_name_or_id
)
err_msg = VALIDATION_ERROR_MESSAGE.format('Network', network_name_or_id)
stdout = self._IssueCommandCheck(cmd, err_msg)
network = json.loads(stdout)
return network
def _IssueCommandCheck(self, cmd, err_msg=None):
"""Issues command and, if stderr is non-empty, raises an error message
Args:
cmd: The command to be issued.
err_msg: string. Error message if command fails.
"""
if err_msg is None:
err_msg = ''
stdout, stderr, _ = cmd.Issue()
if stderr:
raise errors.Config.InvalidValue(err_msg)
return stdout
def _UploadSSHPublicKey(self):
"""Uploads SSH public key to the VM's region."""
with self._lock:
if self.zone in self.uploaded_keypair_set:
return
cmd = os_utils.OpenStackCLICommand(
self, 'keypair', 'create', self.key_name
)
cmd.flags['public-key'] = self.ssh_public_key
cmd.IssueRetryable()
self.uploaded_keypair_set.add(self.zone)
if self.zone in self.deleted_keypair_set:
self.deleted_keypair_set.remove(self.zone)
def _DeleteSSHPublicKey(self):
"""Deletes SSH public key used for the VM."""
with self._lock:
if self.zone in self.deleted_keypair_set:
return
cmd = os_utils.OpenStackCLICommand(
self, 'keypair', 'delete', self.key_name
)
del cmd.flags['format'] # keypair delete does not support json output
cmd.Issue()
self.deleted_keypair_set.add(self.zone)
if self.zone in self.uploaded_keypair_set:
self.uploaded_keypair_set.remove(self.zone)
def _CreateInstance(self):
"""Execute command for creating an OpenStack VM instance."""
create_cmd = self._GetCreateCommand()
stdout, stderr, _ = create_cmd.Issue()
if stderr:
raise errors.Error(stderr)
resp = json.loads(stdout)
self.id = resp['id']
def _GetCreateCommand(self):
cmd = os_utils.OpenStackCLICommand(self, 'server', 'create', self.name)
cmd.flags['flavor'] = self.machine_type
cmd.flags['security-group'] = self.group_id
cmd.flags['key-name'] = self.key_name
cmd.flags['availability-zone'] = self.zone
cmd.flags['nic'] = 'net-id=%s' % self.network_name
cmd.flags['wait'] = True
if FLAGS.openstack_config_drive:
cmd.flags['config-drive'] = 'True'
hints = self._GetSchedulerHints()
if hints:
cmd.flags['hint'] = hints
if FLAGS.openstack_boot_from_volume:
cmd.flags['volume'] = self.boot_volume_id
else:
cmd.flags['image'] = self.image
if self.post_provisioning_script:
cmd.flags['user-data'] = self.post_provisioning_script
return cmd
def _GetSchedulerHints(self):
if FLAGS.openstack_scheduler_policy == NONE:
return None
with self._lock:
group_name = 'perfkit_server_group_%s' % FLAGS.run_uri
hint_temp = 'group=%s'
if self.zone in self.created_server_group_dict:
hint = hint_temp % self.created_server_group_dict[self.zone]['id']
return hint
server_group = self._CreateServerGroup(group_name)
self.server_group_id = server_group['id']
self.created_server_group_dict[self.zone] = server_group
if self.zone in self.deleted_server_group_set:
self.deleted_server_group_set.remove(self.zone)
return hint_temp % server_group['id']
def _CreateServerGroup(self, group_name):
cmd = os_utils.OpenStackCLICommand(
self, 'server group', 'create', group_name
)
cmd.flags['policy'] = FLAGS.openstack_scheduler_policy
stdout, stderr, _ = cmd.Issue()
if stderr:
raise errors.Error(stderr)
server_group = json.loads(stdout)
return server_group
def _DeleteServerGroup(self):
with self._lock:
if self.zone in self.deleted_server_group_set:
return
cmd = os_utils.OpenStackCLICommand(
self, 'server group', 'delete', self.server_group_id
)
del cmd.flags['format'] # delete does not support json output
cmd.Issue()
self.deleted_server_group_set.add(self.zone)
if self.zone in self.created_server_group_dict:
del self.created_server_group_dict[self.zone]
def _DeleteInstance(self):
cmd = os_utils.OpenStackCLICommand(self, 'server', 'delete', self.id)
del cmd.flags['format'] # delete does not support json output
cmd.flags['wait'] = True
cmd.Issue()
def _SetIPAddresses(self):
show_cmd = os_utils.OpenStackCLICommand(self, 'server', 'show', self.name)
stdout, _, _ = show_cmd.Issue()
server_dict = json.loads(stdout)
self.ip_address = self._GetNetworkIPAddress(server_dict, self.network_name)
self.internal_ip = self.ip_address
if self.floating_ip_pool_name:
self.floating_ip = self._AllocateFloatingIP()
self.internal_ip = self.ip_address
self.ip_address = self.floating_ip.floating_ip_address
def _GetNetworkIPAddress(self, server_dict, network_name):
addresses = server_dict['addresses'].split(',')
for address in addresses:
if network_name in address:
_, ip = address.split('=')
return ip
def _GetInternalNetworkCIDR(self):
"""Returns IP addresses source range of internal network."""
net_cmd = os_utils.OpenStackCLICommand(
self, 'network', 'show', self.network_name
)
net_stdout, _, _ = net_cmd.Issue()
network = json.loads(net_stdout)
if isinstance(network['subnets'], list):
self.subnet_id = network['subnets'][0]
else:
self.subnet_id = network['subnets']
subnet_cmd = os_utils.OpenStackCLICommand(
self, 'subnet', 'show', self.subnet_id
)
stdout, _, _ = subnet_cmd.Issue()
subnet_dict = json.loads(stdout)
return subnet_dict['cidr']
def _AllocateFloatingIP(self):
floating_ip = self.public_network.associate(self)
logging.info(
'floating-ip associated: {}'.format(floating_ip.floating_ip_address)
)
return floating_ip
def CreateScratchDisk(self, _, disk_spec):
disks_names = (
'%s_data_%d_%d' % (self.name, len(self.scratch_disks), i)
for i in range(disk_spec.num_striped_disks)
)
disks = [
os_disk.OpenStackDisk(disk_spec, name, self.zone)
for name in disks_names
]
scratch_disk = self._CreateScratchDiskFromDisks(disk_spec, disks)
disk_strategies.PrepareScratchDiskStrategy().PrepareScratchDisk(
self, scratch_disk, disk_spec
)
def GetResourceMetadata(self):
"""Returns a dict containing metadata about the VM.
Returns:
dict mapping string property key to value.
"""
result = super().GetResourceMetadata()
if self.post_provisioning_script:
result['post_provisioning_script'] = self.post_provisioning_script
return result
class ClearBasedOpenStackVirtualMachine(
OpenStackVirtualMachine, linux_virtual_machine.ClearMixin
):
DEFAULT_IMAGE = 'upstream-clear'