Jesse Pretorius 4c8b5f4f99 Add ability to ignore provider quota for a pool
In some circumstances it is useful to tell the launcher to
ignore the provide quota and just trust the max-* settings
for that pool instead.

This particular need arises when using Rackspace public cloud
for both instances and OnMetal nodes. In this situation the
quota for instances and for OnMetal nodes is different, but
nodepool only queries the quota for instances. When trying to
build OnMetal nodes, the quota check fails - but should not.

In this circumstance, instead of making shade/nodepool
complicated by figuring out how to calculate disparate quota
types, it makes sense to rather just allow nodepool to ignore
the quota for a pool to try executing the build instead.

While this is our use-case, it may also be useful to others
for other reasons.

Change-Id: I232a1ab365795381ab180aceb48e8c87843ac713
2018-07-11 17:37:55 +00:00

391 lines
14 KiB
Python

# Copyright (C) 2011-2013 OpenStack Foundation
#
# 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.
import math
import voluptuous as v
from nodepool.driver import ProviderConfig
from nodepool.driver import ConfigValue
from nodepool.driver import ConfigPool
class ProviderDiskImage(ConfigValue):
def __init__(self):
self.name = None
self.pause = False
self.config_drive = None
self.connection_type = None
self.connection_port = None
self.meta = None
def __eq__(self, other):
if isinstance(other, ProviderDiskImage):
return (self.name == other.name and
self.pause == other.pause and
self.config_drive == other.config_drive and
self.connection_type == other.connection_type and
self.connection_port == other.connection_port and
self.meta == other.meta)
return False
def __repr__(self):
return "<ProviderDiskImage %s>" % self.name
class ProviderCloudImage(ConfigValue):
def __init__(self):
self.name = None
self.config_drive = None
self.image_id = None
self.image_name = None
self.username = None
self.connection_type = None
self.connection_port = None
def __eq__(self, other):
if isinstance(other, ProviderCloudImage):
return (self.name == other.name and
self.config_drive == other.config_drive and
self.image_id == other.image_id and
self.image_name == other.image_name and
self.username == other.username and
self.connection_type == other.connection_type and
self.connection_port == other.connection_port)
return False
def __repr__(self):
return "<ProviderCloudImage %s>" % self.name
@property
def external_name(self):
'''Human readable version of external.'''
return self.image_id or self.image_name or self.name
class ProviderLabel(ConfigValue):
def __init__(self):
self.name = None
self.diskimage = None
self.cloud_image = None
self.min_ram = None
self.flavor_name = None
self.key_name = None
self.console_log = False
self.boot_from_volume = False
self.volume_size = None
# The ProviderPool object that owns this label.
self.pool = None
def __eq__(self, other):
if isinstance(other, ProviderLabel):
# NOTE(Shrews): We intentionally do not compare 'pool' here
# since this causes recursive checks with ProviderPool.
return (other.diskimage == self.diskimage and
other.cloud_image == self.cloud_image and
other.min_ram == self.min_ram and
other.flavor_name == self.flavor_name and
other.key_name == self.key_name and
other.name == self.name and
other.console_log == self.console_log and
other.boot_from_volume == self.boot_from_volume and
other.volume_size == self.volume_size)
return False
def __repr__(self):
return "<ProviderLabel %s>" % self.name
class ProviderPool(ConfigPool):
def __init__(self):
self.name = None
self.max_cores = None
self.max_ram = None
self.ignore_provider_quota = False
self.azs = None
self.networks = None
self.security_groups = None
self.auto_floating_ip = True
self.host_key_checking = True
self.labels = None
# The OpenStackProviderConfig object that owns this pool.
self.provider = None
# Initialize base class attributes
super().__init__()
def __eq__(self, other):
if isinstance(other, ProviderPool):
# NOTE(Shrews): We intentionally do not compare 'provider' here
# since this causes recursive checks with OpenStackProviderConfig.
return (super().__eq__(other) and
other.name == self.name and
other.max_cores == self.max_cores and
other.max_ram == self.max_ram and
other.ignore_provider_quota == (
self.ignore_provider_quota) and
other.azs == self.azs and
other.networks == self.networks and
other.security_groups == self.security_groups and
other.auto_floating_ip == self.auto_floating_ip and
other.host_key_checking == self.host_key_checking and
other.labels == self.labels)
return False
def __repr__(self):
return "<ProviderPool %s>" % self.name
class OpenStackProviderConfig(ProviderConfig):
def __init__(self, driver, provider):
self.driver_object = driver
self.__pools = {}
self.cloud_config = None
self.image_type = None
self.rate = None
self.boot_timeout = None
self.launch_timeout = None
self.clean_floating_ips = None
self.diskimages = {}
self.cloud_images = {}
self.hostname_format = None
self.image_name_format = None
super().__init__(provider)
def __eq__(self, other):
if isinstance(other, OpenStackProviderConfig):
return (super().__eq__(other) and
other.cloud_config == self.cloud_config and
other.pools == self.pools and
other.image_type == self.image_type and
other.rate == self.rate and
other.boot_timeout == self.boot_timeout and
other.launch_timeout == self.launch_timeout and
other.clean_floating_ips == self.clean_floating_ips and
other.diskimages == self.diskimages and
other.cloud_images == self.cloud_images)
return False
def _cloudKwargs(self):
cloud_kwargs = {}
for arg in ['region-name', 'cloud']:
if arg in self.provider:
cloud_kwargs[arg] = self.provider[arg]
return cloud_kwargs
@property
def pools(self):
return self.__pools
@property
def manage_images(self):
return True
def load(self, config):
cloud_kwargs = self._cloudKwargs()
occ = self.driver_object.os_client_config
self.cloud_config = occ.get_one_cloud(**cloud_kwargs)
self.image_type = self.cloud_config.config['image_format']
self.region_name = self.provider.get('region-name')
self.rate = float(self.provider.get('rate', 1.0))
self.boot_timeout = self.provider.get('boot-timeout', 60)
self.launch_timeout = self.provider.get('launch-timeout', 3600)
self.launch_retries = self.provider.get('launch-retries', 3)
self.clean_floating_ips = self.provider.get('clean-floating-ips')
self.hostname_format = self.provider.get(
'hostname-format',
'{label.name}-{provider.name}-{node.id}'
)
self.image_name_format = self.provider.get(
'image-name-format',
'{image_name}-{timestamp}'
)
default_port_mapping = {
'ssh': 22,
'winrm': 5986,
}
for image in self.provider.get('diskimages', []):
i = ProviderDiskImage()
i.name = image['name']
self.diskimages[i.name] = i
diskimage = config.diskimages[i.name]
diskimage.image_types.add(self.image_type)
i.pause = bool(image.get('pause', False))
i.config_drive = image.get('config-drive', None)
i.connection_type = image.get('connection-type', 'ssh')
i.connection_port = image.get(
'connection-port',
default_port_mapping.get(i.connection_type, 22))
# This dict is expanded and used as custom properties when
# the image is uploaded.
i.meta = image.get('meta', {})
# 5 elements, and no key or value can be > 255 chars
# per Nova API rules
if i.meta:
if len(i.meta) > 5 or \
any([len(k) > 255 or len(v) > 255
for k, v in i.meta.items()]):
# soft-fail
# self.log.error("Invalid metadata for %s; ignored"
# % i.name)
i.meta = {}
for image in self.provider.get('cloud-images', []):
i = ProviderCloudImage()
i.name = image['name']
i.config_drive = image.get('config-drive', None)
i.image_id = image.get('image-id', None)
i.image_name = image.get('image-name', None)
i.username = image.get('username', None)
i.connection_type = image.get('connection-type', 'ssh')
i.connection_port = image.get(
'connection-port',
default_port_mapping.get(i.connection_type, 22))
self.cloud_images[i.name] = i
for pool in self.provider.get('pools', []):
pp = ProviderPool()
pp.name = pool['name']
pp.provider = self
self.pools[pp.name] = pp
pp.max_cores = pool.get('max-cores', math.inf)
pp.max_servers = pool.get('max-servers', math.inf)
pp.max_ram = pool.get('max-ram', math.inf)
pp.ignore_provider_quota = pool.get('ignore-provider-quota', False)
pp.azs = pool.get('availability-zones')
pp.networks = pool.get('networks', [])
pp.security_groups = pool.get('security-groups', [])
pp.auto_floating_ip = bool(pool.get('auto-floating-ip', True))
pp.host_key_checking = bool(pool.get('host-key-checking', True))
for label in pool.get('labels', []):
pl = ProviderLabel()
pl.name = label['name']
pl.pool = pp
pp.labels[pl.name] = pl
diskimage = label.get('diskimage', None)
if diskimage:
pl.diskimage = config.diskimages[diskimage]
else:
pl.diskimage = None
cloud_image_name = label.get('cloud-image', None)
if cloud_image_name:
cloud_image = self.cloud_images.get(cloud_image_name, None)
if not cloud_image:
raise ValueError(
"cloud-image %s does not exist in provider %s"
" but is referenced in label %s" %
(cloud_image_name, self.name, pl.name))
else:
cloud_image = None
pl.cloud_image = cloud_image
pl.min_ram = label.get('min-ram', 0)
pl.flavor_name = label.get('flavor-name', None)
pl.key_name = label.get('key-name')
pl.console_log = label.get('console-log', False)
pl.boot_from_volume = bool(label.get('boot-from-volume',
False))
pl.volume_size = label.get('volume-size', 50)
top_label = config.labels[pl.name]
top_label.pools.append(pp)
def getSchema(self):
provider_diskimage = {
'name': str,
'pause': bool,
'meta': dict,
'config-drive': bool,
'connection-type': str,
'connection-port': int,
}
provider_cloud_images = {
'name': str,
'config-drive': bool,
'connection-type': str,
'connection-port': int,
v.Exclusive('image-id', 'cloud-image-name-or-id'): str,
v.Exclusive('image-name', 'cloud-image-name-or-id'): str,
'username': str,
}
pool_label_main = {
v.Required('name'): str,
v.Exclusive('diskimage', 'label-image'): str,
v.Exclusive('cloud-image', 'label-image'): str,
'min-ram': int,
'flavor-name': str,
'key-name': str,
'console-log': bool,
'boot-from-volume': bool,
'volume-size': int,
}
label_min_ram = v.Schema({v.Required('min-ram'): int}, extra=True)
label_flavor_name = v.Schema({v.Required('flavor-name'): str},
extra=True)
label_diskimage = v.Schema({v.Required('diskimage'): str}, extra=True)
label_cloud_image = v.Schema({v.Required('cloud-image'): str},
extra=True)
pool_label = v.All(pool_label_main,
v.Any(label_min_ram, label_flavor_name),
v.Any(label_diskimage, label_cloud_image))
pool = {
'name': str,
'networks': [str],
'auto-floating-ip': bool,
'host-key-checking': bool,
'ignore-provider-quota': bool,
'max-cores': int,
'max-servers': int,
'max-ram': int,
'labels': [pool_label],
'availability-zones': [str],
'security-groups': [str]
}
return v.Schema({
'region-name': str,
v.Required('cloud'): str,
'boot-timeout': int,
'launch-timeout': int,
'launch-retries': int,
'nodepool-id': str,
'rate': v.Coerce(float),
'hostname-format': str,
'image-name-format': str,
'clean-floating-ips': bool,
'pools': [pool],
'diskimages': [provider_diskimage],
'cloud-images': [provider_cloud_images],
})
def getSupportedLabels(self, pool_name=None):
labels = set()
for pool in self.pools.values():
if not pool_name or (pool.name == pool_name):
labels.update(pool.labels.keys())
return labels