Support image and kernel selection
- Image selection based of Ubuntu series name (e.g. 'xenial') - Kernel selection based on MAAS kernel names of 'ga-16.04' or 'hwe-16.04' - Validation that defined images and kernels are valid in the current node driver - Unit test for the validation Change-Id: I6256a2e00a4594af38a2d862c738e03efb7ddb29
This commit is contained in:
parent
1804203ea1
commit
4a1367a39a
3
.gitignore
vendored
3
.gitignore
vendored
@ -101,3 +101,6 @@ ENV/
|
|||||||
|
|
||||||
# IDEA IDE
|
# IDEA IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# VIM
|
||||||
|
.*.swp
|
||||||
|
@ -57,3 +57,17 @@ class NodeDriver(ProviderDriver):
|
|||||||
else:
|
else:
|
||||||
raise errors.DriverError("Unsupported action %s for driver %s" %
|
raise errors.DriverError("Unsupported action %s for driver %s" %
|
||||||
(task_action, self.driver_desc))
|
(task_action, self.driver_desc))
|
||||||
|
|
||||||
|
def get_available_images(self):
|
||||||
|
"""Return images that can be deployed to nodes by this driver."""
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_available_kernels(self, image):
|
||||||
|
"""Return a list of kernels that can be specified for deployment.
|
||||||
|
|
||||||
|
:param image: str specifying what image the kernel will be activated
|
||||||
|
within
|
||||||
|
"""
|
||||||
|
|
||||||
|
return []
|
||||||
|
@ -1801,10 +1801,11 @@ class DeployNode(BaseMaasAction):
|
|||||||
"Error setting boot action id key tag for %s." % n.name,
|
"Error setting boot action id key tag for %s." % n.name,
|
||||||
exc_info=ex)
|
exc_info=ex)
|
||||||
|
|
||||||
self.logger.info("Deploying node %s" % (n.name))
|
self.logger.info("Deploying node %s: image=%s, kernel=%s" %
|
||||||
|
(n.name, n.image, n.kernel))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
machine.deploy()
|
machine.deploy(platform=n.image, kernel=n.kernel)
|
||||||
except errors.DriverError:
|
except errors.DriverError:
|
||||||
msg = "Error deploying node %s, skipping" % n.name
|
msg = "Error deploying node %s, skipping" % n.name
|
||||||
self.logger.warning(msg)
|
self.logger.warning(msg)
|
||||||
|
@ -176,6 +176,7 @@ class MaasRequestFactory(object):
|
|||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"Received error response - URL: %s %s - RESPONSE: %s" %
|
"Received error response - URL: %s %s - RESPONSE: %s" %
|
||||||
(prepared_req.method, prepared_req.url, resp.status_code))
|
(prepared_req.method, prepared_req.url, resp.status_code))
|
||||||
|
self.logger.debug("Response content: %s" % resp.text)
|
||||||
raise errors.DriverError("MAAS Error: %s - %s" % (resp.status_code,
|
raise errors.DriverError("MAAS Error: %s - %s" % (resp.status_code,
|
||||||
resp.text))
|
resp.text))
|
||||||
return resp
|
return resp
|
||||||
|
@ -24,6 +24,7 @@ import drydock_provisioner.config as config
|
|||||||
|
|
||||||
from drydock_provisioner.drivers.node.driver import NodeDriver
|
from drydock_provisioner.drivers.node.driver import NodeDriver
|
||||||
from drydock_provisioner.drivers.node.maasdriver.api_client import MaasRequestFactory
|
from drydock_provisioner.drivers.node.maasdriver.api_client import MaasRequestFactory
|
||||||
|
from drydock_provisioner.drivers.node.maasdriver.models.boot_resource import BootResources
|
||||||
|
|
||||||
from .actions.node import ValidateNodeServices
|
from .actions.node import ValidateNodeServices
|
||||||
from .actions.node import CreateStorageTemplate
|
from .actions.node import CreateStorageTemplate
|
||||||
@ -207,6 +208,31 @@ class MaasNodeDriver(NodeDriver):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def get_available_images(self):
|
||||||
|
"""Return images available in MAAS."""
|
||||||
|
maas_client = MaasRequestFactory(
|
||||||
|
config.config_mgr.conf.maasdriver.maas_api_url,
|
||||||
|
config.config_mgr.conf.maasdriver.maas_api_key)
|
||||||
|
|
||||||
|
br = BootResources(maas_client)
|
||||||
|
br.refresh()
|
||||||
|
|
||||||
|
return br.get_available_images()
|
||||||
|
|
||||||
|
def get_available_kernels(self, image_name):
|
||||||
|
"""Return kernels available for ``image_name``.
|
||||||
|
|
||||||
|
:param image_name: str image name (e.g. 'xenial')
|
||||||
|
"""
|
||||||
|
maas_client = MaasRequestFactory(
|
||||||
|
config.config_mgr.conf.maasdriver.maas_api_url,
|
||||||
|
config.config_mgr.conf.maasdriver.maas_api_key)
|
||||||
|
|
||||||
|
br = BootResources(maas_client)
|
||||||
|
br.refresh()
|
||||||
|
|
||||||
|
return br.get_available_kernels(image_name)
|
||||||
|
|
||||||
|
|
||||||
def list_opts():
|
def list_opts():
|
||||||
return {MaasNodeDriver.driver_key: MaasNodeDriver.maasdriver_options}
|
return {MaasNodeDriver.driver_key: MaasNodeDriver.maasdriver_options}
|
||||||
|
@ -37,6 +37,28 @@ class BootResource(model_base.ResourceBase):
|
|||||||
def __init__(self, api_client, **kwargs):
|
def __init__(self, api_client, **kwargs):
|
||||||
super().__init__(api_client, **kwargs)
|
super().__init__(api_client, **kwargs)
|
||||||
|
|
||||||
|
def get_image_name(self):
|
||||||
|
"""Return the name that would be specified in a deployment.
|
||||||
|
|
||||||
|
Return None if this is not an ubuntu image, otherwise
|
||||||
|
the distro series name
|
||||||
|
"""
|
||||||
|
(os, release) = self.name.split('/')
|
||||||
|
|
||||||
|
# Only supply image names for ubuntu-based images
|
||||||
|
if os == 'ubuntu':
|
||||||
|
return release
|
||||||
|
else:
|
||||||
|
# Non-ubuntu images such as the uefi bootloader
|
||||||
|
# should never be selectable
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_kernel_name(self):
|
||||||
|
"""Return the kernel name that would be specified in a deployment."""
|
||||||
|
(_, kernel) = self.architecture.split('/')
|
||||||
|
|
||||||
|
return kernel
|
||||||
|
|
||||||
|
|
||||||
class BootResources(model_base.ResourceCollectionBase):
|
class BootResources(model_base.ResourceCollectionBase):
|
||||||
|
|
||||||
@ -62,3 +84,26 @@ class BootResources(model_base.ResourceCollectionBase):
|
|||||||
resp.status_code, resp.text)
|
resp.status_code, resp.text)
|
||||||
self.logger.error(msg)
|
self.logger.error(msg)
|
||||||
raise errors.DriverError(msg)
|
raise errors.DriverError(msg)
|
||||||
|
|
||||||
|
def get_available_images(self):
|
||||||
|
"""Get list of available deployable images."""
|
||||||
|
image_options = list()
|
||||||
|
for k, v in self.resources.items():
|
||||||
|
if v.get_image_name() not in image_options:
|
||||||
|
image_options.append(v.get_image_name())
|
||||||
|
return image_options
|
||||||
|
|
||||||
|
def get_available_kernels(self, image_name):
|
||||||
|
"""Get kernels available for image_name
|
||||||
|
|
||||||
|
Return list of kernel names available for
|
||||||
|
``image_name``.
|
||||||
|
|
||||||
|
:param image_name: str image_name (e.g. 'xenial')
|
||||||
|
"""
|
||||||
|
kernel_options = list()
|
||||||
|
for k, v in self.resources.items():
|
||||||
|
if (v.get_image_name() == image_name
|
||||||
|
and v.get_kernel_name() not in kernel_options):
|
||||||
|
kernel_options.append(v.get_kernel_name())
|
||||||
|
return kernel_options
|
||||||
|
@ -457,8 +457,7 @@ class Machines(model_base.ResourceCollectionBase):
|
|||||||
if k.startswith('power_params.'):
|
if k.startswith('power_params.'):
|
||||||
field = k[13:]
|
field = k[13:]
|
||||||
result = [
|
result = [
|
||||||
i for i in result
|
i for i in result if str(
|
||||||
if str(
|
|
||||||
getattr(i, 'power_parameters', {}).get(field, None)) ==
|
getattr(i, 'power_parameters', {}).get(field, None)) ==
|
||||||
str(v)
|
str(v)
|
||||||
]
|
]
|
||||||
|
@ -939,8 +939,7 @@ class BootactionReport(BaseAction):
|
|||||||
bas = self.state_manager.get_boot_actions_for_node(n)
|
bas = self.state_manager.get_boot_actions_for_node(n)
|
||||||
running_bas = {
|
running_bas = {
|
||||||
k: v
|
k: v
|
||||||
for (k, v) in bas.items()
|
for (k, v) in bas.items() if v.get('action_status') ==
|
||||||
if v.get('action_status') ==
|
|
||||||
hd_fields.ActionResult.Incomplete
|
hd_fields.ActionResult.Incomplete
|
||||||
}
|
}
|
||||||
if len(running_bas) > 0:
|
if len(running_bas) > 0:
|
||||||
|
@ -281,7 +281,7 @@ class Orchestrator(object):
|
|||||||
"""
|
"""
|
||||||
status = None
|
status = None
|
||||||
site_design = None
|
site_design = None
|
||||||
val = Validator()
|
val = Validator(self)
|
||||||
try:
|
try:
|
||||||
status, site_design = self.get_described_site(design_ref)
|
status, site_design = self.get_described_site(design_ref)
|
||||||
if status.status == hd_fields.ActionResult.Success:
|
if status.status == hd_fields.ActionResult.Success:
|
||||||
|
@ -22,6 +22,13 @@ from drydock_provisioner.objects.task import TaskStatus, TaskStatusMessage
|
|||||||
|
|
||||||
|
|
||||||
class Validator():
|
class Validator():
|
||||||
|
def __init__(self, orchestrator):
|
||||||
|
"""Create a validator with a reference to the orchestrator.
|
||||||
|
|
||||||
|
:param orchestrator: instance of Orchestrator
|
||||||
|
"""
|
||||||
|
self.orchestrator = orchestrator
|
||||||
|
|
||||||
def validate_design(self, site_design, result_status=None):
|
def validate_design(self, site_design, result_status=None):
|
||||||
"""Validate the design in site_design passes all validation rules.
|
"""Validate the design in site_design passes all validation rules.
|
||||||
|
|
||||||
@ -32,13 +39,12 @@ class Validator():
|
|||||||
:param site_design: instance of objects.SiteDesign
|
:param site_design: instance of objects.SiteDesign
|
||||||
:param result_status: instance of objects.TaskStatus
|
:param result_status: instance of objects.TaskStatus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if result_status is None:
|
if result_status is None:
|
||||||
result_status = TaskStatus()
|
result_status = TaskStatus()
|
||||||
|
|
||||||
validation_error = False
|
validation_error = False
|
||||||
for rule in rule_set:
|
for rule in rule_set:
|
||||||
output = rule(site_design)
|
output = rule(site_design, orchestrator=self.orchestrator)
|
||||||
result_status.message_list.extend(output)
|
result_status.message_list.extend(output)
|
||||||
error_msg = [m for m in output if m.error]
|
error_msg = [m for m in output if m.error]
|
||||||
result_status.error_count = result_status.error_count + len(
|
result_status.error_count = result_status.error_count + len(
|
||||||
@ -54,7 +60,66 @@ class Validator():
|
|||||||
return result_status
|
return result_status
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def rational_network_bond(cls, site_design):
|
def valid_platform_selection(cls, site_design, orchestrator=None):
|
||||||
|
"""Validate that the platform selection for all nodes is valid.
|
||||||
|
|
||||||
|
Each node specifies an ``image`` and a ``kernel`` to use for
|
||||||
|
deployment. Check that these are valid for the image repoistory
|
||||||
|
configured in MAAS.
|
||||||
|
"""
|
||||||
|
message_list = list()
|
||||||
|
|
||||||
|
try:
|
||||||
|
node_driver = orchestrator.enabled_drivers['node']
|
||||||
|
except KeyError:
|
||||||
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg="Platform Validation: No enabled node driver, image"
|
||||||
|
"and kernel selections not validated.",
|
||||||
|
error=False,
|
||||||
|
ctx_type='NA',
|
||||||
|
ctx='NA'))
|
||||||
|
return message_list
|
||||||
|
|
||||||
|
valid_images = node_driver.get_available_images()
|
||||||
|
|
||||||
|
valid_kernels = dict()
|
||||||
|
|
||||||
|
for i in valid_images:
|
||||||
|
valid_kernels[i] = node_driver.get_available_kernels(i)
|
||||||
|
|
||||||
|
for n in site_design.baremetal_nodes:
|
||||||
|
if n.image in valid_images:
|
||||||
|
if n.kernel in valid_kernels[n.image]:
|
||||||
|
continue
|
||||||
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg="Platform Validation: invalid kernel %s for node %s."
|
||||||
|
% (n.kernel, n.name),
|
||||||
|
error=True,
|
||||||
|
ctx_type='NA',
|
||||||
|
ctx='NA'))
|
||||||
|
continue
|
||||||
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg="Platform Validation: invalid image %s for node %s." %
|
||||||
|
(n.image, n.name),
|
||||||
|
error=True,
|
||||||
|
ctx_type='NA',
|
||||||
|
ctx='NA'))
|
||||||
|
if not message_list:
|
||||||
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg="Platform Validation: all nodes have valid "
|
||||||
|
"image and kernel selections.",
|
||||||
|
error=False,
|
||||||
|
ctx_type='NA',
|
||||||
|
ctx='NA'))
|
||||||
|
|
||||||
|
return message_list
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rational_network_bond(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
This check ensures that each NetworkLink has a rational bonding setup.
|
This check ensures that each NetworkLink has a rational bonding setup.
|
||||||
If the bonding mode is set to 'disabled' then it ensures that no other options are specified.
|
If the bonding mode is set to 'disabled' then it ensures that no other options are specified.
|
||||||
@ -74,8 +139,7 @@ class Validator():
|
|||||||
if bonding_mode == 'disabled':
|
if bonding_mode == 'disabled':
|
||||||
# check to make sure nothing else is specified
|
# check to make sure nothing else is specified
|
||||||
if any([
|
if any([
|
||||||
network_link.get(x)
|
network_link.get(x) for x in [
|
||||||
for x in [
|
|
||||||
'bonding_peer_rate', 'bonding_xmit_hash',
|
'bonding_peer_rate', 'bonding_xmit_hash',
|
||||||
'bonding_mon_rate', 'bonding_up_delay',
|
'bonding_mon_rate', 'bonding_up_delay',
|
||||||
'bonding_down_delay'
|
'bonding_down_delay'
|
||||||
@ -144,7 +208,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def network_trunking_rational(cls, site_design):
|
def network_trunking_rational(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
This check ensures that for each NetworkLink if the allowed networks are greater then 1 trunking mode is
|
This check ensures that for each NetworkLink if the allowed networks are greater then 1 trunking mode is
|
||||||
enabled. It also makes sure that if trunking mode is disabled then a default network is defined.
|
enabled. It also makes sure that if trunking mode is disabled then a default network is defined.
|
||||||
@ -194,7 +258,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def storage_partitioning(cls, site_design):
|
def storage_partitioning(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
This checks that for each storage device a partition list OR volume group is defined. Also for each partition
|
This checks that for each storage device a partition list OR volume group is defined. Also for each partition
|
||||||
list it ensures that a file system and partition volume group are not defined in the same partition.
|
list it ensures that a file system and partition volume group are not defined in the same partition.
|
||||||
@ -276,7 +340,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def unique_network_check(cls, site_design):
|
def unique_network_check(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensures that each network name appears at most once between all NetworkLink
|
Ensures that each network name appears at most once between all NetworkLink
|
||||||
allowed networks
|
allowed networks
|
||||||
@ -327,7 +391,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def mtu_rational(cls, site_design):
|
def mtu_rational(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensure that the MTU for each network is equal or less than the MTU defined
|
Ensure that the MTU for each network is equal or less than the MTU defined
|
||||||
for the parent NetworkLink for that network.
|
for the parent NetworkLink for that network.
|
||||||
@ -385,7 +449,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def storage_sizing(cls, site_design):
|
def storage_sizing(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensures that for a partitioned physical device or logical volumes
|
Ensures that for a partitioned physical device or logical volumes
|
||||||
in a volume group, if sizing is a percentage then those percentages
|
in a volume group, if sizing is a percentage then those percentages
|
||||||
@ -469,7 +533,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def no_duplicate_IPs_check(cls, site_design):
|
def no_duplicate_IPs_check(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensures that the same IP is not assigned to multiple baremetal node definitions by checking each new IP against
|
Ensures that the same IP is not assigned to multiple baremetal node definitions by checking each new IP against
|
||||||
the list of known IPs. If the IP is unique no error is thrown and the new IP will be added to the list to be
|
the list of known IPs. If the IP is unique no error is thrown and the new IP will be added to the list to be
|
||||||
@ -483,7 +547,9 @@ class Validator():
|
|||||||
|
|
||||||
if not baremetal_nodes_list:
|
if not baremetal_nodes_list:
|
||||||
msg = 'No BaremetalNodes Found.'
|
msg = 'No BaremetalNodes Found.'
|
||||||
message_list.append(TaskStatusMessage(msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
||||||
else:
|
else:
|
||||||
for node in baremetal_nodes_list:
|
for node in baremetal_nodes_list:
|
||||||
addressing_list = node.get('addressing', [])
|
addressing_list = node.get('addressing', [])
|
||||||
@ -504,12 +570,14 @@ class Validator():
|
|||||||
|
|
||||||
if not message_list:
|
if not message_list:
|
||||||
msg = 'No Duplicate IP Addresses.'
|
msg = 'No Duplicate IP Addresses.'
|
||||||
message_list.append(TaskStatusMessage(msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
||||||
|
|
||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def boot_storage_rational(cls, site_design):
|
def boot_storage_rational(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensures that root volume is defined and is at least 20GB and that boot volume is at least 1 GB
|
Ensures that root volume is defined and is at least 20GB and that boot volume is at least 1 GB
|
||||||
"""
|
"""
|
||||||
@ -599,7 +667,7 @@ class Validator():
|
|||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def ip_locality_check(cls, site_design):
|
def ip_locality_check(cls, site_design, orchestrator=None):
|
||||||
"""
|
"""
|
||||||
Ensures that each IP addresses assigned to a baremetal node is within the defined CIDR for the network. Also
|
Ensures that each IP addresses assigned to a baremetal node is within the defined CIDR for the network. Also
|
||||||
verifies that the gateway IP for each static route of a network is within that network's CIDR.
|
verifies that the gateway IP for each static route of a network is within that network's CIDR.
|
||||||
@ -613,7 +681,9 @@ class Validator():
|
|||||||
|
|
||||||
if not network_list:
|
if not network_list:
|
||||||
msg = 'No networks found.'
|
msg = 'No networks found.'
|
||||||
message_list.append(TaskStatusMessage(msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
||||||
else:
|
else:
|
||||||
for net in network_list:
|
for net in network_list:
|
||||||
name = net.get('name')
|
name = net.get('name')
|
||||||
@ -650,7 +720,9 @@ class Validator():
|
|||||||
ctx='NA'))
|
ctx='NA'))
|
||||||
if not baremetal_nodes_list:
|
if not baremetal_nodes_list:
|
||||||
msg = 'No baremetal_nodes found.'
|
msg = 'No baremetal_nodes found.'
|
||||||
message_list.append(TaskStatusMessage(msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
||||||
else:
|
else:
|
||||||
for node in baremetal_nodes_list:
|
for node in baremetal_nodes_list:
|
||||||
addressing_list = node.get('addressing', [])
|
addressing_list = node.get('addressing', [])
|
||||||
@ -687,7 +759,9 @@ class Validator():
|
|||||||
ctx='NA'))
|
ctx='NA'))
|
||||||
if not message_list:
|
if not message_list:
|
||||||
msg = 'IP Locality Success'
|
msg = 'IP Locality Success'
|
||||||
message_list.append(TaskStatusMessage(msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
message_list.append(
|
||||||
|
TaskStatusMessage(
|
||||||
|
msg=msg, error=False, ctx_type='NA', ctx='NA'))
|
||||||
return message_list
|
return message_list
|
||||||
|
|
||||||
|
|
||||||
@ -701,4 +775,5 @@ rule_set = [
|
|||||||
Validator.ip_locality_check,
|
Validator.ip_locality_check,
|
||||||
Validator.no_duplicate_IPs_check,
|
Validator.no_duplicate_IPs_check,
|
||||||
Validator.boot_storage_rational,
|
Validator.boot_storage_rational,
|
||||||
|
Validator.valid_platform_selection,
|
||||||
]
|
]
|
||||||
|
@ -90,6 +90,11 @@ def setup(setup_logging):
|
|||||||
config.config_mgr.register_options(enable_keystone=False)
|
config.config_mgr.register_options(enable_keystone=False)
|
||||||
|
|
||||||
config.config_mgr.conf([])
|
config.config_mgr.conf([])
|
||||||
|
config.config_mgr.conf.set_override(
|
||||||
|
name="node_driver",
|
||||||
|
group="plugins",
|
||||||
|
override=
|
||||||
|
"drydock_provisioner.drivers.node.maasdriver.driver.MaasNodeDriver")
|
||||||
config.config_mgr.conf.set_override(
|
config.config_mgr.conf.set_override(
|
||||||
name="database_connect_string",
|
name="database_connect_string",
|
||||||
group="database",
|
group="database",
|
||||||
|
@ -19,8 +19,15 @@ from drydock_provisioner.orchestrator.actions.orchestrator import PrepareNodes
|
|||||||
|
|
||||||
|
|
||||||
class TestActionPrepareNodes(object):
|
class TestActionPrepareNodes(object):
|
||||||
def test_preparenodes(self, input_files, deckhand_ingester, setup,
|
def test_preparenodes(self, mocker, input_files, deckhand_ingester, setup,
|
||||||
drydock_state):
|
drydock_state):
|
||||||
|
mock_images = mocker.patch("drydock_provisioner.drivers.node.driver.NodeDriver"
|
||||||
|
".get_available_images")
|
||||||
|
mock_images.return_value = ['xenial']
|
||||||
|
mock_kernels = mocker.patch("drydock_provisioner.drivers.node.driver.NodeDriver"
|
||||||
|
".get_available_kernels")
|
||||||
|
mock_kernels.return_value = ['ga-16.04', 'hwe-16.04']
|
||||||
|
|
||||||
input_file = input_files.join("deckhand_fullsite.yaml")
|
input_file = input_files.join("deckhand_fullsite.yaml")
|
||||||
|
|
||||||
design_ref = "file://%s" % str(input_file)
|
design_ref = "file://%s" % str(input_file)
|
||||||
|
@ -30,7 +30,7 @@ class TestDesignValidator(object):
|
|||||||
|
|
||||||
status, site_design = Orchestrator.get_effective_site(orch, design_ref)
|
status, site_design = Orchestrator.get_effective_site(orch, design_ref)
|
||||||
|
|
||||||
val = Validator()
|
val = Validator(orch)
|
||||||
response = val.validate_design(site_design)
|
response = val.validate_design(site_design)
|
||||||
|
|
||||||
for msg in response.message_list:
|
for msg in response.message_list:
|
||||||
|
85
tests/unit/test_validation_rule_valid_platform.py
Normal file
85
tests/unit/test_validation_rule_valid_platform.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Copyright 2017 AT&T Intellectual Property. All other 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.
|
||||||
|
"""Test Validation Rule Rational Boot Storage"""
|
||||||
|
|
||||||
|
import drydock_provisioner.config as config
|
||||||
|
|
||||||
|
from drydock_provisioner.orchestrator.orchestrator import Orchestrator
|
||||||
|
from drydock_provisioner.orchestrator.validations.validator import Validator
|
||||||
|
|
||||||
|
|
||||||
|
class TestValidPlatform(object):
|
||||||
|
def test_valid_platform(self, mocker, deckhand_ingester, drydock_state,
|
||||||
|
input_files):
|
||||||
|
mock_images = mocker.patch(
|
||||||
|
"drydock_provisioner.drivers.node.maasdriver.driver."
|
||||||
|
"MaasNodeDriver.get_available_images")
|
||||||
|
mock_images.return_value = ['xenial']
|
||||||
|
mock_kernels = mocker.patch(
|
||||||
|
"drydock_provisioner.drivers.node.maasdriver.driver."
|
||||||
|
"MaasNodeDriver.get_available_kernels")
|
||||||
|
mock_kernels.return_value = ['ga-16.04', 'hwe-16.04']
|
||||||
|
|
||||||
|
input_file = input_files.join("validation.yaml")
|
||||||
|
design_ref = "file://%s" % str(input_file)
|
||||||
|
|
||||||
|
orch = Orchestrator(
|
||||||
|
state_manager=drydock_state,
|
||||||
|
ingester=deckhand_ingester,
|
||||||
|
enabled_drivers=config.config_mgr.conf.plugins)
|
||||||
|
|
||||||
|
status, site_design = Orchestrator.get_effective_site(orch, design_ref)
|
||||||
|
|
||||||
|
message_list = Validator.valid_platform_selection(
|
||||||
|
site_design, orchestrator=orch)
|
||||||
|
for m in message_list:
|
||||||
|
print(m.to_dict())
|
||||||
|
|
||||||
|
msg = message_list[0].to_dict()
|
||||||
|
|
||||||
|
assert 'all nodes have valid' in msg.get('message')
|
||||||
|
assert msg.get('error') is False
|
||||||
|
assert len(message_list) == 1
|
||||||
|
|
||||||
|
def test_invalid_platform(self, mocker, deckhand_ingester, drydock_state,
|
||||||
|
input_files):
|
||||||
|
mock_images = mocker.patch(
|
||||||
|
"drydock_provisioner.drivers.node.maasdriver.driver."
|
||||||
|
"MaasNodeDriver.get_available_images")
|
||||||
|
mock_images.return_value = ['xenial']
|
||||||
|
mock_kernels = mocker.patch(
|
||||||
|
"drydock_provisioner.drivers.node.maasdriver.driver."
|
||||||
|
"MaasNodeDriver.get_available_kernels")
|
||||||
|
mock_kernels.return_value = ['ga-16.04', 'hwe-16.04']
|
||||||
|
|
||||||
|
input_file = input_files.join("invalid_kernel.yaml")
|
||||||
|
design_ref = "file://%s" % str(input_file)
|
||||||
|
|
||||||
|
orch = Orchestrator(
|
||||||
|
state_manager=drydock_state,
|
||||||
|
ingester=deckhand_ingester,
|
||||||
|
enabled_drivers=config.config_mgr.conf.plugins)
|
||||||
|
|
||||||
|
status, site_design = Orchestrator.get_effective_site(orch, design_ref)
|
||||||
|
|
||||||
|
message_list = Validator.valid_platform_selection(
|
||||||
|
site_design, orchestrator=orch)
|
||||||
|
|
||||||
|
for m in message_list:
|
||||||
|
print(m.to_dict())
|
||||||
|
|
||||||
|
msg = message_list[0].to_dict()
|
||||||
|
assert 'invalid kernel lts' in msg.get('message')
|
||||||
|
assert msg.get('error')
|
||||||
|
assert len(message_list) == 1
|
@ -264,8 +264,8 @@ data:
|
|||||||
fstype: 'xfs'
|
fstype: 'xfs'
|
||||||
mount_options: 'defaults'
|
mount_options: 'defaults'
|
||||||
platform:
|
platform:
|
||||||
image: ubuntu_16.04
|
image: 'xenial'
|
||||||
kernel: generic
|
kernel: 'ga-16.04'
|
||||||
kernel_params:
|
kernel_params:
|
||||||
quiet: true
|
quiet: true
|
||||||
console: ttyS2
|
console: ttyS2
|
||||||
|
329
tests/yaml_samples/invalid_kernel.yaml
Normal file
329
tests/yaml_samples/invalid_kernel.yaml
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
#Copyright 2017 AT&T Intellectual Property. All other 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.
|
||||||
|
####################
|
||||||
|
#
|
||||||
|
# bootstrap_seed.yaml - Site server design definition for physical layer
|
||||||
|
#
|
||||||
|
####################
|
||||||
|
# version the schema in this file so consumers can rationally parse it
|
||||||
|
---
|
||||||
|
schema: 'drydock/Region/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: 'sitename'
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
tag_definitions:
|
||||||
|
- tag: 'test'
|
||||||
|
definition_type: 'lshw_xpath'
|
||||||
|
definition: "//node[@id=\"display\"]/'clock units=\"Hz\"' > 1000000000"
|
||||||
|
authorized_keys:
|
||||||
|
- |
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDENeyO5hLPbLLQRZ0oafTYWs1ieo5Q+XgyZQs51Ju
|
||||||
|
jDGc8lKlWsg1/6yei2JewKMgcwG2Buu1eqU92Xn1SvMZLyt9GZURuBkyjcfVc/8GiU5QP1Of8B7CV0c
|
||||||
|
kfUpHWYJ17olTzT61Hgz10ioicBF6cjgQrLNcyn05xoaJHD2Vpf8Unxzi0YzA2e77yRqBo9jJVRaX2q
|
||||||
|
wUJuZrzb62x3zw8Knz6GGSZBn8xRKLaw1SKFpd1hwvL62GfqX5ZBAT1AYTZP1j8GcAoK8AFVn193SEU
|
||||||
|
vjSdUFa+RNWuJhkjBRfylJczIjTIFb5ls0jpbA3bMA9DE7lFKVQl6vVwFmiIVBI1 samplekey
|
||||||
|
---
|
||||||
|
schema: 'drydock/NetworkLink/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: oob
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
bonding:
|
||||||
|
mode: disabled
|
||||||
|
mtu: 1500
|
||||||
|
linkspeed: 100full
|
||||||
|
trunking:
|
||||||
|
mode: disabled
|
||||||
|
default_network: oob
|
||||||
|
allowed_networks:
|
||||||
|
- oob
|
||||||
|
---
|
||||||
|
schema: 'drydock/NetworkLink/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: pxe
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
bonding:
|
||||||
|
mode: disabled
|
||||||
|
mtu: 1500
|
||||||
|
linkspeed: auto
|
||||||
|
trunking:
|
||||||
|
mode: disabled
|
||||||
|
default_network: pxe
|
||||||
|
allowed_networks:
|
||||||
|
- pxe
|
||||||
|
---
|
||||||
|
schema: 'drydock/NetworkLink/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: gp
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
bonding:
|
||||||
|
mode: 802.3ad
|
||||||
|
hash: layer3+4
|
||||||
|
peer_rate: slow
|
||||||
|
mtu: 9000
|
||||||
|
linkspeed: auto
|
||||||
|
trunking:
|
||||||
|
mode: 802.1q
|
||||||
|
default_network: mgmt
|
||||||
|
allowed_networks:
|
||||||
|
- public
|
||||||
|
- mgmt
|
||||||
|
---
|
||||||
|
schema: 'drydock/Rack/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: rack1
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
tor_switches:
|
||||||
|
switch01name:
|
||||||
|
mgmt_ip: 1.1.1.1
|
||||||
|
sdn_api_uri: polo+https://polo-api.web.att.com/switchmgmt?switch=switch01name
|
||||||
|
switch02name:
|
||||||
|
mgmt_ip: 1.1.1.2
|
||||||
|
sdn_api_uri: polo+https://polo-api.web.att.com/switchmgmt?switch=switch02name
|
||||||
|
location:
|
||||||
|
clli: HSTNTXMOCG0
|
||||||
|
grid: EG12
|
||||||
|
local_networks:
|
||||||
|
- pxe-rack1
|
||||||
|
---
|
||||||
|
schema: 'drydock/Network/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: oob
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
cidr: 172.16.100.0/24
|
||||||
|
ranges:
|
||||||
|
- type: static
|
||||||
|
start: 172.16.100.15
|
||||||
|
end: 172.16.100.254
|
||||||
|
dns:
|
||||||
|
domain: ilo.sitename.att.com
|
||||||
|
servers: 172.16.100.10
|
||||||
|
---
|
||||||
|
schema: 'drydock/Network/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: pxe
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
dhcp_relay:
|
||||||
|
self_ip: 172.16.0.4
|
||||||
|
upstream_target: 172.16.5.5
|
||||||
|
mtu: 1500
|
||||||
|
cidr: 172.16.0.0/24
|
||||||
|
ranges:
|
||||||
|
- type: dhcp
|
||||||
|
start: 172.16.0.5
|
||||||
|
end: 172.16.0.254
|
||||||
|
dns:
|
||||||
|
domain: admin.sitename.att.com
|
||||||
|
servers: 172.16.0.10
|
||||||
|
---
|
||||||
|
schema: 'drydock/Network/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: mgmt
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
vlan: '100'
|
||||||
|
mtu: 1500
|
||||||
|
cidr: 172.16.1.0/24
|
||||||
|
ranges:
|
||||||
|
- type: static
|
||||||
|
start: 172.16.1.15
|
||||||
|
end: 172.16.1.254
|
||||||
|
routes:
|
||||||
|
- subnet: 0.0.0.0/0
|
||||||
|
gateway: 172.16.1.1
|
||||||
|
metric: 10
|
||||||
|
dns:
|
||||||
|
domain: mgmt.sitename.example.com
|
||||||
|
servers: 172.16.1.9,172.16.1.10
|
||||||
|
---
|
||||||
|
schema: 'drydock/Network/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: private
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
vlan: '101'
|
||||||
|
mtu: 9000
|
||||||
|
cidr: 172.16.2.0/24
|
||||||
|
ranges:
|
||||||
|
- type: static
|
||||||
|
start: 172.16.2.15
|
||||||
|
end: 172.16.2.254
|
||||||
|
dns:
|
||||||
|
domain: priv.sitename.example.com
|
||||||
|
servers: 172.16.2.9,172.16.2.10
|
||||||
|
---
|
||||||
|
schema: 'drydock/Network/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: public
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
vlan: '102'
|
||||||
|
mtu: 1500
|
||||||
|
cidr: 172.16.3.0/24
|
||||||
|
ranges:
|
||||||
|
- type: static
|
||||||
|
start: 172.16.3.15
|
||||||
|
end: 172.16.3.254
|
||||||
|
routes:
|
||||||
|
- subnet: 0.0.0.0/0
|
||||||
|
gateway: 172.16.3.1
|
||||||
|
metric: 10
|
||||||
|
dns:
|
||||||
|
domain: sitename.example.com
|
||||||
|
servers: 8.8.8.8
|
||||||
|
---
|
||||||
|
schema: 'drydock/HostProfile/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: defaults
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
hardware_profile: HPGen9v3
|
||||||
|
oob:
|
||||||
|
type: ipmi
|
||||||
|
network: oob
|
||||||
|
account: admin
|
||||||
|
credential: admin
|
||||||
|
storage:
|
||||||
|
physical_devices:
|
||||||
|
sda:
|
||||||
|
labels:
|
||||||
|
role: rootdisk
|
||||||
|
partitions:
|
||||||
|
- name: root
|
||||||
|
size: 39%
|
||||||
|
bootable: true
|
||||||
|
filesystem:
|
||||||
|
mountpoint: '/'
|
||||||
|
fstype: 'ext4'
|
||||||
|
mount_options: 'defaults'
|
||||||
|
- name: boot
|
||||||
|
size: 42%
|
||||||
|
bootable: false
|
||||||
|
filesystem:
|
||||||
|
mountpoint: '/boot'
|
||||||
|
fstype: 'ext4'
|
||||||
|
mount_options: 'defaults'
|
||||||
|
sdb:
|
||||||
|
volume_group: 'log_vg'
|
||||||
|
volume_groups:
|
||||||
|
log_vg:
|
||||||
|
logical_volumes:
|
||||||
|
- name: 'log_lv'
|
||||||
|
size: '25%'
|
||||||
|
filesystem:
|
||||||
|
mountpoint: '/var/log'
|
||||||
|
fstype: 'xfs'
|
||||||
|
mount_options: 'defaults'
|
||||||
|
platform:
|
||||||
|
image: xenial
|
||||||
|
kernel: lts
|
||||||
|
kernel_params:
|
||||||
|
quiet: true
|
||||||
|
console: ttyS2
|
||||||
|
metadata:
|
||||||
|
owner_data:
|
||||||
|
foo: bar
|
||||||
|
---
|
||||||
|
schema: 'drydock/BaremetalNode/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: controller01
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
host_profile: defaults
|
||||||
|
interfaces:
|
||||||
|
bond0:
|
||||||
|
networks:
|
||||||
|
- '!private'
|
||||||
|
addressing:
|
||||||
|
- network: pxe
|
||||||
|
address: dhcp
|
||||||
|
- network: mgmt
|
||||||
|
address: 172.16.1.20
|
||||||
|
- network: public
|
||||||
|
address: 172.16.3.20
|
||||||
|
- network: oob
|
||||||
|
address: 172.16.100.20
|
||||||
|
metadata:
|
||||||
|
rack: rack1
|
||||||
|
---
|
||||||
|
schema: 'drydock/HardwareProfile/v1'
|
||||||
|
metadata:
|
||||||
|
schema: 'metadata/Document/v1'
|
||||||
|
name: HPGen9v3
|
||||||
|
storagePolicy: 'cleartext'
|
||||||
|
labels:
|
||||||
|
application: 'drydock'
|
||||||
|
data:
|
||||||
|
vendor: HP
|
||||||
|
generation: '8'
|
||||||
|
hw_version: '3'
|
||||||
|
bios_version: '2.2.3'
|
||||||
|
boot_mode: bios
|
||||||
|
bootstrap_protocol: pxe
|
||||||
|
pxe_interface: 0
|
||||||
|
device_aliases:
|
||||||
|
prim_nic01:
|
||||||
|
address: '0000:00:03.0'
|
||||||
|
dev_type: '82540EM Gigabit Ethernet Controller'
|
||||||
|
bus_type: 'pci'
|
||||||
|
prim_nic02:
|
||||||
|
address: '0000:00:04.0'
|
||||||
|
dev_type: '82540EM Gigabit Ethernet Controller'
|
||||||
|
bus_type: 'pci'
|
||||||
|
primary_boot:
|
||||||
|
address: '2:0.0.0'
|
||||||
|
dev_type: 'VBOX HARDDISK'
|
||||||
|
bus_type: 'scsi'
|
@ -264,8 +264,8 @@ data:
|
|||||||
fstype: 'xfs'
|
fstype: 'xfs'
|
||||||
mount_options: 'defaults'
|
mount_options: 'defaults'
|
||||||
platform:
|
platform:
|
||||||
image: ubuntu_16.04
|
image: 'xenial'
|
||||||
kernel: generic
|
kernel: 'ga-16.04'
|
||||||
kernel_params:
|
kernel_params:
|
||||||
quiet: true
|
quiet: true
|
||||||
console: ttyS2
|
console: ttyS2
|
||||||
|
Loading…
x
Reference in New Issue
Block a user