Provide a PXE NodeDriver for the Baremetal driver
This patch implements a PXE NodeDriver class within the Baremetal provisioning framework, which provides a means for deploying machine images using TFTP and PXE. This patch relies on functionality provided by the nova-baremetal-deploy-helper utility, implemented in review 15830. blueprint general-bare-metal-provisioning-framework. Change-Id: I8d849601186e3dc13f10382857ff2bbc1ff1026d
This commit is contained in:
parent
335f0f2eab
commit
5327259765
534
nova/tests/baremetal/test_pxe.py
Normal file
534
nova/tests/baremetal/test_pxe.py
Normal file
@ -0,0 +1,534 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# coding=utf-8
|
||||
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2012 NTT DOCOMO, INC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Tests for baremetal pxe driver.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import mox
|
||||
from testtools.matchers import Contains
|
||||
from testtools.matchers import MatchesAll
|
||||
from testtools.matchers import Not
|
||||
from testtools.matchers import StartsWith
|
||||
|
||||
from nova import exception
|
||||
from nova.openstack.common import cfg
|
||||
from nova import test
|
||||
from nova.tests.baremetal.db import base as bm_db_base
|
||||
from nova.tests.baremetal.db import utils as bm_db_utils
|
||||
from nova.tests.image import fake as fake_image
|
||||
from nova.tests import utils
|
||||
from nova.virt.baremetal import db
|
||||
from nova.virt.baremetal import pxe
|
||||
from nova.virt.baremetal import utils as bm_utils
|
||||
from nova.virt.disk import api as disk_api
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
COMMON_FLAGS = dict(
|
||||
firewall_driver='nova.virt.baremetal.fake.FakeFirewallDriver',
|
||||
host='test_host',
|
||||
)
|
||||
|
||||
BAREMETAL_FLAGS = dict(
|
||||
driver='nova.virt.baremetal.pxe.PXE',
|
||||
instance_type_extra_specs=['cpu_arch:test', 'test_spec:test_value'],
|
||||
power_manager='nova.virt.baremetal.fake.FakePowerManager',
|
||||
vif_driver='nova.virt.baremetal.fake.FakeVifDriver',
|
||||
volume_driver='nova.virt.baremetal.fake.FakeVolumeDriver',
|
||||
group='baremetal',
|
||||
)
|
||||
|
||||
|
||||
class BareMetalPXETestCase(bm_db_base.BMDBTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BareMetalPXETestCase, self).setUp()
|
||||
self.flags(**COMMON_FLAGS)
|
||||
self.flags(**BAREMETAL_FLAGS)
|
||||
self.driver = pxe.PXE()
|
||||
|
||||
fake_image.stub_out_image_service(self.stubs)
|
||||
self.addCleanup(fake_image.FakeImageService_reset)
|
||||
self.context = utils.get_test_admin_context()
|
||||
self.test_block_device_info = None,
|
||||
self.instance = utils.get_test_instance()
|
||||
self.test_network_info = utils.get_test_network_info(),
|
||||
self.node_info = bm_db_utils.new_bm_node(
|
||||
id=123,
|
||||
service_host='test_host',
|
||||
cpus=2,
|
||||
memory_mb=2048,
|
||||
prov_mac_address='11:11:11:11:11:11',
|
||||
)
|
||||
self.nic_info = [
|
||||
{'address': '22:22:22:22:22:22', 'datapath_id': '0x1',
|
||||
'port_no': 1},
|
||||
{'address': '33:33:33:33:33:33', 'datapath_id': '0x2',
|
||||
'port_no': 2},
|
||||
]
|
||||
|
||||
def _create_node(self):
|
||||
self.node = db.bm_node_create(self.context, self.node_info)
|
||||
for nic in self.nic_info:
|
||||
db.bm_interface_create(
|
||||
self.context,
|
||||
self.node['id'],
|
||||
nic['address'],
|
||||
nic['datapath_id'],
|
||||
nic['port_no'],
|
||||
)
|
||||
self.instance['node'] = self.node['id']
|
||||
self.spawn_params = dict(
|
||||
admin_password='test_pass',
|
||||
block_device_info=self.test_block_device_info,
|
||||
context=self.context,
|
||||
image_meta=utils.get_test_image_info(None,
|
||||
self.instance),
|
||||
injected_files=[('/fake/path', 'hello world')],
|
||||
instance=self.instance,
|
||||
network_info=self.test_network_info,
|
||||
)
|
||||
|
||||
|
||||
class PXEClassMethodsTestCase(BareMetalPXETestCase):
|
||||
|
||||
def test_build_pxe_config(self):
|
||||
args = {
|
||||
'deployment_id': 'aaa',
|
||||
'deployment_key': 'bbb',
|
||||
'deployment_iscsi_iqn': 'ccc',
|
||||
'deployment_aki_path': 'ddd',
|
||||
'deployment_ari_path': 'eee',
|
||||
'aki_path': 'fff',
|
||||
'ari_path': 'ggg',
|
||||
}
|
||||
config = pxe.build_pxe_config(**args)
|
||||
self.assertThat(config, StartsWith('default deploy'))
|
||||
|
||||
# deploy bits are in the deploy section
|
||||
start = config.index('label deploy')
|
||||
end = config.index('label boot')
|
||||
self.assertThat(config[start:end], MatchesAll(
|
||||
Contains('kernel ddd'),
|
||||
Contains('initrd=eee'),
|
||||
Contains('deployment_id=aaa'),
|
||||
Contains('deployment_key=bbb'),
|
||||
Contains('iscsi_target_iqn=ccc'),
|
||||
Not(Contains('kernel fff')),
|
||||
))
|
||||
|
||||
# boot bits are in the boot section
|
||||
start = config.index('label boot')
|
||||
self.assertThat(config[start:], MatchesAll(
|
||||
Contains('kernel fff'),
|
||||
Contains('initrd=ggg'),
|
||||
Not(Contains('kernel ddd')),
|
||||
))
|
||||
|
||||
def test_build_network_config(self):
|
||||
net = utils.get_test_network_info(1)
|
||||
config = pxe.build_network_config(net)
|
||||
self.assertIn('eth0', config)
|
||||
self.assertNotIn('eth1', config)
|
||||
self.assertIn('hwaddress ether fake', config)
|
||||
self.assertNotIn('hwaddress ether aa:bb:cc:dd', config)
|
||||
|
||||
net[0][1]['mac'] = 'aa:bb:cc:dd'
|
||||
config = pxe.build_network_config(net)
|
||||
self.assertIn('hwaddress ether aa:bb:cc:dd', config)
|
||||
|
||||
net = utils.get_test_network_info(2)
|
||||
config = pxe.build_network_config(net)
|
||||
self.assertIn('eth0', config)
|
||||
self.assertIn('eth1', config)
|
||||
|
||||
def test_build_network_config_dhcp(self):
|
||||
self.flags(
|
||||
net_config_template='$pybasedir/nova/virt/baremetal/'
|
||||
'net-dhcp.ubuntu.template',
|
||||
group='baremetal',
|
||||
)
|
||||
net = utils.get_test_network_info()
|
||||
net[0][1]['ips'][0]['ip'] = '1.2.3.4'
|
||||
config = pxe.build_network_config(net)
|
||||
self.assertIn('iface eth0 inet dhcp', config)
|
||||
self.assertNotIn('address 1.2.3.4', config)
|
||||
|
||||
def test_build_network_config_static(self):
|
||||
self.flags(
|
||||
net_config_template='$pybasedir/nova/virt/baremetal/'
|
||||
'net-static.ubuntu.template',
|
||||
group='baremetal',
|
||||
)
|
||||
net = utils.get_test_network_info()
|
||||
net[0][1]['ips'][0]['ip'] = '1.2.3.4'
|
||||
config = pxe.build_network_config(net)
|
||||
self.assertIn('iface eth0 inet static', config)
|
||||
self.assertIn('address 1.2.3.4', config)
|
||||
|
||||
def test_image_dir_path(self):
|
||||
self.assertEqual(
|
||||
pxe.get_image_dir_path(self.instance),
|
||||
os.path.join(CONF.instances_path, 'instance-00000001'))
|
||||
|
||||
def test_image_file_path(self):
|
||||
self.assertEqual(
|
||||
pxe.get_image_file_path(self.instance),
|
||||
os.path.join(
|
||||
CONF.instances_path, 'instance-00000001', 'disk'))
|
||||
|
||||
def test_pxe_config_file_path(self):
|
||||
self.instance['uuid'] = 'aaaa-bbbb-cccc'
|
||||
self.assertEqual(
|
||||
pxe.get_pxe_config_file_path(self.instance),
|
||||
os.path.join(CONF.baremetal.tftp_root,
|
||||
'aaaa-bbbb-cccc', 'config'))
|
||||
|
||||
def test_pxe_mac_path(self):
|
||||
self.assertEqual(
|
||||
pxe.get_pxe_mac_path('23:45:67:89:AB'),
|
||||
os.path.join(CONF.baremetal.tftp_root,
|
||||
'pxelinux.cfg', '01-23-45-67-89-ab'))
|
||||
|
||||
def test_get_instance_deploy_ids(self):
|
||||
self.instance['extra_specs'] = {
|
||||
'deploy_kernel_id': 'aaaa',
|
||||
'deploy_ramdisk_id': 'bbbb',
|
||||
}
|
||||
self.flags(deploy_kernel="fail", group='baremetal')
|
||||
self.flags(deploy_ramdisk="fail", group='baremetal')
|
||||
|
||||
self.assertEqual(
|
||||
pxe.get_deploy_aki_id(self.instance), 'aaaa')
|
||||
self.assertEqual(
|
||||
pxe.get_deploy_ari_id(self.instance), 'bbbb')
|
||||
|
||||
def test_get_default_deploy_ids(self):
|
||||
self.instance['extra_specs'] = {}
|
||||
self.flags(deploy_kernel="aaaa", group='baremetal')
|
||||
self.flags(deploy_ramdisk="bbbb", group='baremetal')
|
||||
|
||||
self.assertEqual(
|
||||
pxe.get_deploy_aki_id(self.instance), 'aaaa')
|
||||
self.assertEqual(
|
||||
pxe.get_deploy_ari_id(self.instance), 'bbbb')
|
||||
|
||||
def test_get_partition_sizes(self):
|
||||
# m1.tiny: 10GB root, 0GB swap
|
||||
self.instance['instance_type_id'] = 1
|
||||
sizes = pxe.get_partition_sizes(self.instance)
|
||||
self.assertEqual(sizes[0], 10240)
|
||||
self.assertEqual(sizes[1], 1)
|
||||
|
||||
# kinda.big: 40GB root, 1GB swap
|
||||
ref = utils.get_test_instance_type()
|
||||
self.instance['instance_type_id'] = ref['id']
|
||||
self.instance['root_gb'] = ref['root_gb']
|
||||
sizes = pxe.get_partition_sizes(self.instance)
|
||||
self.assertEqual(sizes[0], 40960)
|
||||
self.assertEqual(sizes[1], 1024)
|
||||
|
||||
def test_get_tftp_image_info(self):
|
||||
# Raises an exception when options are neither specified
|
||||
# on the instance nor in configuration file
|
||||
CONF.baremetal.deploy_kernel = None
|
||||
CONF.baremetal.deploy_ramdisk = None
|
||||
self.assertRaises(exception.NovaException,
|
||||
pxe.get_tftp_image_info,
|
||||
self.instance)
|
||||
|
||||
# Even if the instance includes kernel_id and ramdisk_id,
|
||||
# we still need deploy_kernel_id and deploy_ramdisk_id.
|
||||
# If those aren't present in instance[], and not specified in
|
||||
# config file, then we raise an exception.
|
||||
self.instance['kernel_id'] = 'aaaa'
|
||||
self.instance['ramdisk_id'] = 'bbbb'
|
||||
self.assertRaises(exception.NovaException,
|
||||
pxe.get_tftp_image_info,
|
||||
self.instance)
|
||||
|
||||
# If an instance doesn't specify deploy_kernel_id or deploy_ramdisk_id,
|
||||
# but defaults are set in the config file, we should use those.
|
||||
|
||||
# Here, we confirm both that all four values were set
|
||||
# and that the proper paths are getting set for all of them
|
||||
CONF.baremetal.deploy_kernel = 'cccc'
|
||||
CONF.baremetal.deploy_ramdisk = 'dddd'
|
||||
base = os.path.join(CONF.baremetal.tftp_root, self.instance['uuid'])
|
||||
res = pxe.get_tftp_image_info(self.instance)
|
||||
expected = {
|
||||
'kernel': ['aaaa', os.path.join(base, 'kernel')],
|
||||
'ramdisk': ['bbbb', os.path.join(base, 'ramdisk')],
|
||||
'deploy_kernel': ['cccc', os.path.join(base, 'deploy_kernel')],
|
||||
'deploy_ramdisk': ['dddd',
|
||||
os.path.join(base, 'deploy_ramdisk')],
|
||||
}
|
||||
self.assertEqual(res, expected)
|
||||
|
||||
# If deploy_kernel_id and deploy_ramdisk_id are specified on
|
||||
# image extra_specs, this should override any default configuration.
|
||||
# Note that it is passed on the 'instance' object, despite being
|
||||
# inherited from the instance_types_extra_specs table.
|
||||
extra_specs = {
|
||||
'deploy_kernel_id': 'eeee',
|
||||
'deploy_ramdisk_id': 'ffff',
|
||||
}
|
||||
self.instance['extra_specs'] = extra_specs
|
||||
res = pxe.get_tftp_image_info(self.instance)
|
||||
self.assertEqual(res['deploy_kernel'][0], 'eeee')
|
||||
self.assertEqual(res['deploy_ramdisk'][0], 'ffff')
|
||||
|
||||
|
||||
class PXEPrivateMethodsTestCase(BareMetalPXETestCase):
|
||||
|
||||
def test_collect_mac_addresses(self):
|
||||
self._create_node()
|
||||
address_list = [nic['address'] for nic in self.nic_info]
|
||||
address_list.append(self.node_info['prov_mac_address'])
|
||||
address_list.sort()
|
||||
macs = self.driver._collect_mac_addresses(self.context, self.node)
|
||||
self.assertEqual(macs, address_list)
|
||||
|
||||
def test_generate_udev_rules(self):
|
||||
self._create_node()
|
||||
address_list = [nic['address'] for nic in self.nic_info]
|
||||
address_list.append(self.node_info['prov_mac_address'])
|
||||
|
||||
rules = self.driver._generate_udev_rules(self.context, self.node)
|
||||
for address in address_list:
|
||||
self.assertIn('ATTR{address}=="%s"' % address, rules)
|
||||
|
||||
def test_cache_tftp_images(self):
|
||||
self.instance['kernel_id'] = 'aaaa'
|
||||
self.instance['ramdisk_id'] = 'bbbb'
|
||||
extra_specs = {
|
||||
'deploy_kernel_id': 'cccc',
|
||||
'deploy_ramdisk_id': 'dddd',
|
||||
}
|
||||
self.instance['extra_specs'] = extra_specs
|
||||
image_info = pxe.get_tftp_image_info(self.instance)
|
||||
|
||||
self.mox.StubOutWithMock(os, 'makedirs')
|
||||
self.mox.StubOutWithMock(os.path, 'exists')
|
||||
os.makedirs(os.path.join(CONF.baremetal.tftp_root,
|
||||
self.instance['uuid'])).AndReturn(True)
|
||||
for uuid, path in [image_info[label] for label in image_info]:
|
||||
os.path.exists(path).AndReturn(True)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver._cache_tftp_images(
|
||||
self.context, self.instance, image_info)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_cache_image(self):
|
||||
self.mox.StubOutWithMock(os, 'makedirs')
|
||||
self.mox.StubOutWithMock(os.path, 'exists')
|
||||
os.makedirs(pxe.get_image_dir_path(self.instance)).\
|
||||
AndReturn(True)
|
||||
os.path.exists(pxe.get_image_file_path(self.instance)).\
|
||||
AndReturn(True)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
image_meta = utils.get_test_image_info(
|
||||
self.context, self.instance)
|
||||
self.driver._cache_image(
|
||||
self.context, self.instance, image_meta)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_inject_into_image(self):
|
||||
# NOTE(deva): we could also test this method by stubbing
|
||||
# nova.virt.disk.api._inject_*_into_fs
|
||||
self._create_node()
|
||||
files = []
|
||||
files.append(('/etc/udev/rules.d/70-persistent-net.rules',
|
||||
self.driver._generate_udev_rules(self.context, self.node)))
|
||||
self.instance['hostname'] = 'fake hostname'
|
||||
files.append(('/etc/hostname', 'fake hostname'))
|
||||
self.instance['key_data'] = 'fake ssh key'
|
||||
net_info = utils.get_test_network_info(1)
|
||||
net = pxe.build_network_config(net_info)
|
||||
admin_password = 'fake password'
|
||||
|
||||
self.mox.StubOutWithMock(disk_api, 'inject_data')
|
||||
disk_api.inject_data(
|
||||
admin_password=admin_password,
|
||||
image=pxe.get_image_file_path(self.instance),
|
||||
key='fake ssh key',
|
||||
metadata=None,
|
||||
partition=None,
|
||||
net=net,
|
||||
files=files, # this is what we're really testing
|
||||
).AndReturn(True)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver._inject_into_image(
|
||||
self.context, self.node, self.instance,
|
||||
network_info=net_info,
|
||||
admin_password=admin_password,
|
||||
injected_files=None)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
|
||||
class PXEPublicMethodsTestCase(BareMetalPXETestCase):
|
||||
|
||||
def test_cache_images(self):
|
||||
self._create_node()
|
||||
self.mox.StubOutWithMock(pxe, "get_tftp_image_info")
|
||||
self.mox.StubOutWithMock(self.driver, "_cache_tftp_images")
|
||||
self.mox.StubOutWithMock(self.driver, "_cache_image")
|
||||
self.mox.StubOutWithMock(self.driver, "_inject_into_image")
|
||||
|
||||
pxe.get_tftp_image_info(self.instance).AndReturn([])
|
||||
self.driver._cache_tftp_images(self.context, self.instance, [])
|
||||
self.driver._cache_image(self.context, self.instance, [])
|
||||
self.driver._inject_into_image(self.context, self.node, self.instance,
|
||||
self.test_network_info, None, '')
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver.cache_images(
|
||||
self.context, self.node, self.instance,
|
||||
admin_password='',
|
||||
image_meta=[],
|
||||
injected_files=None,
|
||||
network_info=self.test_network_info,
|
||||
)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_destroy_images(self):
|
||||
self._create_node()
|
||||
self.mox.StubOutWithMock(os, 'unlink')
|
||||
|
||||
os.unlink(pxe.get_image_file_path(self.instance))
|
||||
os.unlink(pxe.get_image_dir_path(self.instance))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver.destroy_images(self.context, self.node, self.instance)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_activate_bootloader(self):
|
||||
self._create_node()
|
||||
macs = [nic['address'] for nic in self.nic_info]
|
||||
macs.append(self.node_info['prov_mac_address'])
|
||||
macs.sort()
|
||||
image_info = {
|
||||
'deploy_kernel': [None, 'aaaa'],
|
||||
'deploy_ramdisk': [None, 'bbbb'],
|
||||
'kernel': [None, 'cccc'],
|
||||
'ramdisk': [None, 'dddd'],
|
||||
}
|
||||
self.instance['uuid'] = 'fake-uuid'
|
||||
iqn = "iqn-%s" % self.instance['uuid']
|
||||
pxe_config = 'this is a fake pxe config'
|
||||
pxe_path = pxe.get_pxe_config_file_path(self.instance)
|
||||
image_path = pxe.get_image_file_path(self.instance)
|
||||
|
||||
self.mox.StubOutWithMock(pxe, 'get_tftp_image_info')
|
||||
self.mox.StubOutWithMock(pxe, 'get_partition_sizes')
|
||||
self.mox.StubOutWithMock(bm_utils, 'random_alnum')
|
||||
self.mox.StubOutWithMock(db, 'bm_deployment_create')
|
||||
self.mox.StubOutWithMock(pxe, 'build_pxe_config')
|
||||
self.mox.StubOutWithMock(bm_utils, 'write_to_file')
|
||||
self.mox.StubOutWithMock(bm_utils, 'create_link_without_raise')
|
||||
|
||||
pxe.get_tftp_image_info(self.instance).AndReturn(image_info)
|
||||
pxe.get_partition_sizes(self.instance).AndReturn((0, 0))
|
||||
bm_utils.random_alnum(32).AndReturn('alnum')
|
||||
db.bm_deployment_create(
|
||||
self.context, 'alnum', image_path, pxe_path, 0, 0).\
|
||||
AndReturn(1234)
|
||||
pxe.build_pxe_config(
|
||||
1234, 'alnum', iqn, 'aaaa', 'bbbb', 'cccc', 'dddd').\
|
||||
AndReturn(pxe_config)
|
||||
bm_utils.write_to_file(pxe_path, pxe_config)
|
||||
for mac in macs:
|
||||
bm_utils.create_link_without_raise(
|
||||
pxe_path, pxe.get_pxe_mac_path(mac))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver.activate_bootloader(
|
||||
self.context, self.node, self.instance)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_deactivate_bootloader(self):
|
||||
self._create_node()
|
||||
macs = [nic['address'] for nic in self.nic_info]
|
||||
macs.append(self.node_info['prov_mac_address'])
|
||||
macs.sort()
|
||||
image_info = {
|
||||
'deploy_kernel': [None, 'aaaa'],
|
||||
'deploy_ramdisk': [None, 'bbbb'],
|
||||
'kernel': [None, 'cccc'],
|
||||
'ramdisk': [None, 'dddd'],
|
||||
}
|
||||
self.instance['uuid'] = 'fake-uuid'
|
||||
pxe_path = pxe.get_pxe_config_file_path(self.instance)
|
||||
|
||||
self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise')
|
||||
self.mox.StubOutWithMock(pxe, 'get_tftp_image_info')
|
||||
self.mox.StubOutWithMock(self.driver, '_collect_mac_addresses')
|
||||
|
||||
pxe.get_tftp_image_info(self.instance).AndReturn(image_info)
|
||||
for uuid, path in [image_info[label] for label in image_info]:
|
||||
bm_utils.unlink_without_raise(path)
|
||||
bm_utils.unlink_without_raise(pxe_path)
|
||||
self.driver._collect_mac_addresses(self.context, self.node).\
|
||||
AndReturn(macs)
|
||||
for mac in macs:
|
||||
bm_utils.unlink_without_raise(pxe.get_pxe_mac_path(mac))
|
||||
bm_utils.unlink_without_raise(
|
||||
os.path.join(CONF.baremetal.tftp_root, 'fake-uuid'))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver.deactivate_bootloader(
|
||||
self.context, self.node, self.instance)
|
||||
self.mox.VerifyAll()
|
||||
|
||||
def test_deactivate_bootloader_for_nonexistent_instance(self):
|
||||
self._create_node()
|
||||
macs = [nic['address'] for nic in self.nic_info]
|
||||
macs.append(self.node_info['prov_mac_address'])
|
||||
macs.sort()
|
||||
image_info = {
|
||||
'deploy_kernel': [None, 'aaaa'],
|
||||
'deploy_ramdisk': [None, 'bbbb'],
|
||||
'kernel': [None, 'cccc'],
|
||||
'ramdisk': [None, 'dddd'],
|
||||
}
|
||||
self.instance['uuid'] = 'fake-uuid'
|
||||
pxe_path = pxe.get_pxe_config_file_path(self.instance)
|
||||
|
||||
self.mox.StubOutWithMock(bm_utils, 'unlink_without_raise')
|
||||
self.mox.StubOutWithMock(pxe, 'get_tftp_image_info')
|
||||
self.mox.StubOutWithMock(self.driver, '_collect_mac_addresses')
|
||||
|
||||
pxe.get_tftp_image_info(self.instance).\
|
||||
AndRaise(exception.NovaException)
|
||||
bm_utils.unlink_without_raise(pxe_path)
|
||||
self.driver._collect_mac_addresses(self.context, self.node).\
|
||||
AndRaise(exception.DBError)
|
||||
bm_utils.unlink_without_raise(
|
||||
os.path.join(CONF.baremetal.tftp_root, 'fake-uuid'))
|
||||
self.mox.ReplayAll()
|
||||
|
||||
self.driver.deactivate_bootloader(
|
||||
self.context, self.node, self.instance)
|
||||
self.mox.VerifyAll()
|
36
nova/tests/baremetal/test_utils.py
Normal file
36
nova/tests/baremetal/test_utils.py
Normal file
@ -0,0 +1,36 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# coding=utf-8
|
||||
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Tests for baremetal utils
|
||||
"""
|
||||
|
||||
import mox
|
||||
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.virt.baremetal import utils
|
||||
|
||||
|
||||
class BareMetalUtilsTestCase(test.TestCase):
|
||||
|
||||
def test_random_alnum(self):
|
||||
s = utils.random_alnum(10)
|
||||
self.assertEqual(len(s), 10)
|
||||
s = utils.random_alnum(100)
|
||||
self.assertEqual(len(s), 100)
|
21
nova/virt/baremetal/net-dhcp.ubuntu.template
Normal file
21
nova/virt/baremetal/net-dhcp.ubuntu.template
Normal file
@ -0,0 +1,21 @@
|
||||
# Injected by Nova on instance boot
|
||||
#
|
||||
# This file describes the network interfaces available on your system
|
||||
# and how to activate them. For more information, see interfaces(5).
|
||||
|
||||
# The loopback network interface
|
||||
auto lo
|
||||
iface lo inet loopback
|
||||
|
||||
#for $ifc in $interfaces
|
||||
auto ${ifc.name}
|
||||
iface ${ifc.name} inet dhcp
|
||||
#if $ifc.hwaddress
|
||||
hwaddress ether ${ifc.hwaddress}
|
||||
#end if
|
||||
|
||||
#if $use_ipv6
|
||||
iface ${ifc.name} inet6 dhcp
|
||||
#end if
|
||||
|
||||
#end for
|
@ -12,7 +12,6 @@ auto ${ifc.name}
|
||||
iface ${ifc.name} inet static
|
||||
address ${ifc.address}
|
||||
netmask ${ifc.netmask}
|
||||
broadcast ${ifc.broadcast}
|
||||
gateway ${ifc.gateway}
|
||||
#if $ifc.dns
|
||||
dns-nameservers ${ifc.dns}
|
460
nova/virt/baremetal/pxe.py
Normal file
460
nova/virt/baremetal/pxe.py
Normal file
@ -0,0 +1,460 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2012 NTT DOCOMO, INC.
|
||||
# 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 for PXE bare-metal nodes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from nova.compute import instance_types
|
||||
from nova import exception
|
||||
from nova.openstack.common import cfg
|
||||
from nova.openstack.common import fileutils
|
||||
from nova.openstack.common import log as logging
|
||||
from nova import utils
|
||||
from nova.virt.baremetal import base
|
||||
from nova.virt.baremetal import db
|
||||
from nova.virt.baremetal import utils as bm_utils
|
||||
from nova.virt.disk import api as disk
|
||||
|
||||
|
||||
pxe_opts = [
|
||||
cfg.StrOpt('dnsmasq_pid_dir',
|
||||
default='$state_path/baremetal/dnsmasq',
|
||||
help='path to directory stores pidfiles of dnsmasq'),
|
||||
cfg.StrOpt('dnsmasq_lease_dir',
|
||||
default='$state_path/baremetal/dnsmasq',
|
||||
help='path to directory stores leasefiles of dnsmasq'),
|
||||
cfg.StrOpt('deploy_kernel',
|
||||
help='Default kernel image ID used in deployment phase'),
|
||||
cfg.StrOpt('deploy_ramdisk',
|
||||
help='Default ramdisk image ID used in deployment phase'),
|
||||
cfg.StrOpt('net_config_template',
|
||||
default='$pybasedir/nova/virt/baremetal/'
|
||||
'net-dhcp.ubuntu.template',
|
||||
help='Template file for injected network config'),
|
||||
cfg.StrOpt('pxe_append_params',
|
||||
help='additional append parameters for baremetal PXE boot'),
|
||||
cfg.StrOpt('pxe_config_template',
|
||||
default='$pybasedir/nova/virt/baremetal/pxe_config.template',
|
||||
help='Template file for PXE configuration'),
|
||||
cfg.StrOpt('pxe_interface',
|
||||
default='eth0'),
|
||||
cfg.StrOpt('pxe_path',
|
||||
default='/usr/lib/syslinux/pxelinux.0',
|
||||
help='path to pxelinux.0'),
|
||||
]
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
baremetal_group = cfg.OptGroup(name='baremetal',
|
||||
title='Baremetal Options')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_group(baremetal_group)
|
||||
CONF.register_opts(pxe_opts, baremetal_group)
|
||||
|
||||
|
||||
CHEETAH = None
|
||||
|
||||
|
||||
def _get_cheetah():
|
||||
global CHEETAH
|
||||
if CHEETAH is None:
|
||||
from Cheetah.Template import Template as CHEETAH
|
||||
return CHEETAH
|
||||
|
||||
|
||||
def build_pxe_config(deployment_id, deployment_key, deployment_iscsi_iqn,
|
||||
deployment_aki_path, deployment_ari_path,
|
||||
aki_path, ari_path):
|
||||
"""Build the PXE config file for a node
|
||||
|
||||
This method builds the PXE boot configuration file for a node,
|
||||
given all the required parameters.
|
||||
|
||||
The resulting file has both a "deploy" and "boot" label, which correspond
|
||||
to the two phases of booting. This may be extended later.
|
||||
|
||||
"""
|
||||
LOG.debug(_("Building PXE config for deployment %s.") % deployment_id)
|
||||
pxe_options = {
|
||||
'deployment_id': deployment_id,
|
||||
'deployment_key': deployment_key,
|
||||
'deployment_iscsi_iqn': deployment_iscsi_iqn,
|
||||
'deployment_aki_path': deployment_aki_path,
|
||||
'deployment_ari_path': deployment_ari_path,
|
||||
'aki_path': aki_path,
|
||||
'ari_path': ari_path,
|
||||
'pxe_append_params': CONF.baremetal.pxe_append_params,
|
||||
}
|
||||
cheetah = _get_cheetah()
|
||||
pxe_config = str(cheetah(
|
||||
open(CONF.baremetal.pxe_config_template).read(),
|
||||
searchList=[{'pxe_options': pxe_options,
|
||||
'ROOT': '${ROOT}',
|
||||
}]))
|
||||
return pxe_config
|
||||
|
||||
|
||||
def build_network_config(network_info):
|
||||
# TODO(deva): fix assumption that device names begin with "eth"
|
||||
# and fix assumption about ordering
|
||||
try:
|
||||
assert isinstance(network_info, list)
|
||||
except AssertionError:
|
||||
network_info = [network_info]
|
||||
interfaces = []
|
||||
for id, (network, mapping) in enumerate(network_info):
|
||||
address_v6 = None
|
||||
gateway_v6 = None
|
||||
netmask_v6 = None
|
||||
if CONF.use_ipv6:
|
||||
address_v6 = mapping['ip6s'][0]['ip']
|
||||
netmask_v6 = mapping['ip6s'][0]['netmask']
|
||||
gateway_v6 = mapping['gateway_v6']
|
||||
interface = {
|
||||
'name': 'eth%d' % id,
|
||||
'hwaddress': mapping['mac'],
|
||||
'address': mapping['ips'][0]['ip'],
|
||||
'gateway': mapping['gateway'],
|
||||
'netmask': mapping['ips'][0]['netmask'],
|
||||
'dns': ' '.join(mapping['dns']),
|
||||
'address_v6': address_v6,
|
||||
'gateway_v6': gateway_v6,
|
||||
'netmask_v6': netmask_v6,
|
||||
}
|
||||
interfaces.append(interface)
|
||||
|
||||
cheetah = _get_cheetah()
|
||||
network_config = str(cheetah(
|
||||
open(CONF.baremetal.net_config_template).read(),
|
||||
searchList=[
|
||||
{'interfaces': interfaces,
|
||||
'use_ipv6': CONF.use_ipv6,
|
||||
}
|
||||
]))
|
||||
return network_config
|
||||
|
||||
|
||||
def get_deploy_aki_id(instance):
|
||||
return instance.get('extra_specs', {}).\
|
||||
get('deploy_kernel_id', CONF.baremetal.deploy_kernel)
|
||||
|
||||
|
||||
def get_deploy_ari_id(instance):
|
||||
return instance.get('extra_specs', {}).\
|
||||
get('deploy_ramdisk_id', CONF.baremetal.deploy_ramdisk)
|
||||
|
||||
|
||||
def get_image_dir_path(instance):
|
||||
"""Generate the dir for an instances disk"""
|
||||
return os.path.join(CONF.instances_path, instance['name'])
|
||||
|
||||
|
||||
def get_image_file_path(instance):
|
||||
"""Generate the full path for an instances disk"""
|
||||
return os.path.join(CONF.instances_path, instance['name'], 'disk')
|
||||
|
||||
|
||||
def get_pxe_config_file_path(instance):
|
||||
"""Generate the path for an instances PXE config file"""
|
||||
return os.path.join(CONF.baremetal.tftp_root, instance['uuid'], 'config')
|
||||
|
||||
|
||||
def get_partition_sizes(instance):
|
||||
type_id = instance['instance_type_id']
|
||||
root_mb = instance['root_gb'] * 1024
|
||||
|
||||
# NOTE(deva): is there a way to get swap_mb directly from instance?
|
||||
swap_mb = instance_types.get_instance_type(type_id)['swap']
|
||||
|
||||
# NOTE(deva): For simpler code paths on the deployment side,
|
||||
# we always create a swap partition. If the flavor
|
||||
# does not specify any swap, we default to 1MB
|
||||
if swap_mb < 1:
|
||||
swap_mb = 1
|
||||
|
||||
return (root_mb, swap_mb)
|
||||
|
||||
|
||||
def get_pxe_mac_path(mac):
|
||||
"""Convert a MAC address into a PXE config file name"""
|
||||
return os.path.join(
|
||||
CONF.baremetal.tftp_root,
|
||||
'pxelinux.cfg',
|
||||
"01-" + mac.replace(":", "-").lower()
|
||||
)
|
||||
|
||||
|
||||
def get_tftp_image_info(instance):
|
||||
"""Generate the paths for tftp files for this instance
|
||||
|
||||
Raises NovaException if
|
||||
- instance does not contain kernel_id or ramdisk_id
|
||||
- deploy_kernel_id or deploy_ramdisk_id can not be read from
|
||||
instance['extra_specs'] and defaults are not set
|
||||
|
||||
"""
|
||||
image_info = {
|
||||
'kernel': [None, None],
|
||||
'ramdisk': [None, None],
|
||||
'deploy_kernel': [None, None],
|
||||
'deploy_ramdisk': [None, None],
|
||||
}
|
||||
try:
|
||||
image_info['kernel'][0] = str(instance['kernel_id'])
|
||||
image_info['ramdisk'][0] = str(instance['ramdisk_id'])
|
||||
image_info['deploy_kernel'][0] = get_deploy_aki_id(instance)
|
||||
image_info['deploy_ramdisk'][0] = get_deploy_ari_id(instance)
|
||||
except KeyError as e:
|
||||
pass
|
||||
|
||||
missing_labels = []
|
||||
for label in image_info.keys():
|
||||
(uuid, path) = image_info[label]
|
||||
if uuid is None:
|
||||
missing_labels.append(label)
|
||||
else:
|
||||
image_info[label][1] = os.path.join(CONF.baremetal.tftp_root,
|
||||
instance['uuid'], label)
|
||||
if missing_labels:
|
||||
raise exception.NovaException(_(
|
||||
"Can not activate PXE bootloader. The following boot parameters "
|
||||
"were not passed to baremetal driver: %s") % missing_labels)
|
||||
return image_info
|
||||
|
||||
|
||||
class PXE(base.NodeDriver):
|
||||
"""PXE bare metal driver"""
|
||||
|
||||
def __init__(self):
|
||||
super(PXE, self).__init__()
|
||||
|
||||
def _collect_mac_addresses(self, context, node):
|
||||
macs = []
|
||||
macs.append(db.bm_node_get(context, node['id'])['prov_mac_address'])
|
||||
for nic in db.bm_interface_get_all_by_bm_node_id(context, node['id']):
|
||||
if nic['address']:
|
||||
macs.append(nic['address'])
|
||||
macs.sort()
|
||||
return macs
|
||||
|
||||
def _generate_udev_rules(self, context, node):
|
||||
# TODO(deva): fix assumption that device names begin with "eth"
|
||||
# and fix assumption of ordering
|
||||
macs = self._collect_mac_addresses(context, node)
|
||||
rules = ''
|
||||
for (i, mac) in enumerate(macs):
|
||||
rules += 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' \
|
||||
'ATTR{address}=="%(mac)s", ATTR{dev_id}=="0x0", ' \
|
||||
'ATTR{type}=="1", KERNEL=="eth*", NAME="%(name)s"\n' \
|
||||
% {'mac': mac.lower(),
|
||||
'name': 'eth%d' % i,
|
||||
}
|
||||
return rules
|
||||
|
||||
def _cache_tftp_images(self, context, instance, image_info):
|
||||
"""Fetch the necessary kernels and ramdisks for the instance."""
|
||||
fileutils.ensure_tree(
|
||||
os.path.join(CONF.baremetal.tftp_root, instance['uuid']))
|
||||
|
||||
LOG.debug(_("Fetching kernel and ramdisk for instance %s") %
|
||||
instance['name'])
|
||||
for label in image_info.keys():
|
||||
(uuid, path) = image_info[label]
|
||||
bm_utils.cache_image(
|
||||
context=context,
|
||||
target=path,
|
||||
image_id=uuid,
|
||||
user_id=instance['user_id'],
|
||||
project_id=instance['project_id'],
|
||||
)
|
||||
|
||||
def _cache_image(self, context, instance, image_meta):
|
||||
"""Fetch the instance's image from Glance
|
||||
|
||||
This method pulls the relevant AMI and associated kernel and ramdisk,
|
||||
and the deploy kernel and ramdisk from Glance, and writes them
|
||||
to the appropriate places on local disk.
|
||||
|
||||
Both sets of kernel and ramdisk are needed for PXE booting, so these
|
||||
are stored under CONF.baremetal.tftp_root.
|
||||
|
||||
At present, the AMI is cached and certain files are injected.
|
||||
Debian/ubuntu-specific assumptions are made regarding the injected
|
||||
files. In a future revision, this functionality will be replaced by a
|
||||
more scalable and os-agnostic approach: the deployment ramdisk will
|
||||
fetch from Glance directly, and write its own last-mile configuration.
|
||||
|
||||
"""
|
||||
fileutils.ensure_tree(get_image_dir_path(instance))
|
||||
image_path = get_image_file_path(instance)
|
||||
|
||||
LOG.debug(_("Fetching image %(ami)s for instance %(name)s") %
|
||||
{'ami': image_meta['id'], 'name': instance['name']})
|
||||
bm_utils.cache_image(context=context,
|
||||
target=image_path,
|
||||
image_id=image_meta['id'],
|
||||
user_id=instance['user_id'],
|
||||
project_id=instance['project_id']
|
||||
)
|
||||
|
||||
return [image_meta['id'], image_path]
|
||||
|
||||
def _inject_into_image(self, context, node, instance, network_info,
|
||||
injected_files=None, admin_password=None):
|
||||
"""Inject last-mile configuration into instances image
|
||||
|
||||
Much of this method is a hack around DHCP and cloud-init
|
||||
not working together with baremetal provisioning yet.
|
||||
|
||||
"""
|
||||
# NOTE(deva): We assume that if we're not using a kernel,
|
||||
# then the target partition is the first partition
|
||||
partition = None
|
||||
if not instance['kernel_id']:
|
||||
partition = "1"
|
||||
|
||||
ssh_key = None
|
||||
if 'key_data' in instance and instance['key_data']:
|
||||
ssh_key = str(instance['key_data'])
|
||||
|
||||
if injected_files is None:
|
||||
injected_files = []
|
||||
|
||||
net_config = build_network_config(network_info)
|
||||
udev_rules = self._generate_udev_rules(context, node)
|
||||
injected_files.append(
|
||||
('/etc/udev/rules.d/70-persistent-net.rules', udev_rules))
|
||||
|
||||
if instance['hostname']:
|
||||
injected_files.append(('/etc/hostname', instance['hostname']))
|
||||
|
||||
LOG.debug(_("Injecting files into image for instance %(name)s") %
|
||||
{'name': instance['name']})
|
||||
|
||||
bm_utils.inject_into_image(
|
||||
image=get_image_file_path(instance),
|
||||
key=ssh_key,
|
||||
net=net_config,
|
||||
metadata=instance['metadata'],
|
||||
admin_password=admin_password,
|
||||
files=injected_files,
|
||||
partition=partition,
|
||||
)
|
||||
|
||||
def cache_images(self, context, node, instance,
|
||||
admin_password, image_meta, injected_files, network_info):
|
||||
"""Prepare all the images for this instance"""
|
||||
tftp_image_info = get_tftp_image_info(instance)
|
||||
self._cache_tftp_images(context, instance, tftp_image_info)
|
||||
|
||||
self._cache_image(context, instance, image_meta)
|
||||
self._inject_into_image(context, node, instance, network_info,
|
||||
injected_files, admin_password)
|
||||
|
||||
def destroy_images(self, context, node, instance):
|
||||
"""Delete instance's image file"""
|
||||
bm_utils.unlink_without_raise(get_image_file_path(instance))
|
||||
bm_utils.unlink_without_raise(get_image_dir_path(instance))
|
||||
|
||||
def activate_bootloader(self, context, node, instance):
|
||||
"""Configure PXE boot loader for an instance
|
||||
|
||||
Kernel and ramdisk images are downloaded by cache_tftp_images,
|
||||
and stored in /tftpboot/{uuid}/
|
||||
|
||||
This method writes the instances config file, and then creates
|
||||
symlinks for each MAC address in the instance.
|
||||
|
||||
By default, the complete layout looks like this:
|
||||
|
||||
/tftpboot/
|
||||
./{uuid}/
|
||||
kernel
|
||||
ramdisk
|
||||
deploy_kernel
|
||||
deploy_ramdisk
|
||||
config
|
||||
./pxelinux.cfg/
|
||||
{mac} -> ../{uuid}/config
|
||||
|
||||
"""
|
||||
image_info = get_tftp_image_info(instance)
|
||||
(root_mb, swap_mb) = get_partition_sizes(instance)
|
||||
pxe_config_file_path = get_pxe_config_file_path(instance)
|
||||
image_file_path = get_image_file_path(instance)
|
||||
|
||||
deployment_key = bm_utils.random_alnum(32)
|
||||
deployment_iscsi_iqn = "iqn-%s" % instance['uuid']
|
||||
deployment_id = db.bm_deployment_create(
|
||||
context,
|
||||
deployment_key,
|
||||
image_file_path,
|
||||
pxe_config_file_path,
|
||||
root_mb,
|
||||
swap_mb
|
||||
)
|
||||
pxe_config = build_pxe_config(
|
||||
deployment_id,
|
||||
deployment_key,
|
||||
deployment_iscsi_iqn,
|
||||
image_info['deploy_kernel'][1],
|
||||
image_info['deploy_ramdisk'][1],
|
||||
image_info['kernel'][1],
|
||||
image_info['ramdisk'][1],
|
||||
)
|
||||
bm_utils.write_to_file(pxe_config_file_path, pxe_config)
|
||||
|
||||
macs = self._collect_mac_addresses(context, node)
|
||||
for mac in macs:
|
||||
mac_path = get_pxe_mac_path(mac)
|
||||
bm_utils.unlink_without_raise(mac_path)
|
||||
bm_utils.create_link_without_raise(pxe_config_file_path, mac_path)
|
||||
|
||||
def deactivate_bootloader(self, context, node, instance):
|
||||
"""Delete PXE bootloader images and config"""
|
||||
try:
|
||||
image_info = get_tftp_image_info(instance)
|
||||
except exception.NovaException:
|
||||
pass
|
||||
else:
|
||||
for label in image_info.keys():
|
||||
(uuid, path) = image_info[label]
|
||||
bm_utils.unlink_without_raise(path)
|
||||
|
||||
bm_utils.unlink_without_raise(get_pxe_config_file_path(instance))
|
||||
try:
|
||||
macs = self._collect_mac_addresses(context, node)
|
||||
except exception.DBError:
|
||||
pass
|
||||
else:
|
||||
for mac in macs:
|
||||
bm_utils.unlink_without_raise(get_pxe_mac_path(mac))
|
||||
|
||||
bm_utils.unlink_without_raise(
|
||||
os.path.join(CONF.baremetal.tftp_root, instance['uuid']))
|
||||
|
||||
def activate_node(self, context, node, instance):
|
||||
pass
|
||||
|
||||
def deactivate_node(self, context, node, instance):
|
||||
pass
|
11
nova/virt/baremetal/pxe_config.template
Normal file
11
nova/virt/baremetal/pxe_config.template
Normal file
@ -0,0 +1,11 @@
|
||||
default deploy
|
||||
|
||||
label deploy
|
||||
kernel ${pxe_options.deployment_aki_path}
|
||||
append initrd=${pxe_options.deployment_ari_path} selinux=0 disk=cciss/c0d0,sda,hda,vda iscsi_target_iqn=${pxe_options.deployment_iscsi_iqn} deployment_id=${pxe_options.deployment_id} deployment_key=${pxe_options.deployment_key} ${pxe_options.pxe_append_params}
|
||||
ipappend 3
|
||||
|
||||
|
||||
label boot
|
||||
kernel ${pxe_options.aki_path}
|
||||
append initrd=${pxe_options.ari_path} root=${ROOT} ro ${pxe_options.pxe_append_params}
|
@ -58,3 +58,10 @@ def create_link_without_raise(source, link):
|
||||
except OSError:
|
||||
LOG.exception(_("Failed to create symlink from %(source)s to %(link)s")
|
||||
% locals())
|
||||
|
||||
|
||||
def random_alnum(count):
|
||||
import random
|
||||
import string
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
return "".join(random.choice(chars) for _ in range(count))
|
||||
|
Loading…
Reference in New Issue
Block a user