mogan/mogan/engine/flows/create_server.py

330 lines
13 KiB
Python

# Copyright 2016 Huawei Technologies Co.,LTD.
# 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.
import base64
import gzip
import shutil
import tempfile
import traceback
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import six
import taskflow.engines
from taskflow.patterns import linear_flow
from mogan.common import exception
from mogan.common import flow_utils
from mogan.common.i18n import _
from mogan.common import utils
from mogan.engine import configdrive
from mogan.engine import metadata as server_metadata
from mogan import objects
LOG = logging.getLogger(__name__)
ACTION = 'server:create'
CONF = cfg.CONF
class OnFailureRescheduleTask(flow_utils.MoganTask):
"""Triggers a rescheduling request to be sent when reverting occurs.
If rescheduling doesn't occur this task errors out the server.
"""
def __init__(self, engine_rpcapi):
requires = ['filter_properties', 'request_spec', 'server',
'requested_networks', 'user_data', 'injected_files',
'admin_password', 'key_pair', 'partitions', 'context']
super(OnFailureRescheduleTask, self).__init__(addons=[ACTION],
requires=requires)
self.engine_rpcapi = engine_rpcapi
# These exception types will trigger the server to be set into error
# status rather than being rescheduled.
self.no_reschedule_exc_types = [
# The server has been removed from the database, that can not
# be fixed by rescheduling.
exception.ServerNotFound,
exception.ServerDeployAborted,
exception.NetworkError,
]
def execute(self, **kwargs):
pass
def _reschedule(self, context, cause, request_spec, filter_properties,
server, requested_networks, user_data, injected_files,
admin_password, key_pair, partitions):
"""Actions that happen during the rescheduling attempt occur here."""
create_server = self.engine_rpcapi.schedule_and_create_servers
if not filter_properties:
filter_properties = {}
if 'retry' not in filter_properties:
filter_properties['retry'] = {}
retry_info = filter_properties['retry']
num_attempts = retry_info.get('num_attempts', 0)
LOG.debug("Server %(server_id)s: re-scheduling %(method)s "
"attempt %(num)d due to %(reason)s",
{'server_id': server.uuid,
'method': utils.make_pretty_name(create_server),
'num': num_attempts,
'reason': cause.exception_str})
if all(cause.exc_info):
# Stringify to avoid circular ref problem in json serialization
retry_info['exc'] = traceback.format_exception(*cause.exc_info)
return create_server(context, [server], requested_networks,
user_data=user_data,
injected_files=injected_files,
admin_password=admin_password,
key_pair=key_pair,
partitions=partitions,
request_spec=request_spec,
filter_properties=filter_properties)
def revert(self, context, result, flow_failures, server, **kwargs):
# Clean up associated node
server.node_uuid = None
server.node = None
server.save()
# Check if we have a cause which can tell us not to reschedule and
# set the server's status to error.
for failure in flow_failures.values():
if failure.check(*self.no_reschedule_exc_types):
LOG.error("Server %s: create failed and no reschedule.",
server.uuid)
return False
cause = list(flow_failures.values())[0]
try:
self._reschedule(context, cause, server=server, **kwargs)
return True
except exception.MoganException:
LOG.exception("Server %s: rescheduling failed",
server.uuid)
return False
class BuildNetworkTask(flow_utils.MoganTask):
"""Build network for the server."""
def __init__(self, manager):
requires = ['server', 'requested_networks', 'context']
super(BuildNetworkTask, self).__init__(addons=[ACTION],
requires=requires)
self.manager = manager
def _build_networks(self, context, server, requested_networks):
ports = self.manager.driver.get_portgroups_and_ports(server.node_uuid)
if len(requested_networks) > len(ports):
raise exception.InterfacePlugException(_(
"Ironic node: %(id)s virtual to physical interface count"
" mismatch"
" (Vif count: %(vif_count)d, Pif count: %(pif_count)d)")
% {'id': server.node_uuid,
'vif_count': len(requested_networks),
'pif_count': len(ports)})
nics_obj = objects.ServerNics(context)
for vif in requested_networks:
try:
if vif.get('net_id'):
port = self.manager.network_api.create_port(
context, vif['net_id'], server.uuid)
preserve_on_delete = False
elif vif.get('port_id'):
port = self.manager.network_api.show_port(
context, vif.get('port_id'))
self.manager.network_api.check_port_availability(port)
self.manager.network_api.bind_port(context,
port['id'],
server)
preserve_on_delete = True
nic_dict = {'port_id': port['id'],
'network_id': port['network_id'],
'mac_address': port['mac_address'],
'fixed_ips': port['fixed_ips'],
'preserve_on_delete': preserve_on_delete,
'server_uuid': server.uuid}
server_nic = objects.ServerNic(context, **nic_dict)
nics_obj.objects.append(server_nic)
self.manager.driver.plug_vif(server.node_uuid,
port['id'])
# Get updated VIF info
port_dict = self.manager.network_api.show_port(
context, port.get('id'))
# Update the real physical mac address from ironic.
server_nic.mac_address = port_dict['mac_address']
except Exception as e:
# Set nics here, so we can clean up the
# created networks during reverting.
server.nics = nics_obj
LOG.error("Server %(server)s: create or get network "
"failed. The reason is %(reason)s",
{"server": server.uuid, "reason": e})
raise exception.NetworkError(_(
"Build network for server failed."))
return nics_obj
def execute(self, context, server, requested_networks):
server_nics = self._build_networks(
context,
server,
requested_networks)
server.nics = server_nics
server.save()
def revert(self, context, result, flow_failures, server, **kwargs):
# Check if we need to clean up networks.
if server.nics:
LOG.debug("Server %s: cleaning up node networks",
server.uuid)
self.manager.destroy_networks(context, server)
# Unset nics here as we have destroyed it.
server.nics = None
return True
return False
class GenerateConfigDriveTask(flow_utils.MoganTask):
"""Generate ConfigDrive value the server."""
def __init__(self):
requires = ['server', 'user_data', 'injected_files', 'admin_password',
'key_pair', 'configdrive', 'context']
super(GenerateConfigDriveTask, self).__init__(addons=[ACTION],
requires=requires)
def _generate_configdrive(self, context, server, user_data=None,
files=None, admin_password=None, key_pair=None):
"""Generate a config drive."""
extra_md = {}
if admin_password:
extra_md['admin_pass'] = admin_password
i_meta = server_metadata.ServerMetadata(
server, content=files, user_data=user_data, key_pair=key_pair,
extra_md=extra_md)
with tempfile.NamedTemporaryFile() as uncompressed:
with configdrive.ConfigDriveBuilder(server_md=i_meta) as cdb:
cdb.make_drive(uncompressed.name)
with tempfile.NamedTemporaryFile() as compressed:
# compress config drive
with gzip.GzipFile(fileobj=compressed, mode='wb') as gzipped:
uncompressed.seek(0)
shutil.copyfileobj(uncompressed, gzipped)
# base64 encode config drive
compressed.seek(0)
return base64.b64encode(compressed.read())
def execute(self, context, server, user_data, injected_files,
admin_password, key_pair, configdrive):
try:
configdrive['value'] = self._generate_configdrive(
context, server, user_data=user_data, files=injected_files,
admin_password=admin_password, key_pair=key_pair)
except Exception as e:
with excutils.save_and_reraise_exception():
msg = ("Failed to build configdrive: %s" %
six.text_type(e))
LOG.error(msg, server=server)
LOG.info("Config drive for server %(server)s created.",
{'server': server.uuid})
class CreateServerTask(flow_utils.MoganTask):
"""Build and deploy the server."""
def __init__(self, driver):
requires = ['server', 'configdrive', 'context']
super(CreateServerTask, self).__init__(addons=[ACTION],
requires=requires)
self.driver = driver
def execute(self, context, server, configdrive, partitions):
configdrive_value = configdrive.get('value')
self.driver.spawn(context, server, configdrive_value, partitions)
LOG.info('Successfully provisioned Ironic node %s',
server.node_uuid)
def revert(self, context, result, flow_failures, server, **kwargs):
LOG.debug("Server %s: destroy backend node", server.uuid)
self.driver.destroy(context, server)
return True
def get_flow(context, manager, server, requested_networks, user_data,
injected_files, admin_password, key_pair, partitions,
request_spec, filter_properties):
"""Constructs and returns the manager entrypoint flow
This flow will do the following:
1. Build networks for the server and set port id back to baremetal port
2. Generate configdrive value for server.
3. Do node deploy and handle errors.
4. Reschedule if the tasks are on failure.
"""
flow_name = ACTION.replace(":", "_") + "_manager"
server_flow = linear_flow.Flow(flow_name)
# This injects the initial starting flow values into the workflow so that
# the dependency order of the tasks provides/requires can be correctly
# determined.
create_what = {
'context': context,
'filter_properties': filter_properties,
'request_spec': request_spec,
'server': server,
'requested_networks': requested_networks,
'user_data': user_data,
'injected_files': injected_files,
'admin_password': admin_password,
'key_pair': key_pair,
'partitions': partitions,
'configdrive': {}
}
server_flow.add(OnFailureRescheduleTask(manager.engine_rpcapi),
BuildNetworkTask(manager),
GenerateConfigDriveTask(),
CreateServerTask(manager.driver))
# Now load (but do not run) the flow using the provided initial data.
return taskflow.engines.load(server_flow, store=create_what)