Merge "Adds Ironic test_baremetal_basic_ops scenario test"
This commit is contained in:
commit
dd80c02bcd
@ -101,14 +101,32 @@
|
|||||||
# Options defined in tempest.config
|
# Options defined in tempest.config
|
||||||
#
|
#
|
||||||
|
|
||||||
# Catalog type of the baremetal provisioning service. (string
|
# Catalog type of the baremetal provisioning service (string
|
||||||
# value)
|
# value)
|
||||||
#catalog_type=baremetal
|
#catalog_type=baremetal
|
||||||
|
|
||||||
|
# Whether the Ironic nova-compute driver is enabled (boolean
|
||||||
|
# value)
|
||||||
|
#driver_enabled=false
|
||||||
|
|
||||||
# The endpoint type to use for the baremetal provisioning
|
# The endpoint type to use for the baremetal provisioning
|
||||||
# service. (string value)
|
# service (string value)
|
||||||
#endpoint_type=publicURL
|
#endpoint_type=publicURL
|
||||||
|
|
||||||
|
# Timeout for Ironic node to completely provision (integer
|
||||||
|
# value)
|
||||||
|
#active_timeout=300
|
||||||
|
|
||||||
|
# Timeout for association of Nova instance and Ironic node
|
||||||
|
# (integer value)
|
||||||
|
#association_timeout=10
|
||||||
|
|
||||||
|
# Timeout for Ironic power transitions. (integer value)
|
||||||
|
#power_timeout=20
|
||||||
|
|
||||||
|
# Timeout for unprovisioning an Ironic node. (integer value)
|
||||||
|
#unprovision_timeout=20
|
||||||
|
|
||||||
|
|
||||||
[boto]
|
[boto]
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ python-novaclient>=2.17.0
|
|||||||
python-neutronclient>=2.3.4,<3
|
python-neutronclient>=2.3.4,<3
|
||||||
python-cinderclient>=1.0.6
|
python-cinderclient>=1.0.6
|
||||||
python-heatclient>=0.2.3
|
python-heatclient>=0.2.3
|
||||||
|
python-ironicclient
|
||||||
python-saharaclient>=0.6.0
|
python-saharaclient>=0.6.0
|
||||||
python-swiftclient>=1.6
|
python-swiftclient>=1.6
|
||||||
testresources>=0.2.4
|
testresources>=0.2.4
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
import cinderclient.client
|
import cinderclient.client
|
||||||
import glanceclient
|
import glanceclient
|
||||||
import heatclient.client
|
import heatclient.client
|
||||||
|
import ironicclient.client
|
||||||
import keystoneclient.exceptions
|
import keystoneclient.exceptions
|
||||||
import keystoneclient.v2_0.client
|
import keystoneclient.v2_0.client
|
||||||
import neutronclient.v2_0.client
|
import neutronclient.v2_0.client
|
||||||
@ -463,6 +464,7 @@ class OfficialClientManager(manager.Manager):
|
|||||||
NOVACLIENT_VERSION = '2'
|
NOVACLIENT_VERSION = '2'
|
||||||
CINDERCLIENT_VERSION = '1'
|
CINDERCLIENT_VERSION = '1'
|
||||||
HEATCLIENT_VERSION = '1'
|
HEATCLIENT_VERSION = '1'
|
||||||
|
IRONICCLIENT_VERSION = '1'
|
||||||
|
|
||||||
def __init__(self, username, password, tenant_name):
|
def __init__(self, username, password, tenant_name):
|
||||||
# FIXME(andreaf) Auth provider for client_type 'official' is
|
# FIXME(andreaf) Auth provider for client_type 'official' is
|
||||||
@ -472,6 +474,7 @@ class OfficialClientManager(manager.Manager):
|
|||||||
# super cares for credentials validation
|
# super cares for credentials validation
|
||||||
super(OfficialClientManager, self).__init__(
|
super(OfficialClientManager, self).__init__(
|
||||||
username=username, password=password, tenant_name=tenant_name)
|
username=username, password=password, tenant_name=tenant_name)
|
||||||
|
self.baremetal_client = self._get_baremetal_client()
|
||||||
self.compute_client = self._get_compute_client(username,
|
self.compute_client = self._get_compute_client(username,
|
||||||
password,
|
password,
|
||||||
tenant_name)
|
tenant_name)
|
||||||
@ -492,6 +495,22 @@ class OfficialClientManager(manager.Manager):
|
|||||||
password,
|
password,
|
||||||
tenant_name)
|
tenant_name)
|
||||||
|
|
||||||
|
def _get_roles(self):
|
||||||
|
keystone_admin = self._get_identity_client(
|
||||||
|
CONF.identity.admin_username,
|
||||||
|
CONF.identity.admin_password,
|
||||||
|
CONF.identity.admin_tenant_name)
|
||||||
|
|
||||||
|
username = self.credentials['username']
|
||||||
|
tenant_name = self.credentials['tenant_name']
|
||||||
|
user_id = keystone_admin.users.find(name=username).id
|
||||||
|
tenant_id = keystone_admin.tenants.find(name=tenant_name).id
|
||||||
|
|
||||||
|
roles = keystone_admin.roles.roles_for_user(
|
||||||
|
user=user_id, tenant=tenant_id)
|
||||||
|
|
||||||
|
return [r.name for r in roles]
|
||||||
|
|
||||||
def _get_compute_client(self, username, password, tenant_name):
|
def _get_compute_client(self, username, password, tenant_name):
|
||||||
# Novaclient will not execute operations for anyone but the
|
# Novaclient will not execute operations for anyone but the
|
||||||
# identified user, so a new client needs to be created for
|
# identified user, so a new client needs to be created for
|
||||||
@ -613,6 +632,34 @@ class OfficialClientManager(manager.Manager):
|
|||||||
auth_url=auth_url,
|
auth_url=auth_url,
|
||||||
insecure=dscv)
|
insecure=dscv)
|
||||||
|
|
||||||
|
def _get_baremetal_client(self):
|
||||||
|
# ironic client is currently intended to by used by admin users
|
||||||
|
roles = self._get_roles()
|
||||||
|
if CONF.identity.admin_role not in roles:
|
||||||
|
return None
|
||||||
|
|
||||||
|
auth_url = CONF.identity.uri
|
||||||
|
api_version = self.IRONICCLIENT_VERSION
|
||||||
|
insecure = CONF.identity.disable_ssl_certificate_validation
|
||||||
|
service_type = CONF.baremetal.catalog_type
|
||||||
|
endpoint_type = CONF.baremetal.endpoint_type
|
||||||
|
creds = {
|
||||||
|
'os_username': self.credentials['username'],
|
||||||
|
'os_password': self.credentials['password'],
|
||||||
|
'os_tenant_name': self.credentials['tenant_name']
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return ironicclient.client.get_client(
|
||||||
|
api_version=api_version,
|
||||||
|
os_auth_url=auth_url,
|
||||||
|
insecure=insecure,
|
||||||
|
os_service_type=service_type,
|
||||||
|
os_endpoint_type=endpoint_type,
|
||||||
|
**creds)
|
||||||
|
except keystoneclient.exceptions.EndpointNotFound:
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_network_client(self):
|
def _get_network_client(self):
|
||||||
# The intended configuration is for the network client to have
|
# The intended configuration is for the network client to have
|
||||||
# admin privileges and indicate for whom resources are being
|
# admin privileges and indicate for whom resources are being
|
||||||
|
@ -844,13 +844,29 @@ baremetal_group = cfg.OptGroup(name='baremetal',
|
|||||||
BaremetalGroup = [
|
BaremetalGroup = [
|
||||||
cfg.StrOpt('catalog_type',
|
cfg.StrOpt('catalog_type',
|
||||||
default='baremetal',
|
default='baremetal',
|
||||||
help="Catalog type of the baremetal provisioning service."),
|
help="Catalog type of the baremetal provisioning service"),
|
||||||
|
cfg.BoolOpt('driver_enabled',
|
||||||
|
default=False,
|
||||||
|
help="Whether the Ironic nova-compute driver is enabled"),
|
||||||
cfg.StrOpt('endpoint_type',
|
cfg.StrOpt('endpoint_type',
|
||||||
default='publicURL',
|
default='publicURL',
|
||||||
choices=['public', 'admin', 'internal',
|
choices=['public', 'admin', 'internal',
|
||||||
'publicURL', 'adminURL', 'internalURL'],
|
'publicURL', 'adminURL', 'internalURL'],
|
||||||
help="The endpoint type to use for the baremetal provisioning "
|
help="The endpoint type to use for the baremetal provisioning "
|
||||||
"service."),
|
"service"),
|
||||||
|
cfg.IntOpt('active_timeout',
|
||||||
|
default=300,
|
||||||
|
help="Timeout for Ironic node to completely provision"),
|
||||||
|
cfg.IntOpt('association_timeout',
|
||||||
|
default=10,
|
||||||
|
help="Timeout for association of Nova instance and Ironic "
|
||||||
|
"node"),
|
||||||
|
cfg.IntOpt('power_timeout',
|
||||||
|
default=20,
|
||||||
|
help="Timeout for Ironic power transitions."),
|
||||||
|
cfg.IntOpt('unprovision_timeout',
|
||||||
|
default=20,
|
||||||
|
help="Timeout for unprovisioning an Ironic node.")
|
||||||
]
|
]
|
||||||
|
|
||||||
cli_group = cfg.OptGroup(name='cli', title="cli Configuration Options")
|
cli_group = cfg.OptGroup(name='cli', title="cli Configuration Options")
|
||||||
|
@ -19,6 +19,7 @@ import os
|
|||||||
import six
|
import six
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from ironicclient import exc as ironic_exceptions
|
||||||
import netaddr
|
import netaddr
|
||||||
from neutronclient.common import exceptions as exc
|
from neutronclient.common import exceptions as exc
|
||||||
from novaclient import exceptions as nova_exceptions
|
from novaclient import exceptions as nova_exceptions
|
||||||
@ -71,6 +72,7 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
username, password, tenant_name)
|
username, password, tenant_name)
|
||||||
cls.compute_client = cls.manager.compute_client
|
cls.compute_client = cls.manager.compute_client
|
||||||
cls.image_client = cls.manager.image_client
|
cls.image_client = cls.manager.image_client
|
||||||
|
cls.baremetal_client = cls.manager.baremetal_client
|
||||||
cls.identity_client = cls.manager.identity_client
|
cls.identity_client = cls.manager.identity_client
|
||||||
cls.network_client = cls.manager.network_client
|
cls.network_client = cls.manager.network_client
|
||||||
cls.volume_client = cls.manager.volume_client
|
cls.volume_client = cls.manager.volume_client
|
||||||
@ -283,7 +285,7 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
return rules
|
return rules
|
||||||
|
|
||||||
def create_server(self, client=None, name=None, image=None, flavor=None,
|
def create_server(self, client=None, name=None, image=None, flavor=None,
|
||||||
create_kwargs={}):
|
wait=True, create_kwargs={}):
|
||||||
if client is None:
|
if client is None:
|
||||||
client = self.compute_client
|
client = self.compute_client
|
||||||
if name is None:
|
if name is None:
|
||||||
@ -318,7 +320,8 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
server = client.servers.create(name, image, flavor, **create_kwargs)
|
server = client.servers.create(name, image, flavor, **create_kwargs)
|
||||||
self.assertEqual(server.name, name)
|
self.assertEqual(server.name, name)
|
||||||
self.set_resource(name, server)
|
self.set_resource(name, server)
|
||||||
self.status_timeout(client.servers, server.id, 'ACTIVE')
|
if wait:
|
||||||
|
self.status_timeout(client.servers, server.id, 'ACTIVE')
|
||||||
# The instance retrieved on creation is missing network
|
# The instance retrieved on creation is missing network
|
||||||
# details, necessitating retrieval after it becomes active to
|
# details, necessitating retrieval after it becomes active to
|
||||||
# ensure correct details.
|
# ensure correct details.
|
||||||
@ -439,6 +442,80 @@ class OfficialClientTest(tempest.test.BaseTestCase):
|
|||||||
LOG.debug("image:%s" % self.image)
|
LOG.debug("image:%s" % self.image)
|
||||||
|
|
||||||
|
|
||||||
|
class BaremetalScenarioTest(OfficialClientTest):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(BaremetalScenarioTest, cls).setUpClass()
|
||||||
|
|
||||||
|
if (not CONF.service_available.ironic or
|
||||||
|
not CONF.baremetal.driver_enabled):
|
||||||
|
msg = 'Ironic not available or Ironic compute driver not enabled'
|
||||||
|
raise cls.skipException(msg)
|
||||||
|
|
||||||
|
# use an admin client manager for baremetal client
|
||||||
|
username, password, tenant = cls.admin_credentials()
|
||||||
|
manager = clients.OfficialClientManager(username, password, tenant)
|
||||||
|
cls.baremetal_client = manager.baremetal_client
|
||||||
|
|
||||||
|
# allow any issues obtaining the node list to raise early
|
||||||
|
cls.baremetal_client.node.list()
|
||||||
|
|
||||||
|
def _node_state_timeout(self, node_id, state_attr,
|
||||||
|
target_states, timeout=10, interval=1):
|
||||||
|
if not isinstance(target_states, list):
|
||||||
|
target_states = [target_states]
|
||||||
|
|
||||||
|
def check_state():
|
||||||
|
node = self.get_node(node_id=node_id)
|
||||||
|
if getattr(node, state_attr) in target_states:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not tempest.test.call_until_true(
|
||||||
|
check_state, timeout, interval):
|
||||||
|
msg = ("Timed out waiting for node %s to reach %s state(s) %s" %
|
||||||
|
(node_id, state_attr, target_states))
|
||||||
|
raise exceptions.TimeoutException(msg)
|
||||||
|
|
||||||
|
def wait_provisioning_state(self, node_id, state, timeout):
|
||||||
|
self._node_state_timeout(
|
||||||
|
node_id=node_id, state_attr='provision_state',
|
||||||
|
target_states=state, timeout=timeout)
|
||||||
|
|
||||||
|
def wait_power_state(self, node_id, state):
|
||||||
|
self._node_state_timeout(
|
||||||
|
node_id=node_id, state_attr='power_state',
|
||||||
|
target_states=state, timeout=CONF.baremetal.power_timeout)
|
||||||
|
|
||||||
|
def wait_node(self, instance_id):
|
||||||
|
"""Waits for a node to be associated with instance_id."""
|
||||||
|
def _get_node():
|
||||||
|
node = None
|
||||||
|
try:
|
||||||
|
node = self.get_node(instance_id=instance_id)
|
||||||
|
except ironic_exceptions.HTTPNotFound:
|
||||||
|
pass
|
||||||
|
return node is not None
|
||||||
|
|
||||||
|
if not tempest.test.call_until_true(
|
||||||
|
_get_node, CONF.baremetal.association_timeout, 1):
|
||||||
|
msg = ('Timed out waiting to get Ironic node by instance id %s'
|
||||||
|
% instance_id)
|
||||||
|
raise exceptions.TimeoutException(msg)
|
||||||
|
|
||||||
|
def get_node(self, node_id=None, instance_id=None):
|
||||||
|
if node_id:
|
||||||
|
return self.baremetal_client.node.get(node_id)
|
||||||
|
elif instance_id:
|
||||||
|
return self.baremetal_client.node.get_by_instance_uuid(instance_id)
|
||||||
|
|
||||||
|
def get_ports(self, node_id):
|
||||||
|
ports = []
|
||||||
|
for port in self.baremetal_client.node.list_ports(node_id):
|
||||||
|
ports.append(self.baremetal_client.port.get(port.uuid))
|
||||||
|
return ports
|
||||||
|
|
||||||
|
|
||||||
class NetworkScenarioTest(OfficialClientTest):
|
class NetworkScenarioTest(OfficialClientTest):
|
||||||
"""
|
"""
|
||||||
Base class for network scenario tests
|
Base class for network scenario tests
|
||||||
|
147
tempest/scenario/test_baremetal_basic_ops.py
Normal file
147
tempest/scenario/test_baremetal_basic_ops.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
#
|
||||||
|
# Copyright 2014 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
from tempest import config
|
||||||
|
from tempest.openstack.common import log as logging
|
||||||
|
from tempest.scenario import manager
|
||||||
|
from tempest import test
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# power/provision states as of icehouse
|
||||||
|
class PowerStates(object):
|
||||||
|
"""Possible power states of an Ironic node."""
|
||||||
|
POWER_ON = 'power on'
|
||||||
|
POWER_OFF = 'power off'
|
||||||
|
REBOOT = 'rebooting'
|
||||||
|
SUSPEND = 'suspended'
|
||||||
|
|
||||||
|
|
||||||
|
class ProvisionStates(object):
|
||||||
|
"""Possible provision states of an Ironic node."""
|
||||||
|
NOSTATE = None
|
||||||
|
INIT = 'initializing'
|
||||||
|
ACTIVE = 'active'
|
||||||
|
BUILDING = 'building'
|
||||||
|
DEPLOYWAIT = 'wait call-back'
|
||||||
|
DEPLOYING = 'deploying'
|
||||||
|
DEPLOYFAIL = 'deploy failed'
|
||||||
|
DEPLOYDONE = 'deploy complete'
|
||||||
|
DELETING = 'deleting'
|
||||||
|
DELETED = 'deleted'
|
||||||
|
ERROR = 'error'
|
||||||
|
|
||||||
|
|
||||||
|
class BaremetalBasicOptsPXESSH(manager.BaremetalScenarioTest):
|
||||||
|
"""
|
||||||
|
This smoke test tests the pxe_ssh Ironic driver. It follows this basic
|
||||||
|
set of operations:
|
||||||
|
* Creates a keypair
|
||||||
|
* Boots an instance using the keypair
|
||||||
|
* Monitors the associated Ironic node for power and
|
||||||
|
expected state transitions
|
||||||
|
* Validates Ironic node's driver_info has been properly
|
||||||
|
updated
|
||||||
|
* Validates Ironic node's port data has been properly updated
|
||||||
|
* Verifies SSH connectivity using created keypair via fixed IP
|
||||||
|
* Associates a floating ip
|
||||||
|
* Verifies SSH connectivity using created keypair via floating IP
|
||||||
|
* Deletes instance
|
||||||
|
* Monitors the associated Ironic node for power and
|
||||||
|
expected state transitions
|
||||||
|
"""
|
||||||
|
def add_keypair(self):
|
||||||
|
self.keypair = self.create_keypair()
|
||||||
|
|
||||||
|
def add_floating_ip(self):
|
||||||
|
floating_ip = self.compute_client.floating_ips.create()
|
||||||
|
self.instance.add_floating_ip(floating_ip)
|
||||||
|
return floating_ip.ip
|
||||||
|
|
||||||
|
def verify_connectivity(self, ip=None):
|
||||||
|
if ip:
|
||||||
|
dest = self.get_remote_client(ip)
|
||||||
|
else:
|
||||||
|
dest = self.get_remote_client(self.instance)
|
||||||
|
dest.validate_authentication()
|
||||||
|
|
||||||
|
def validate_driver_info(self):
|
||||||
|
f_id = self.instance.flavor['id']
|
||||||
|
flavor_extra = self.compute_client.flavors.get(f_id).get_keys()
|
||||||
|
driver_info = self.node.driver_info
|
||||||
|
self.assertEqual(driver_info['pxe_deploy_kernel'],
|
||||||
|
flavor_extra['baremetal:deploy_kernel_id'])
|
||||||
|
self.assertEqual(driver_info['pxe_deploy_ramdisk'],
|
||||||
|
flavor_extra['baremetal:deploy_ramdisk_id'])
|
||||||
|
self.assertEqual(driver_info['pxe_image_source'],
|
||||||
|
self.instance.image['id'])
|
||||||
|
|
||||||
|
def validate_ports(self):
|
||||||
|
for port in self.get_ports(self.node.uuid):
|
||||||
|
n_port_id = port.extra['vif_port_id']
|
||||||
|
n_port = self.network_client.show_port(n_port_id)['port']
|
||||||
|
self.assertEqual(n_port['device_id'], self.instance.id)
|
||||||
|
self.assertEqual(n_port['mac_address'], port.address)
|
||||||
|
|
||||||
|
def boot_instance(self):
|
||||||
|
create_kwargs = {
|
||||||
|
'key_name': self.keypair.id
|
||||||
|
}
|
||||||
|
self.instance = self.create_server(
|
||||||
|
wait=False, create_kwargs=create_kwargs)
|
||||||
|
|
||||||
|
self.set_resource('instance', self.instance)
|
||||||
|
|
||||||
|
self.wait_node(self.instance.id)
|
||||||
|
self.node = self.get_node(instance_id=self.instance.id)
|
||||||
|
|
||||||
|
self.wait_power_state(self.node.uuid, PowerStates.POWER_ON)
|
||||||
|
|
||||||
|
self.wait_provisioning_state(
|
||||||
|
self.node.uuid,
|
||||||
|
[ProvisionStates.DEPLOYWAIT, ProvisionStates.ACTIVE],
|
||||||
|
timeout=15)
|
||||||
|
|
||||||
|
self.wait_provisioning_state(self.node.uuid, ProvisionStates.ACTIVE,
|
||||||
|
timeout=CONF.baremetal.active_timeout)
|
||||||
|
|
||||||
|
self.status_timeout(
|
||||||
|
self.compute_client.servers, self.instance.id, 'ACTIVE')
|
||||||
|
|
||||||
|
self.node = self.get_node(instance_id=self.instance.id)
|
||||||
|
self.instance = self.compute_client.servers.get(self.instance.id)
|
||||||
|
|
||||||
|
def terminate_instance(self):
|
||||||
|
self.instance.delete()
|
||||||
|
self.remove_resource('instance')
|
||||||
|
self.wait_power_state(self.node.uuid, PowerStates.POWER_OFF)
|
||||||
|
self.wait_provisioning_state(
|
||||||
|
self.node.uuid,
|
||||||
|
ProvisionStates.NOSTATE,
|
||||||
|
timeout=CONF.baremetal.unprovision_timeout)
|
||||||
|
|
||||||
|
@test.services('baremetal', 'compute', 'image', 'network')
|
||||||
|
def test_baremetal_server_ops(self):
|
||||||
|
self.add_keypair()
|
||||||
|
self.boot_instance()
|
||||||
|
self.validate_driver_info()
|
||||||
|
self.validate_ports()
|
||||||
|
self.verify_connectivity()
|
||||||
|
floating_ip = self.add_floating_ip()
|
||||||
|
self.verify_connectivity(ip=floating_ip)
|
||||||
|
self.terminate_instance()
|
@ -94,6 +94,7 @@ def services(*args, **kwargs):
|
|||||||
service_list = {
|
service_list = {
|
||||||
'compute': CONF.service_available.nova,
|
'compute': CONF.service_available.nova,
|
||||||
'image': CONF.service_available.glance,
|
'image': CONF.service_available.glance,
|
||||||
|
'baremetal': CONF.service_available.ironic,
|
||||||
'volume': CONF.service_available.cinder,
|
'volume': CONF.service_available.cinder,
|
||||||
'orchestration': CONF.service_available.heat,
|
'orchestration': CONF.service_available.heat,
|
||||||
# NOTE(mtreinish) nova-network will provide networking functionality
|
# NOTE(mtreinish) nova-network will provide networking functionality
|
||||||
|
Loading…
Reference in New Issue
Block a user