8eb63c2078
This rename to "conductor" more clearly communicates that this service has a many-to-many relationship. One or more service instances coordinate between each other to conduct actions on a set of nodes, using guarded locks to prevent conflicting simultaneous actions on any given node. The old name "manager" suggested a more one-to-many relationship, which is not the design pattern which we use here. Rename ironic/manager to ironic/conductor Rename ironic.manager.manager.ManagerService to ironic.conductor.manager.ConductorManager Rename ironic-manager to ironic-conductor Update docs too Change-Id: I3191be72a44bdaf14c763ce7519a7ae9066b2bc5
257 lines
8.2 KiB
Python
257 lines
8.2 KiB
Python
# 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.
|
|
|
|
"""
|
|
Ironic IPMI power manager.
|
|
"""
|
|
|
|
import contextlib
|
|
import os
|
|
import stat
|
|
import tempfile
|
|
|
|
from oslo.config import cfg
|
|
|
|
from ironic.common import exception
|
|
from ironic.common import paths
|
|
from ironic.common import states
|
|
from ironic.common import utils
|
|
from ironic.conductor import task_manager
|
|
from ironic.drivers import base
|
|
from ironic.openstack.common import excutils
|
|
from ironic.openstack.common import jsonutils as json
|
|
from ironic.openstack.common import log as logging
|
|
from ironic.openstack.common import loopingcall
|
|
|
|
opts = [
|
|
cfg.StrOpt('terminal',
|
|
default='shellinaboxd',
|
|
help='path to baremetal terminal program'),
|
|
cfg.StrOpt('terminal_cert_dir',
|
|
default=None,
|
|
help='path to baremetal terminal SSL cert(PEM)'),
|
|
cfg.StrOpt('terminal_pid_dir',
|
|
default=paths.state_path_def('baremetal/console'),
|
|
help='path to directory stores pidfiles of baremetal_terminal'),
|
|
cfg.IntOpt('ipmi_power_retry',
|
|
default=5,
|
|
help='Maximum seconds to retry IPMI operations'),
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(opts)
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
VALID_BOOT_DEVICES = ['pxe', 'disk', 'safe', 'cdrom', 'bios']
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _make_password_file(password):
|
|
try:
|
|
fd, path = tempfile.mkstemp()
|
|
os.fchmod(fd, stat.S_IRUSR | stat.S_IWUSR)
|
|
with os.fdopen(fd, "w") as f:
|
|
f.write(password)
|
|
|
|
yield path
|
|
utils.delete_if_exists(path)
|
|
except Exception:
|
|
with excutils.save_and_reraise_exception():
|
|
utils.delete_if_exists(path)
|
|
|
|
|
|
def _parse_driver_info(node):
|
|
driver_info = json.loads(node.get('driver_info', ''))
|
|
ipmi_info = driver_info.get('ipmi')
|
|
address = ipmi_info.get('address', None)
|
|
username = ipmi_info.get('username', None)
|
|
password = ipmi_info.get('password', None)
|
|
port = ipmi_info.get('terminal_port', None)
|
|
|
|
if not address or not username or not password:
|
|
raise exception.InvalidParameterValue(_(
|
|
"IPMI credentials not supplied to IPMI driver."))
|
|
|
|
return {
|
|
'address': address,
|
|
'username': username,
|
|
'password': password,
|
|
'port': port,
|
|
'uuid': node.get('uuid')
|
|
}
|
|
|
|
|
|
def _exec_ipmitool(driver_info, command):
|
|
args = ['ipmitool',
|
|
'-I',
|
|
'lanplus',
|
|
'-H',
|
|
driver_info['address'],
|
|
'-U',
|
|
driver_info['username'],
|
|
'-f']
|
|
with _make_password_file(driver_info['password']) as pw_file:
|
|
args.append(pw_file)
|
|
args.extend(command.split(" "))
|
|
out, err = utils.execute(*args, attempts=3)
|
|
LOG.debug(_("ipmitool stdout: '%(out)s', stderr: '%(err)s'"),
|
|
locals())
|
|
return out, err
|
|
|
|
|
|
def _power_on(driver_info):
|
|
"""Turn the power to this node ON."""
|
|
|
|
# use mutable objects so the looped method can change them
|
|
state = [None]
|
|
retries = [0]
|
|
|
|
def _wait_for_power_on(state, retries):
|
|
"""Called at an interval until the node's power is on."""
|
|
|
|
state[0] = _power_status(driver_info)
|
|
if state[0] == states.POWER_ON:
|
|
raise loopingcall.LoopingCallDone()
|
|
|
|
if retries[0] > CONF.ipmi_power_retry:
|
|
state[0] = states.ERROR
|
|
raise loopingcall.LoopingCallDone()
|
|
try:
|
|
retries[0] += 1
|
|
_exec_ipmitool(driver_info, "power on")
|
|
except Exception:
|
|
# Log failures but keep trying
|
|
LOG.warning(_("IPMI power on failed for node %s.")
|
|
% driver_info['uuid'])
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_on,
|
|
state, retries)
|
|
timer.start(interval=1).wait()
|
|
return state[0]
|
|
|
|
|
|
def _power_off(driver_info):
|
|
"""Turn the power to this node OFF."""
|
|
|
|
# use mutable objects so the looped method can change them
|
|
state = [None]
|
|
retries = [0]
|
|
|
|
def _wait_for_power_off(state, retries):
|
|
"""Called at an interval until the node's power is off."""
|
|
|
|
state[0] = _power_status(driver_info)
|
|
if state[0] == states.POWER_OFF:
|
|
raise loopingcall.LoopingCallDone()
|
|
|
|
if retries[0] > CONF.ipmi_power_retry:
|
|
state[0] = states.ERROR
|
|
raise loopingcall.LoopingCallDone()
|
|
try:
|
|
retries[0] += 1
|
|
_exec_ipmitool(driver_info, "power off")
|
|
except Exception:
|
|
# Log failures but keep trying
|
|
LOG.warning(_("IPMI power off failed for node %s.")
|
|
% driver_info['uuid'])
|
|
|
|
timer = loopingcall.FixedIntervalLoopingCall(_wait_for_power_off,
|
|
state=state, retries=retries)
|
|
timer.start(interval=1).wait()
|
|
return state[0]
|
|
|
|
|
|
def _power_status(driver_info):
|
|
out_err = _exec_ipmitool(driver_info, "power status")
|
|
if out_err[0] == "Chassis Power is on\n":
|
|
return states.POWER_ON
|
|
elif out_err[0] == "Chassis Power is off\n":
|
|
return states.POWER_OFF
|
|
else:
|
|
return states.ERROR
|
|
|
|
|
|
class IPMIPower(base.PowerInterface):
|
|
|
|
def validate(self, node):
|
|
"""Check that node['driver_info'] contains IPMI credentials."""
|
|
try:
|
|
_parse_driver_info(node)
|
|
except exception.InvalidParameterValue:
|
|
return False
|
|
return True
|
|
|
|
def get_power_state(self, task, node):
|
|
"""Get the current power state."""
|
|
driver_info = _parse_driver_info(node)
|
|
return _power_status(driver_info)
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def set_power_state(self, task, node, pstate):
|
|
"""Turn the power on or off."""
|
|
driver_info = _parse_driver_info(node)
|
|
|
|
if pstate == states.POWER_ON:
|
|
state = _power_on(driver_info)
|
|
elif pstate == states.POWER_OFF:
|
|
state = _power_off(driver_info)
|
|
else:
|
|
raise exception.IronicException(_(
|
|
"set_power_state called with invalid power state."))
|
|
|
|
if state != pstate:
|
|
raise exception.PowerStateFailure(pstate=pstate)
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def reboot(self, task, node):
|
|
"""Cycles the power to a node."""
|
|
driver_info = _parse_driver_info(node)
|
|
_power_off(driver_info)
|
|
state = _power_on(driver_info)
|
|
|
|
if state != states.POWER_ON:
|
|
raise exception.PowerStateFailure(pstate=states.POWER_ON)
|
|
|
|
@task_manager.require_exclusive_lock
|
|
def _set_boot_device(self, task, node, device, persistent=False):
|
|
"""Set the boot device for a node.
|
|
|
|
:param task: a TaskManager instance.
|
|
:param node: The Node.
|
|
:param device: Boot device. One of [pxe, disk, cdrom, safe, bios].
|
|
:param persistent: Whether to set next-boot, or make the change
|
|
permanent. Default: False.
|
|
:raises: InvalidParameterValue if an invalid boot device is specified.
|
|
:raises: IPMIFailure on an error from ipmitool.
|
|
|
|
"""
|
|
if device not in VALID_BOOT_DEVICES:
|
|
raise exception.InvalidParameterValue(_(
|
|
"Invalid boot device %s specified.") % device)
|
|
cmd = "chassis bootdev %s" % device
|
|
if persistent:
|
|
cmd = cmd + " options=persistent"
|
|
driver_info = _parse_driver_info(node)
|
|
try:
|
|
out, err = _exec_ipmitool(driver_info, cmd)
|
|
# TODO(deva): validate (out, err) and add unit test for failure
|
|
except Exception:
|
|
raise exception.IPMIFailure(cmd=cmd)
|