Merge "[Ironic] Add callback script for deploy with Ironic"
This commit is contained in:
commit
672841256f
83
fuel_agent/cmd/ironic_callback.py
Executable file
83
fuel_agent/cmd/ironic_callback.py
Executable file
@ -0,0 +1,83 @@
|
||||
# Copyright 2015 Mirantis, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from fuel_agent.utils import utils
|
||||
|
||||
|
||||
def _process_error(message):
|
||||
sys.stderr.write(message)
|
||||
sys.stderr.write('\n')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
"""Script informs Ironic that bootstrap loading is done.
|
||||
|
||||
There are three mandatory parameters in kernel command line.
|
||||
Ironic prepares these two:
|
||||
'api-url' - URL of Ironic API service,
|
||||
'deployment_id' - UUID of the node in Ironic.
|
||||
Passed from PXE boot loader:
|
||||
'BOOTIF' - MAC address of the boot interface,
|
||||
http://www.syslinux.org/wiki/index.php/SYSLINUX#APPEND_-
|
||||
Example: api_url=http://192.168.122.184:6385
|
||||
deployment_id=eeeeeeee-dddd-cccc-bbbb-aaaaaaaaaaaa
|
||||
BOOTIF=01-88-99-aa-bb-cc-dd
|
||||
"""
|
||||
kernel_params = utils.parse_kernel_cmdline()
|
||||
api_url = kernel_params.get('api-url')
|
||||
deployment_id = kernel_params.get('deployment_id')
|
||||
if api_url is None or deployment_id is None:
|
||||
_process_error('Mandatory parameter ("api-url" or "deployment_id") is '
|
||||
'missing.')
|
||||
|
||||
bootif = kernel_params.get('BOOTIF')
|
||||
if bootif is None:
|
||||
_process_error('Cannot define boot interface, "BOOTIF" parameter is '
|
||||
'missing.')
|
||||
|
||||
# The leading `01-' denotes the device type (Ethernet) and is not a part of
|
||||
# the MAC address
|
||||
boot_mac = bootif[3:].replace('-', ':')
|
||||
for n in range(10):
|
||||
boot_ip = utils.get_interface_ip(boot_mac)
|
||||
if boot_ip is not None:
|
||||
break
|
||||
time.sleep(10)
|
||||
else:
|
||||
_process_error('Cannot find IP address of boot interface.')
|
||||
|
||||
data = {"address": boot_ip,
|
||||
"status": "ready",
|
||||
"error_message": "no errors"}
|
||||
|
||||
passthru = '%(api-url)s/v1/nodes/%(deployment_id)s/vendor_passthru' \
|
||||
'/pass_deploy_info' % {'api-url': api_url,
|
||||
'deployment_id': deployment_id}
|
||||
try:
|
||||
resp = requests.post(passthru, data=json.dumps(data),
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'})
|
||||
except Exception as e:
|
||||
_process_error(str(e))
|
||||
|
||||
if resp.status_code != 202:
|
||||
_process_error('Wrong status code %d returned from Ironic API' %
|
||||
resp.status_code)
|
@ -28,6 +28,36 @@ from fuel_agent.utils import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
_LO_DEVICE = """lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state \
|
||||
UNKNOWN group default
|
||||
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
|
||||
inet 127.0.0.1/8 scope host lo
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 ::1/128 scope host
|
||||
valid_lft forever preferred_lft forever
|
||||
"""
|
||||
|
||||
_ETH_DEVICE_NO_IP = """eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc \
|
||||
pfifo_fast state UP group default qlen 1000
|
||||
link/ether 08:60:6e:6f:7d:a5 brd ff:ff:ff:ff:ff:ff
|
||||
"""
|
||||
|
||||
_ETH_DEVICE_IP = """inet 172.18.204.10/25 brd 172.18.204.127 scope global \
|
||||
eth0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet6 fe80::a60:6eff:fe6f:6da2/64 scope link
|
||||
valid_lft forever preferred_lft forever
|
||||
"""
|
||||
|
||||
_ETH_DEVICE = _ETH_DEVICE_NO_IP + _ETH_DEVICE_IP
|
||||
|
||||
_DOCKER_DEVICE = """docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 \
|
||||
qdisc noqueue state DOWN group default
|
||||
link/ether 56:86:7a:fe:97:79 brd ff:ff:ff:ff:ff:ff
|
||||
inet 172.17.42.1/16 scope global docker0
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class ExecuteTestCase(unittest2.TestCase):
|
||||
"""This class is partly based on the same class in openstack/ironic."""
|
||||
@ -387,3 +417,61 @@ class TestUdevRulesBlacklisting(unittest2.TestCase):
|
||||
self.assertFalse(mock_os.remove.called)
|
||||
self.assertFalse(mock_os.rename.called)
|
||||
mock_udev.assert_called_once_with()
|
||||
|
||||
|
||||
@mock.patch.object(utils, 'execute')
|
||||
class GetIPTestCase(unittest2.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(GetIPTestCase, self).setUp()
|
||||
self.mac = '08:60:6e:6f:7d:a5'
|
||||
self.cmd = ('ip', 'addr', 'show', 'scope', 'global')
|
||||
|
||||
def _build_out(self, lines):
|
||||
out = ''
|
||||
for num, line in enumerate(lines, start=1):
|
||||
out += str(num) + ': ' + line
|
||||
return out
|
||||
|
||||
def test_get_interface_ip(self, mock_execute):
|
||||
lines = _LO_DEVICE, _ETH_DEVICE, _DOCKER_DEVICE
|
||||
out = self._build_out(lines)
|
||||
mock_execute.return_value = out, ''
|
||||
ip = utils.get_interface_ip(self.mac)
|
||||
self.assertEqual('172.18.204.10', ip)
|
||||
mock_execute.assert_called_once_with(*self.cmd)
|
||||
|
||||
def test_get_interface_no_mac(self, mock_execute):
|
||||
lines = _LO_DEVICE, _DOCKER_DEVICE
|
||||
out = self._build_out(lines)
|
||||
mock_execute.return_value = out, ''
|
||||
ip = utils.get_interface_ip(self.mac)
|
||||
self.assertIsNone(ip)
|
||||
mock_execute.assert_called_once_with(*self.cmd)
|
||||
|
||||
def test_get_interface_no_ip(self, mock_execute):
|
||||
lines = _LO_DEVICE, _ETH_DEVICE_NO_IP, _DOCKER_DEVICE
|
||||
out = self._build_out(lines)
|
||||
mock_execute.return_value = out, ''
|
||||
ip = utils.get_interface_ip(self.mac)
|
||||
self.assertIsNone(ip)
|
||||
mock_execute.assert_called_once_with(*self.cmd)
|
||||
|
||||
def test_get_interface_no_ip_last(self, mock_execute):
|
||||
lines = _LO_DEVICE, _ETH_DEVICE_NO_IP
|
||||
out = self._build_out(lines)
|
||||
mock_execute.return_value = out, ''
|
||||
ip = utils.get_interface_ip(self.mac)
|
||||
self.assertIsNone(ip)
|
||||
mock_execute.assert_called_once_with(*self.cmd)
|
||||
|
||||
|
||||
class ParseKernelCmdline(unittest2.TestCase):
|
||||
|
||||
def test_parse_kernel_cmdline(self):
|
||||
data = 'foo=bar baz abc=def=123'
|
||||
with mock.patch('six.moves.builtins.open',
|
||||
mock.mock_open(read_data=data)) as mock_open:
|
||||
params = utils.parse_kernel_cmdline()
|
||||
self.assertEqual('bar', params['foo'])
|
||||
mock_open.assert_called_once_with('/proc/cmdline', 'rt')
|
||||
|
@ -335,3 +335,32 @@ def unblacklist_udev_rules(udev_rules_dir, udev_rename_substr):
|
||||
|
||||
def udevadm_settle():
|
||||
execute('udevadm', 'settle', '--quiet', check_exit_code=[0])
|
||||
|
||||
|
||||
def parse_kernel_cmdline():
|
||||
"""Parse linux kernel command line"""
|
||||
with open('/proc/cmdline', 'rt') as f:
|
||||
cmdline = f.read()
|
||||
parameters = {}
|
||||
for p in cmdline.split():
|
||||
name, _, value = p.partition('=')
|
||||
parameters[name] = value
|
||||
return parameters
|
||||
|
||||
|
||||
def get_interface_ip(mac_addr):
|
||||
"""Get IP address of interface with mac_addr"""
|
||||
# NOTE(yuriyz): current limitations IPv4 addresses only and one IP per
|
||||
# interface.
|
||||
ip_pattern = re.compile('inet ([\d\.]+)/')
|
||||
out, err = execute('ip', 'addr', 'show', 'scope', 'global')
|
||||
lines = out.splitlines()
|
||||
for num, line in enumerate(lines):
|
||||
if mac_addr in line:
|
||||
try:
|
||||
ip_line = lines[num + 1]
|
||||
except IndexError:
|
||||
return
|
||||
match = ip_pattern.search(ip_line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
|
@ -21,6 +21,7 @@ console_scripts =
|
||||
fa_copyimage = fuel_agent.cmd.agent:copyimage
|
||||
fa_bootloader = fuel_agent.cmd.agent:bootloader
|
||||
fa_build_image = fuel_agent.cmd.agent:build_image
|
||||
fa_ironic_callback = fuel_agent.cmd.ironic_callback:main
|
||||
|
||||
fuel_agent.drivers =
|
||||
nailgun = fuel_agent.drivers.nailgun:Nailgun
|
||||
|
Loading…
x
Reference in New Issue
Block a user