StarlingX System Configuration Management
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

13719 lines
602 KiB

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# coding=utf-8
# Copyright 2013 Hewlett-Packard Development Company, L.P.
# Copyright 2013 International Business Machines Corporation
# 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.
#
# Copyright (c) 2013-2021 Wind River Systems, Inc.
#
"""Conduct all activity related system inventory.
A single instance of :py:class:`sysinv.conductor.manager.ConductorManager` is
created within the *sysinv-conductor* process, and is responsible for
performing all actions for hosts managed by system inventory.
Commands are received via RPC calls. The conductor service also performs
collection of inventory data for each host.
"""
import base64
import errno
import filecmp
import fnmatch
import glob
import hashlib
import math
import os
import re
import requests
import ruamel.yaml as yaml
import shutil
import socket
import tempfile
import time
import traceback
import uuid
import xml.etree.ElementTree as ElementTree
from contextlib import contextmanager
from datetime import datetime
from copy import deepcopy
import tsconfig.tsconfig as tsc
from collections import namedtuple
from collections import OrderedDict
from cgcs_patch.patch_verify import verify_files
from controllerconfig.upgrades import management as upgrades_management
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from eventlet import greenthread
# Make subprocess module greenthread friendly
from eventlet.green import subprocess
from fm_api import constants as fm_constants
from fm_api import fm_api
from netaddr import IPAddress
from netaddr import IPNetwork
from oslo_config import cfg
from oslo_log import log
from oslo_serialization import jsonutils
from oslo_utils import excutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
from platform_util.license import license
from sqlalchemy.orm import exc
from six.moves import http_client as httplib
from sysinv._i18n import _
from sysinv.agent import rpcapi as agent_rpcapi
from sysinv.api.controllers.v1 import address_pool
from sysinv.api.controllers.v1 import cpu_utils
from sysinv.api.controllers.v1 import kube_app as kube_api
from sysinv.api.controllers.v1 import mtce_api
from sysinv.api.controllers.v1 import utils
from sysinv.api.controllers.v1 import vim_api
from sysinv.common import constants
from sysinv.common import ceph as cceph
from sysinv.common import dc_api
from sysinv.common import device as dconstants
from sysinv.common import exception
from sysinv.common import fm
from sysinv.common import fernet
from sysinv.common import health
from sysinv.common import kubernetes
from sysinv.common import retrying
from sysinv.common import service
from sysinv.common import utils as cutils
from sysinv.common.retrying import retry
from sysinv.common.storage_backend_conf import StorageBackendConfig
from cephclient import wrapper as ceph
from sysinv.conductor import ceph as iceph
from sysinv.conductor import kube_app
from sysinv.conductor import openstack
from sysinv.conductor import docker_registry
from sysinv.conductor import keystone_listener
from sysinv.db import api as dbapi
from sysinv.fpga_agent import rpcapi as fpga_agent_rpcapi
from sysinv.fpga_agent import constants as fpga_constants
from sysinv import objects
from sysinv.objects import base as objects_base
from sysinv.objects import kube_app as kubeapp_obj
from sysinv.openstack.common import context as ctx
from sysinv.openstack.common import periodic_task
from sysinv.puppet import common as puppet_common
from sysinv.puppet import puppet
from sysinv.helm import helm
from sysinv.helm.lifecycle_constants import LifecycleConstants
from sysinv.helm.lifecycle_hook import LifecycleHookInfo
MANAGER_TOPIC = 'sysinv.conductor_manager'
LOG = log.getLogger(__name__)
conductor_opts = [
cfg.StrOpt('api_url',
default=None,
help=('Url of SysInv API service. If not set SysInv can '
'get current value from Keystone service catalog.')),
cfg.IntOpt('audit_interval',
default=60,
help='Interval to run conductor audit'),
cfg.IntOpt('osd_remove_retry_count',
default=11,
help=('Maximum number of retries in case Ceph OSD remove '
'requests fail because OSD is still up.')),
cfg.IntOpt('osd_remove_retry_interval',
default=5,
help='Interval in seconds between retries to remove Ceph OSD.'),
cfg.IntOpt('managed_app_auto_recovery_interval',
default=300,
help='Interval to run managed app auto recovery'),
cfg.IntOpt('kube_upgrade_downgrade_retry_interval',
default=3600,
help='Interval in seconds between retries to upgrade/downgrade kubernetes components'),
cfg.IntOpt('fw_update_large_timeout',
default=3600,
help='Timeout interval in seconds for a large device image'),
cfg.IntOpt('fw_update_small_timeout',
default=300,
help='Timeout interval in seconds for a small device image'),
]
CONF = cfg.CONF
CONF.register_opts(conductor_opts, 'conductor')
# doesn't work otherwise for ceph-manager RPC calls; reply is lost
#
CONF.amqp_rpc_single_reply_queue = True
# configuration flags
CFS_DRBDADM_RECONFIGURED = os.path.join(
tsc.PLATFORM_CONF_PATH, ".cfs_drbdadm_reconfigured")
# volatile flags
CONFIG_CONTROLLER_FINI_FLAG = os.path.join(tsc.VOLATILE_PATH,
".config_controller_fini")
CONFIG_FAIL_FLAG = os.path.join(tsc.VOLATILE_PATH, ".config_fail")
ACTIVE_CONFIG_REBOOT_REQUIRED = os.path.join(
constants.SYSINV_VOLATILE_PATH, ".reboot_required")
# configuration UUID reboot required flag (bit)
CONFIG_REBOOT_REQUIRED = (1 << 127)
# Types of runtime configuration applies
CONFIG_APPLY_RUNTIME_MANIFEST = 'config_apply_runtime_manifest'
CONFIG_UPDATE_FILE = 'config_update_file'
LOCK_NAME_UPDATE_CONFIG = 'update_config_'
LOCK_AUTO_APPLY = 'AutoApplyLock'
AppTarBall = namedtuple(
'AppTarBall',
"tarball_name app_name app_version manifest_name manifest_file")
class ConductorManager(service.PeriodicService):
"""Sysinv Conductor service main class."""
RPC_API_VERSION = '1.1'
my_host_id = None
def __init__(self, host, topic):
serializer = objects_base.SysinvObjectSerializer()
super(ConductorManager, self).__init__(host, topic,
serializer=serializer)
self.dbapi = None
self.fm_api = None
self.fm_log = None
self.host_uuid = None
self._app = None
self._ceph = None
self._ceph_api = ceph.CephWrapper(
endpoint='http://localhost:{}'.format(constants.CEPH_MGR_PORT))
self._kube = None
self._fernet = None
self._openstack = None
self._api_token = None
self._mtc_address = constants.LOCALHOST_HOSTNAME
self._mtc_port = 2112
# Timeouts for adding & removing operations
self._pv_op_timeouts = {}
self._stor_bck_op_timeouts = {}
# struct {'host_uuid':[config_uuid_0,config_uuid_1]}
# this will track the config w/ reboot request to apply
self._host_reboot_config_uuid = {}
# track deferred runtime config which need to be applied
self._host_deferred_runtime_config = []
# Guard for a function that should run only once per conductor start
self._do_detect_swact = True
# Guard for a function that should run only once per conductor start
self._has_loaded_missing_apps_metadata = False
self.apps_metadata = {constants.APP_METADATA_APPS: {},
constants.APP_METADATA_PLATFORM_MANAGED_APPS: {},
constants.APP_METADATA_DESIRED_STATES: {},
constants.APP_METADATA_ORDERED_APPS: []}
self._backup_action_map = dict()
for action in [constants.BACKUP_ACTION_SEMANTIC_CHECK,
constants.BACKUP_ACTION_PRE_BACKUP,
constants.BACKUP_ACTION_POST_BACKUP,
constants.BACKUP_ACTION_PRE_ETCD_BACKUP,
constants.BACKUP_ACTION_POST_ETCD_BACKUP,
constants.BACKUP_ACTION_PRE_RESTORE,
constants.BACKUP_ACTION_POST_RESTORE]:
impl = getattr(self, '_do_' + action.replace('-', '_'))
self._backup_action_map[action] = impl
self._initialize_backup_actions_log()
def start(self):
self._start()
# accept API calls and run periodic tasks after
# initializing conductor manager service
super(ConductorManager, self).start()
# Upgrade/Downgrade kubernetes components.
# greenthread must be called after super.start for it to work properly.
greenthread.spawn(self._upgrade_downgrade_kube_components)
# monitor keystone user update event to check whether admin password is
# changed or not. If changed, then sync it to kubernetes's secret info.
greenthread.spawn(keystone_listener.start_keystone_listener, self._app)
# Monitor ceph to become responsive
if StorageBackendConfig.has_backend_configured(
self.dbapi,
constants.SB_TYPE_CEPH):
greenthread.spawn(self._init_ceph_cluster_info)
def _start(self):
self.dbapi = dbapi.get_instance()
self.fm_api = fm_api.FaultAPIs()
self.fm_log = fm.FmCustomerLog()
self.host_uuid = self._get_active_controller_uuid()
self._openstack = openstack.OpenStackOperator(self.dbapi)
self._puppet = puppet.PuppetOperator(self.dbapi)
# create /var/run/sysinv if required. On DOR, the manifests
# may not run to create this volatile directory.
cutils.check_lock_path()
self._initialize_active_controller_reboot_config()
system = self._create_default_system()
# Besides OpenStack and Puppet operators, all other operators
# should be initialized after the default system is in place.
# For instance, CephOperator expects a system to exist to initialize
# correctly. With Ansible bootstrap deployment, sysinv conductor is
# brought up during bootstrap manifest apply and is not restarted
# until host unlock and we need ceph-mon up in order to configure
# ceph for the initial unlock.
self._helm = helm.HelmOperator(self.dbapi)
self._app = kube_app.AppOperator(self.dbapi, self._helm, self.apps_metadata)
self._docker = kube_app.DockerHelper(self.dbapi)
self._kube = kubernetes.KubeOperator()
self._armada = kube_app.ArmadaHelper(self._kube)
self._kube_app_helper = kube_api.KubeAppHelper(self.dbapi)
self._fernet = fernet.FernetOperator()
# Upgrade start tasks
self._upgrade_init_actions()
self._kube_upgrade_init_actions()
self._handle_restore_in_progress()
self._sx_to_dx_post_migration_actions(system)
LOG.info("sysinv-conductor start committed system=%s" %
system.as_dict())
# Save our start time for time limited init actions
self._start_time = timeutils.utcnow()
# Load apps metadata
for app in self.dbapi.kube_app_get_all():
self._app.load_application_metadata_from_database(app)
def _get_active_controller_uuid(self):
ahost = utils.HostHelper.get_active_controller(self.dbapi)
if ahost:
return ahost.uuid
else:
return None
def _initialize_active_controller_reboot_config(self):
# initialize host_reboot_config for active controller in case
# process has been restarted
if self.host_uuid and os.path.exists(ACTIVE_CONFIG_REBOOT_REQUIRED):
ahost = self.dbapi.ihost_get(self.host_uuid)
self._host_reboot_config_uuid[self.host_uuid] = \
[ahost.config_target]
def periodic_tasks(self, context, raise_on_error=False):
""" Periodic tasks are run at pre-specified intervals. """
return self.run_periodic_tasks(context, raise_on_error=raise_on_error)
@contextmanager
def session(self):
session = dbapi.get_instance().get_session(autocommit=True)
try:
yield session
finally:
session.remove()
def _create_default_system(self):
"""Populate the default system tables"""
system = None
try:
system = self.dbapi.isystem_get_one()
# fill in empty remotelogging system_id fields
self.dbapi.remotelogging_fill_empty_system_id(system.id)
# fill in empty ptp system_id fields
self.dbapi.ptp_fill_empty_system_id(system.id)
return system # system already configured
except exception.NotFound:
pass # create default system
# Create the default system entry
mode = None
if tsc.system_mode is not None:
mode = tsc.system_mode
security_profile = None
if tsc.security_profile is not None:
security_profile = tsc.security_profile
security_feature = constants.SYSTEM_SECURITY_FEATURE_SPECTRE_MELTDOWN_DEFAULT_OPTS
if tsc.security_feature is not None:
security_feature = tsc.security_feature
system = self.dbapi.isystem_create({
'name': uuidutils.generate_uuid(),
'system_mode': mode,
'software_version': cutils.get_sw_version(),
'capabilities': {},
'security_profile': security_profile,
'security_feature': security_feature
})
# Populate the default system tables, referencing the newly created
# table (additional attributes will be populated during
# config_controller configuration population)
values = {'forisystemid': system.id}
self.dbapi.iuser_create(values)
self.dbapi.idns_create(values)
self.dbapi.intp_create(values)
self.dbapi.drbdconfig_create({
'forisystemid': system.id,
'uuid': uuidutils.generate_uuid(),
'link_util': constants.DRBD_LINK_UTIL_DEFAULT,
'num_parallel': constants.DRBD_NUM_PARALLEL_DEFAULT,
'rtt_ms': constants.DRBD_RTT_MS_DEFAULT
})
# remotelogging and ptp tables have attribute 'system_id' not 'forisystemid'
system_id_attribute_value = {'system_id': system.id}
self.dbapi.remotelogging_create(system_id_attribute_value)
self.dbapi.ptp_create(system_id_attribute_value)
# populate service table
for optional_service in constants.ALL_OPTIONAL_SERVICES:
self.dbapi.service_create({'name': optional_service,
'enabled': False})
self._create_default_service_parameter()
return system
def _update_pvc_migration_alarm(self, alarm_state=None):
entity_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_K8S,
"PV-migration-failed")
reason_text = "Failed to patch Persistent Volumes backed by CEPH "\
"during AIO-SX to AIO-DX migration"
if alarm_state == fm_constants.FM_ALARM_STATE_SET:
fault = fm_api.Fault(
alarm_id=fm_constants.FM_ALARM_ID_K8S_RESOURCE_PV,
alarm_state=fm_constants.FM_ALARM_STATE_SET,
entity_type_id=fm_constants.FM_ENTITY_TYPE_K8S,
entity_instance_id=entity_instance_id,
severity=fm_constants.FM_ALARM_SEVERITY_MAJOR,
reason_text=reason_text,
alarm_type=fm_constants.FM_ALARM_TYPE_3,
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_6,
proposed_repair_action=_("Manually execute /usr/bin/ceph_k8s_update_monitors.sh "
"to confirm PVs are updated, then lock/unlock to clear "
"alarms. If problem persists, contact next level of "
"support."),
service_affecting=False)
self.fm_api.set_fault(fault)
else:
alarms = self.fm_api.get_faults(entity_instance_id)
if alarms:
self.fm_api.clear_all(entity_instance_id)
def _pvc_monitor_migration(self):
ceph_backend_enabled = StorageBackendConfig.get_backend(
self.dbapi,
constants.SB_TYPE_CEPH)
if not ceph_backend_enabled:
# if it does not have ceph backend enabled there is
# nothing to migrate
return True
# get the controller-0 and floating management IP address
controller_0_address = self.dbapi.address_get_by_name(
constants.CONTROLLER_0_MGMT).address
floating_address = self.dbapi.address_get_by_name(
cutils.format_address_name(constants.CONTROLLER_HOSTNAME,
constants.NETWORK_TYPE_MGMT)).address
try:
cmd = ["/usr/bin/ceph_k8s_update_monitors.sh",
controller_0_address,
floating_address]
__, __ = cutils.execute(*cmd, run_as_root=True)
LOG.info("Updated ceph-mon address from {} to {} on existing Persistent Volumes."
.format(controller_0_address, floating_address))
self._update_pvc_migration_alarm()
except exception.ProcessExecutionError:
error_msg = "Failed to patch Kubernetes Persistent Volume resources. "\
"ceph-mon address changed from {} to {}".format(
controller_0_address, floating_address)
LOG.error(error_msg)
# raise alarm
self._update_pvc_migration_alarm(fm_constants.FM_ALARM_STATE_SET)
return False
return True
def _sx_to_dx_post_migration_actions(self, system):
host = self.dbapi.ihost_get(self.host_uuid)
# Skip if the system mode is not set to duplex or it is not unlocked
if (system.system_mode != constants.SYSTEM_MODE_DUPLEX or
host.administrative != constants.ADMIN_UNLOCKED):
return
if system.capabilities.get('simplex_to_duplex_migration'):
system_dict = system.as_dict()
del system_dict['capabilities']['simplex_to_duplex_migration']
self.dbapi.isystem_update(system.uuid, system_dict)
greenthread.spawn(self._pvc_monitor_migration)
elif self.fm_api.get_faults_by_id(fm_constants.FM_ALARM_ID_K8S_RESOURCE_PV):
greenthread.spawn(self._pvc_monitor_migration)
def _upgrade_init_actions(self):
""" Perform any upgrade related startup actions"""
try:
upgrade = self.dbapi.software_upgrade_get_one()
except exception.NotFound:
# Not upgrading. No need to update status
return
hostname = socket.gethostname()
if hostname == constants.CONTROLLER_0_HOSTNAME:
if os.path.isfile(tsc.UPGRADE_ROLLBACK_FLAG):
self._set_state_for_rollback(upgrade)
elif os.path.isfile(tsc.UPGRADE_ABORT_FLAG):
self._set_state_for_abort(upgrade)
elif hostname == constants.CONTROLLER_1_HOSTNAME:
self._init_controller_for_upgrade(upgrade)
system_mode = self.dbapi.isystem_get_one().system_mode
if system_mode == constants.SYSTEM_MODE_SIMPLEX:
self._init_controller_for_upgrade(upgrade)
if upgrade.state in [constants.UPGRADE_ACTIVATION_REQUESTED,
constants.UPGRADE_ACTIVATING]:
# Reset to activation-failed if the conductor restarts. This could
# be due to a swact or the process restarting. Either way we'll
# need to rerun the activation.
self.dbapi.software_upgrade_update(
upgrade.uuid, {'state': constants.UPGRADE_ACTIVATION_FAILED})
self._upgrade_default_service()
self._upgrade_default_service_parameter()
def _handle_restore_in_progress(self):
if os.path.isfile(tsc.SKIP_CEPH_OSD_WIPING):
LOG.info("Starting thread to fix storage nodes install uuid.")
greenthread.spawn(self._fix_storage_install_uuid)
if os.path.isfile(tsc.RESTORE_IN_PROGRESS_FLAG):
if StorageBackendConfig.has_backend(
self.dbapi,
constants.CINDER_BACKEND_CEPH):
StorageBackendConfig.update_backend_states(
self.dbapi,
constants.CINDER_BACKEND_CEPH,
task=constants.SB_TASK_RESTORE)
def _set_state_for_abort(self, upgrade):
""" Update the database to reflect the abort"""
LOG.info("Upgrade Abort detected. Correcting database state.")
# Update the upgrade state
self.dbapi.software_upgrade_update(
upgrade.uuid, {'state': constants.UPGRADE_ABORTING})
try:
os.remove(tsc.UPGRADE_ABORT_FLAG)
except OSError:
LOG.exception("Failed to remove upgrade rollback flag")
def _set_state_for_rollback(self, upgrade):
""" Update the database to reflect the rollback"""
LOG.info("Upgrade Rollback detected. Correcting database state.")
# Update the upgrade state
self.dbapi.software_upgrade_update(
upgrade.uuid, {'state': constants.UPGRADE_ABORTING_ROLLBACK})
# At this point we are swacting to controller-0 which has just been
# downgraded.
# Before downgrading controller-0 all storage/worker nodes were locked
# The database of the from_load is not aware of this, so we set the
# state in the database to match the state of the system. This does not
# actually lock the nodes.
hosts = self.dbapi.ihost_get_list()
for host in hosts:
if host.personality not in [constants.WORKER, constants.STORAGE]:
continue
self.dbapi.ihost_update(host.uuid, {
'administrative': constants.ADMIN_LOCKED})
# Remove the rollback flag, we only want to modify the database once
try:
os.remove(tsc.UPGRADE_ROLLBACK_FLAG)
except OSError:
LOG.exception("Failed to remove upgrade rollback flag")
def _init_controller_for_upgrade(self, upgrade):
# Raise alarm to show an upgrade is in progress
# After upgrading controller-1 and swacting to it, we must
# re-raise the upgrades alarm, because alarms are not preserved
# from the previous release.
entity_instance_id = "%s=%s" % (fm_constants.FM_ENTITY_TYPE_HOST,
constants.CONTROLLER_HOSTNAME)
if not self.fm_api.get_fault(
fm_constants.FM_ALARM_ID_UPGRADE_IN_PROGRESS,
entity_instance_id):
fault = fm_api.Fault(
alarm_id=fm_constants.FM_ALARM_ID_UPGRADE_IN_PROGRESS,
alarm_state=fm_constants.FM_ALARM_STATE_SET,
entity_type_id=fm_constants.FM_ENTITY_TYPE_HOST,
entity_instance_id=entity_instance_id,
severity=fm_constants.FM_ALARM_SEVERITY_MINOR,
reason_text="System Upgrade in progress.",
# operational
alarm_type=fm_constants.FM_ALARM_TYPE_7,
# congestion
probable_cause=fm_constants.ALARM_PROBABLE_CAUSE_8,
proposed_repair_action="No action required.",
service_affecting=False)
self.fm_api.set_fault(fault)
# Regenerate dnsmasq.hosts and dnsmasq.addn_hosts.
# This is necessary to handle the case where a lease expires during
# an upgrade, in order to allow hostnames to be resolved from
# the dnsmasq.addn_hosts file before unlocking controller-0 forces
# dnsmasq.addn_hosts to be regenerated.
self._generate_dnsmasq_hosts_file()
DEFAULT_PARAMETERS = [
{'service': constants.SERVICE_TYPE_IDENTITY,
'section': constants.SERVICE_PARAM_SECTION_IDENTITY_CONFIG,
'name': constants.SERVICE_PARAM_IDENTITY_CONFIG_TOKEN_EXPIRATION,
'value': constants.SERVICE_PARAM_IDENTITY_CONFIG_TOKEN_EXPIRATION_DEFAULT
},
{'service': constants.SERVICE_TYPE_HORIZON,
'section': constants.SERVICE_PARAM_SECTION_HORIZON_AUTH,
'name': constants.SERVICE_PARAM_HORIZON_AUTH_LOCKOUT_PERIOD_SEC,
'value': constants.SERVICE_PARAM_HORIZON_AUTH_LOCKOUT_PERIOD_SEC_DEFAULT
},
{'service': constants.SERVICE_TYPE_HORIZON,
'section': constants.SERVICE_PARAM_SECTION_HORIZON_AUTH,
'name': constants.SERVICE_PARAM_HORIZON_AUTH_LOCKOUT_RETRIES,
'value': constants.SERVICE_PARAM_HORIZON_AUTH_LOCKOUT_RETRIES_DEFAULT
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_WORKER_BOOT_TIMEOUT,
'value': constants.SERVICE_PARAM_PLAT_MTCE_WORKER_BOOT_TIMEOUT_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_CONTROLLER_BOOT_TIMEOUT,
'value': constants.SERVICE_PARAM_PLAT_MTCE_CONTROLLER_BOOT_TIMEOUT_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_HBS_PERIOD,
'value': constants.SERVICE_PARAM_PLAT_MTCE_HBS_PERIOD_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_ACTION,
'value': constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_ACTION_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_THRESHOLD,
'value': constants.SERVICE_PARAM_PLAT_MTCE_HBS_FAILURE_THRESHOLD_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_HBS_DEGRADE_THRESHOLD,
'value': constants.SERVICE_PARAM_PLAT_MTCE_HBS_DEGRADE_THRESHOLD_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_MNFA_THRESHOLD,
'value': constants.SERVICE_PARAM_PLAT_MTCE_MNFA_THRESHOLD_DEFAULT,
},
{'service': constants.SERVICE_TYPE_PLATFORM,
'section': constants.SERVICE_PARAM_SECTION_PLATFORM_MAINTENANCE,
'name': constants.SERVICE_PARAM_PLAT_MTCE_MNFA_TIMEOUT,
'value': constants.SERVICE_PARAM_PLAT_MTCE_MNFA_TIMEOUT_DEFAULT,
},
{'service': constants.SERVICE_TYPE_RADOSGW,
'section': constants.SERVICE_PARAM_SECTION_RADOSGW_CONFIG,
'name': constants.SERVICE_PARAM_NAME_RADOSGW_SERVICE_ENABLED,
'value': False},
{'service': constants.SERVICE_TYPE_RADOSGW,
'section': constants.SERVICE_PARAM_SECTION_RADOSGW_CONFIG,
'name': constants.SERVICE_PARAM_NAME_RADOSGW_FS_SIZE_MB,
'value': constants.SERVICE_PARAM_RADOSGW_FS_SIZE_MB_DEFAULT},
{'service': constants.SERVICE_TYPE_HTTP,
'section': constants.SERVICE_PARAM_SECTION_HTTP_CONFIG,
'name': constants.SERVICE_PARAM_HTTP_PORT_HTTP,
'value': constants.SERVICE_PARAM_HTTP_PORT_HTTP_DEFAULT
},
{'service': constants.SERVICE_TYPE_HTTP,
'section': constants.SERVICE_PARAM_SECTION_HTTP_CONFIG,
'name': constants.SERVICE_PARAM_HTTP_PORT_HTTPS,
'value': constants.SERVICE_PARAM_HTTP_PORT_HTTPS_DEFAULT
},
]
def _create_default_service_parameter(self):
""" Populate the default service parameters"""
for p in ConductorManager.DEFAULT_PARAMETERS:
self.dbapi.service_parameter_create(p)
def _upgrade_default_service_parameter(self):
""" Update the default service parameters when upgrade is done"""
parms = self.dbapi.service_parameter_get_all()
for p_new in ConductorManager.DEFAULT_PARAMETERS:
found = False
for p_db in parms:
if (p_new['service'] == p_db.service and
p_new['section'] == p_db.section and
p_new['name'] == p_db.name):
found = True
break
if not found:
self.dbapi.service_parameter_create(p_new)
def _get_service_parameter_sections(self, service):
""" Given a service, returns all sections defined"""
params = self.dbapi.service_parameter_get_all(service)
return params
def _upgrade_default_service(self):
""" Update the default service when upgrade is done"""
services = self.dbapi.service_get_all()
for s_new in constants.ALL_OPTIONAL_SERVICES:
found = False
for s_db in services:
if (s_new == s_db.name):
found = True
break
if not found:
self.dbapi.service_create({'name': s_new,
'enabled': False})
def _lookup_static_ip_address(self, name, networktype):
""""Find a statically configured address based on name and network
type."""
try:
# address names are refined by network type to ensure they are
# unique across different address pools
name = cutils.format_address_name(name, networktype)
address = self.dbapi.address_get_by_name(name)
return address.address
except exception.AddressNotFoundByName:
return None
def _using_static_ip(self, ihost, personality=None, hostname=None):
using_static = False
if ihost:
ipersonality = ihost['personality']
ihostname = ihost['hostname'] or ""
else:
ipersonality = personality
ihostname = hostname or ""
if ipersonality and ipersonality == constants.CONTROLLER:
using_static = True
elif ipersonality and ipersonality == constants.STORAGE:
# only storage-0 and storage-1 have static (later storage-2)
if (ihostname[:len(constants.STORAGE_0_HOSTNAME)] in
[constants.STORAGE_0_HOSTNAME, constants.STORAGE_1_HOSTNAME]):
using_static = True
return using_static
def handle_dhcp_lease(self, context, tags, mac, ip_address, cid=None):
"""Synchronously, have a conductor handle a DHCP lease update.
Handling depends on the interface:
- management interface: do nothing
- pxeboot interface: create i_host
:param context: request context.
:param tags: specifies the interface type (mgmt)
:param mac: MAC for the lease
:param ip_address: IP address for the lease
"""
LOG.info("receiving dhcp_lease: %s %s %s %s %s" %
(context, tags, mac, ip_address, cid))
# Get the first field from the tags
first_tag = tags.split()[0]
if 'pxeboot' == first_tag:
mgmt_network = self.dbapi.network_get_by_type(
constants.NETWORK_TYPE_MGMT)
if not mgmt_network.dynamic:
return
# This is a DHCP lease for a node on the pxeboot network
# Create the ihost (if necessary).
ihost_dict = {'mgmt_mac': mac}
self.create_ihost(context, ihost_dict, reason='dhcp pxeboot')
def handle_dhcp_lease_from_clone(self, context, mac):
"""Handle dhcp request from a cloned controller-1.
If MAC address in DB is still set to well known
clone label, then this is the first boot of the
other controller. Real MAC address from PXE request
is updated in the DB."""
controller_hosts =\
self.dbapi.ihost_get_by_personality(constants.CONTROLLER)
for host in controller_hosts:
if (constants.CLONE_ISO_MAC in host.mgmt_mac and
host.personality == constants.CONTROLLER and
host.administrative == constants.ADMIN_LOCKED):
LOG.info("create_ihost (clone): Host found: {}:{}:{}->{}"
.format(host.hostname, host.personality,
host.mgmt_mac, mac))
values = {'mgmt_mac': mac}
self.dbapi.ihost_update(host.uuid, values)
host.mgmt_mac = mac
self._configure_controller_host(context, host)
if host.personality and host.hostname:
ihost_mtc = host.as_dict()
ihost_mtc['operation'] = 'modify'
ihost_mtc = cutils.removekeys_nonmtce(ihost_mtc)
mtce_api.host_modify(
self._api_token, self._mtc_address,
self._mtc_port, ihost_mtc,
constants.MTC_DEFAULT_TIMEOUT_IN_SECS)
return host
return None
def create_ihost(self, context, values, reason=None):
"""Create an ihost with the supplied data.
This method allows an ihost to be created.
:param context: an admin context
:param values: initial values for new ihost object
:returns: updated ihost object, including all fields.
"""
if 'mgmt_mac' not in values:
raise exception.SysinvException(_(
"Invalid method call: create_ihost requires mgmt_mac."))
try:
mgmt_update_required = False
mac = values['mgmt_mac']
mac = mac.rstrip()
mac = cutils.validate_and_normalize_mac(mac)
ihost = self.dbapi.ihost_get_by_mgmt_mac(mac)
LOG.info("Not creating ihost for mac: %s because it "
"already exists with uuid: %s" % (values['mgmt_mac'],
ihost['uuid']))
mgmt_ip = values.get('mgmt_ip') or ""
if mgmt_ip and not ihost.mgmt_ip:
LOG.info("%s create_ihost setting mgmt_ip to %s" %
(ihost.uuid, mgmt_ip))
mgmt_update_required = True
elif mgmt_ip and ihost.mgmt_ip and \
(ihost.mgmt_ip.strip() != mgmt_ip.strip()):
# Changing the management IP on an already configured
# host should not occur nor be allowed.
LOG.error("DANGER %s create_ihost mgmt_ip dnsmasq change "
"detected from %s to %s." %
(ihost.uuid, ihost.mgmt_ip, mgmt_ip))
if mgmt_update_required:
ihost = self.dbapi.ihost_update(ihost.uuid, values)
if ihost.personality and ihost.hostname:
ihost_mtc = ihost.as_dict()
ihost_mtc['operation'] = 'modify'
ihost_mtc = cutils.removekeys_nonmtce(ihost_mtc)
LOG.info("%s create_ihost update mtce %s " %
(ihost.hostname, ihost_mtc))
mtce_api.host_modify(
self._api_token, self._mtc_address, self._mtc_port,
ihost_mtc,
constants.MTC_DEFAULT_TIMEOUT_IN_SECS)
return ihost
except exception.NodeNotFound:
# If host is not found, check if this is cloning scenario.
# If yes, update management MAC in the DB and create PXE config.
clone_host = self.handle_dhcp_lease_from_clone(context, mac)
if clone_host:
return clone_host
# assign default system
system = self.dbapi.isystem_get_one()
values.update({'forisystemid': system.id})
values.update({constants.HOST_ACTION_STATE: constants.HAS_REINSTALLING})
# get tboot value from the active controller
active_controller = None
hosts = self.dbapi.ihost_get_by_personality(constants.CONTROLLER)
for h in hosts:
if utils.is_host_active_controller(h):
active_controller = h
break
software_load = None
if active_controller is not None:
tboot_value = active_controller.get('tboot')
if tboot_value is not None:
values.update({'tboot': tboot_value})
software_load = active_controller.software_load
LOG.info("create_ihost software_load=%s" % software_load)
ihost = self.dbapi.ihost_create(values, software_load=software_load)
# A host is being created, generate discovery log.
self._log_host_create(ihost, reason)
ihost_id = ihost.get('uuid')
LOG.debug("RPC create_ihost called and created ihost %s." % ihost_id)
return ihost
def update_ihost(self, context, ihost_obj):
"""Update an ihost with the supplied data.
This method allows an ihost to be updated.
:param context: an admin context
:param ihost_obj: a changed (but not saved) ihost object
:returns: updated ihost object, including all fields.
"""
ihost_id = ihost_obj['uuid']
LOG.debug("RPC update_ihost called for ihost %s." % ihost_id)
delta = ihost_obj.obj_what_changed()
if ('id' in delta) or ('uuid' in delta):
raise exception.SysinvException(_(
"Invalid method call: update_ihost cannot change id or uuid "))
ihost_obj.save(context)
return ihost_obj
def _dnsmasq_host_entry_to_string(self, ip_addr, hostname,
mac_addr=None, cid=None):
if IPNetwork(ip_addr).version == constants.IPV6_FAMILY:
ip_addr = "[%s]" % ip_addr
if cid:
line = "id:%s,%s,%s,1d\n" % (cid, hostname, ip_addr)
elif mac_addr:
line = "%s,%s,%s,1d\n" % (mac_addr, hostname, ip_addr)
else:
line = "%s,%s\n" % (hostname, ip_addr)
return line
def _dnsmasq_addn_host_entry_to_string(self, ip_addr, hostname,
aliases=None):
if aliases is None:
aliases = []
line = "%s %s" % (ip_addr, hostname)
for alias in aliases:
line = "%s %s" % (line, alias)
line = "%s\n" % line
return line
def _generate_dnsmasq_hosts_file(self, existing_host=None,
deleted_host=None):
"""Regenerates the dnsmasq host and addn_hosts files from database.
:param existing_host: Include this host in list of hosts.
:param deleted_host: Skip over writing MAC address for this host.
"""
if (self.topic == 'test-topic'):
dnsmasq_hosts_file = '/tmp/dnsmasq.hosts'
else:
dnsmasq_hosts_file = tsc.CONFIG_PATH + 'dnsmasq.hosts'
if (self.topic == 'test-topic'):
dnsmasq_addn_hosts_file = '/tmp/dnsmasq.addn_hosts'
else:
dnsmasq_addn_hosts_file = tsc.CONFIG_PATH + 'dnsmasq.addn_hosts'
if deleted_host:
deleted_hostname = deleted_host.hostname
else:
deleted_hostname = None
temp_dnsmasq_hosts_file = dnsmasq_hosts_file + '.temp'
temp_dnsmasq_addn_hosts_file = dnsmasq_addn_hosts_file + '.temp'
mgmt_network = self.dbapi.network_get_by_type(
constants.NETWORK_TYPE_MGMT
)
with open(temp_dnsmasq_hosts_file, 'w') as f_out,\
open(temp_dnsmasq_addn_hosts_file, 'w') as f_out_addn:
# Write entry for pxecontroller into addn_hosts file
try:
self.dbapi.network_get_by_type(
constants.NETWORK_TYPE_PXEBOOT
)
address = self.dbapi.address_get_by_name(
cutils.format_address_name(constants.CONTROLLER_HOSTNAME,
constants.NETWORK_TYPE_PXEBOOT)
)
except exception.NetworkTypeNotFound:
address = self.dbapi.address_get_by_name(
cutils.format_address_name(constants.CONTROLLER_HOSTNAME,
constants.NETWORK_TYPE_MGMT)
)
addn_line = self._dnsmasq_addn_host_entry_to_string(
address.address, constants.PXECONTROLLER_HOSTNAME
)
f_out_addn.write(addn_line)
# Loop through mgmt addresses to write to file
for address in self.dbapi._addresses_get_by_pool_uuid(
mgmt_network.pool_uuid):
line = None
hostname = re.sub("-%s$" % constants.NETWORK_TYPE_MGMT,
'', str(address.name))
if address.interface:
mac_address = address.interface.imac
# For cloning scenario, controller-1 MAC address will
# be updated in ethernet_interfaces table only later
# when sysinv-agent is initialized on controller-1.
# So, use the mac_address passed in (got from PXE request).
if (existing_host and
constants.CLONE_ISO_MAC in mac_address and
hostname == existing_host.hostname):
LOG.info("gen dnsmasq (clone):{}:{}->{}"
.format(hostname, mac_address,
existing_host.mgmt_mac))
mac_address = existing_host.mgmt_mac
# If host is being deleted, don't check ihost
elif deleted_hostname and deleted_hostname == hostname:
mac_address = None
else:
try:
ihost = self.dbapi.ihost_get_by_hostname(hostname)
mac_address = ihost.mgmt_mac
except exception.NodeNotFound:
if existing_host and existing_host.hostname == hostname:
mac_address = existing_host.mgmt_mac
else:
mac_address = None
line = self._dnsmasq_host_entry_to_string(address.address,
hostname,
mac_address)
f_out.write(line)
# Update host files atomically and reload dnsmasq
if (not os.path.isfile(dnsmasq_hosts_file) or
not filecmp.cmp(temp_dnsmasq_hosts_file, dnsmasq_hosts_file)):
os.rename(temp_dnsmasq_hosts_file, dnsmasq_hosts_file)
if (not os.path.isfile(dnsmasq_addn_hosts_file) or
not filecmp.cmp(temp_dnsmasq_addn_hosts_file,
dnsmasq_addn_hosts_file)):
os.rename(temp_dnsmasq_addn_hosts_file, dnsmasq_addn_hosts_file)
# If there is no distributed cloud addn_hosts file, create an empty one
# so dnsmasq will not complain.
dnsmasq_addn_hosts_dc_file = os.path.join(tsc.CONFIG_PATH, 'dnsmasq.addn_hosts_dc')
temp_dnsmasq_addn_hosts_dc_file = os.path.join(tsc.CONFIG_PATH, 'dnsmasq.addn_hosts_dc.temp')
if not os.path.isfile(dnsmasq_addn_hosts_dc_file):
with open(temp_dnsmasq_addn_hosts_dc_file, 'w') as f_out_addn_dc:
f_out_addn_dc.write(' ')
os.rename(temp_dnsmasq_addn_hosts_dc_file, dnsmasq_addn_hosts_dc_file)
os.system("pkill -HUP dnsmasq")
def _update_pxe_config(self, host, load=None):
"""Set up the PXE config file for this host so it can run
the installer.
This method must always be backward compatible with the previous
software release. During upgrades, this method is called when
locking/unlocking hosts running the previous release and when
downgrading a host. In both cases, it must be able to re-generate
the host's pxe config files appropriate to that host's software
version, using the pxeboot-update-<release>.sh script from the
previous release.
:param host: host object.
"""
sw_version = tsc.SW_VERSION
if load:
sw_version = load.software_version
else:
# No load provided, look it up...
host_upgrade = self.dbapi.host_upgrade_get_by_host(host.id)
target_load = self.dbapi.load_get(host_upgrade.target_load)
sw_version = target_load.software_version
if (host.personality == constants.CONTROLLER and
constants.WORKER in tsc.subfunctions):
if constants.LOWLATENCY in host.subfunctions:
pxe_config = "pxe-smallsystem_lowlatency-install-%s" % sw_version
else:
pxe_config = "pxe-smallsystem-install-%s" % sw_version
elif host.personality == constants.CONTROLLER:
pxe_config = "pxe-controller-install-%s" % sw_version
elif host.personality == constants.WORKER:
if constants.LOWLATENCY in host.subfunctions:
pxe_config = "pxe-worker_lowlatency-install-%s" % sw_version
else:
pxe_config = "pxe-worker-install-%s" % sw_version
elif host.personality == constants.STORAGE:
pxe_config = "pxe-storage-install-%s" % sw_version
# Defaults for configurable install parameters
install_opts = []
boot_device = host.get('boot_device') or "/dev/sda"
install_opts += ['-b', boot_device]
rootfs_device = host.get('rootfs_device') or "/dev/sda"
install_opts += ['-r', rootfs_device]
install_output = host.get('install_output') or "text"
if install_output == "text":
install_output_arg = "-t"
elif install_output == "graphical":
install_output_arg = "-g"
else:
LOG.warning("install_output set to invalid value (%s)"
% install_output)
install_output_arg = "-t"
install_opts += [install_output_arg]
# This method is called during upgrades to
# re-generate the host's pxe config files to the appropriate host's
# software version. It is required specifically when we downgrade a
# host or when we lock/unlock a host.
host_uuid = host.get('uuid')
notify_url = \
"http://pxecontroller:%d/v1/ihosts/%s/install_progress" % \
(CONF.sysinv_api_port, host_uuid)
install_opts += ['-u', notify_url]
system = self.dbapi.isystem_get_one()
secprofile = system.security_profile
# ensure that the securtiy profile selection is valid
if secprofile not in [constants.SYSTEM_SECURITY_PROFILE_STANDARD,
constants.SYSTEM_SECURITY_PROFILE_EXTENDED]:
LOG.error("Security Profile (%s) not a valid selection. "
"Defaulting to: %s" % (secprofile,
constants.SYSTEM_SECURITY_PROFILE_STANDARD))
secprofile = constants.SYSTEM_SECURITY_PROFILE_STANDARD
install_opts += ['-s', secprofile]
# If 'console' is not present in ihost_obj, we want to use the default.
# If, however, it is present and is explicitly set to None or "", then
# we don't specify the -c argument at all.
if 'console' not in host:
console = "ttyS0,115200"
else:
console = host.get('console')
if console is not None and console != "":
install_opts += ['-c', console]
# If 'tboot' is present in ihost_obj, retrieve and send the value
if 'tboot' in host:
tboot = host.get('tboot')
if tboot is not None and tboot != "":
install_opts += ['-T', tboot]
install_opts += ['-k', system.security_feature]
base_url = "http://pxecontroller:%d" % cutils.get_http_port(self.dbapi)
install_opts += ['-l', base_url]
if host['mgmt_mac']:
dashed_mac = host["mgmt_mac"].replace(":", "-")
pxeboot_update = "/usr/sbin/pxeboot-update-%s.sh" % sw_version
# Remove an old file if it exists
try:
os.remove("/pxeboot/pxelinux.cfg/01-" + dashed_mac)
except OSError:
pass
try:
os.remove("/pxeboot/pxelinux.cfg/efi-01-" + dashed_mac)
except OSError:
pass
with open(os.devnull, "w") as fnull:
try:
subprocess.check_call( # pylint: disable=not-callable
[pxeboot_update, "-i", "/pxeboot/pxelinux.cfg.files/" +
pxe_config, "-o", "/pxeboot/pxelinux.cfg/01-" +
dashed_mac] + install_opts,
stdout=fnull,
stderr=fnull)
except subprocess.CalledProcessError:
raise exception.SysinvException(_(
"Failed to create pxelinux.cfg file"))
def _enable_etcd_security_config(self, context):
"""Update the manifests for etcd security
Note: this can be removed in the release after STX5.0
returns True if runtime manifests were applied
"""
controllers = self.dbapi.ihost_get_by_personality(constants.CONTROLLER)
for host in controllers:
if not utils.is_host_active_controller(host):
# Just enable etcd security on the standby controller.
# Etcd security was enabled on the active controller with a
# migration script.
personalities = [constants.CONTROLLER]
host_uuids = [host.uuid]
config_uuid = self._config_update_hosts(
context, personalities, host_uuids)
config_dict = {
"personalities": personalities,
"host_uuids": host_uuids,
"classes": ['platform::etcd::upgrade::runtime'],
puppet_common.REPORT_STATUS_CFG:
puppet_common.REPORT_UPGRADE_ACTIONS
}
self._config_apply_runtime_manifest(context,
config_uuid=config_uuid,
config_dict=config_dict)
return True
return False
def _remove_pxe_config(self, host):
"""Delete the PXE config file for this host.
:param host: host object.
"""
if host.mgmt_mac:
dashed_mac = host.mgmt_mac.replace(":", "-")
# Remove the old file if it exists
try:
os.remove("/pxeboot/pxelinux.cfg/01-" + dashed_mac)
except OSError:
pass
try:
os.remove("/pxeboot/pxelinux.cfg/efi-01-" + dashed_mac)
except OSError:
pass
def _create_or_update_address(self, context, hostname, ip_address,
iface_type, iface_id=None):
if hostname is None or ip_address is None:
return
address_name = cutils.format_address_name(hostname, iface_type)
address_family = IPNetwork(ip_address).version
try:
address = self.dbapi.address_get_by_address(ip_address)
address_uuid = address['uuid']
# If name is already set, return
if (self.dbapi.address_get_by_name(address_name) ==
address_uuid and iface_id is None):
return
except exception.AddressNotFoundByAddress:
address_uuid = None
except exception.AddressNotFoundByName:
pass
network = self.dbapi.network_get_by_type(iface_type)
address_pool_uuid = network.pool_uuid
address_pool = self.dbapi.address_pool_get(address_pool_uuid)
values = {
'name': address_name,
'family': address_family,
'prefix': address_pool.prefix,
'address': ip_address,
'address_pool_id': address_pool.id,
}
if iface_id:
values['interface_id'] = iface_id
if address_uuid:
address = self.dbapi.address_update(address_uuid, values)
else:
address = self.dbapi.address_create(values)
self._generate_dnsmasq_hosts_file()
return address
def _allocate_pool_address(self, interface_id, pool_uuid, address_name):
return address_pool.AddressPoolController.assign_address(
interface_id, pool_uuid, address_name, dbapi=self.dbapi
)
def _allocate_addresses_for_host(self, context, host):
"""Allocates addresses for a given host.
Does the following tasks:
- Check if addresses exist for host
- Allocate addresses for host from pools
- Update ihost with mgmt address
- Regenerate the dnsmasq hosts file
:param context: request context
:param host: host object
"""
mgmt_ip = host.mgmt_ip
mgmt_interfaces = self.iinterfaces_get_by_ihost_nettype(
context, host.uuid, constants.NETWORK_TYPE_MGMT
)
mgmt_interface_id = None
if mgmt_interfaces:
mgmt_interface_id = mgmt_interfaces[0]['id']
hostname = host.hostname
address_name = cutils.format_address_name(hostname,
constants.NETWORK_TYPE_MGMT)
# if ihost has mgmt_ip, make sure address in address table
if mgmt_ip:
self._create_or_update_address(context, hostname, mgmt_ip,
constants.NETWORK_TYPE_MGMT,
mgmt_interface_id)
# if ihost has no management IP, check for static mgmt IP
if not mgmt_ip:
mgmt_ip = self._lookup_static_ip_address(
hostname, constants.NETWORK_TYPE_MGMT
)
if mgmt_ip:
host.mgmt_ip = mgmt_ip
self.update_ihost(context, host)
# if no static address, then allocate one
if not mgmt_ip:
mgmt_pool = self.dbapi.network_get_by_type(
constants.NETWORK_TYPE_MGMT
).pool_uuid
mgmt_ip = self._allocate_pool_address(mgmt_interface_id, mgmt_pool,
address_name).address
if mgmt_ip:
host.mgmt_ip = mgmt_ip
self.update_ihost(context, host)
self._generate_dnsmasq_hosts_file(existing_host=host)
def get_my_host_id(self):
if not ConductorManager.my_host_id:
local_hostname = socket.gethostname()
controller = self.dbapi.ihost_get(local_hostname)
ConductorManager.my_host_id = controller['id']
return ConductorManager.my_host_id
def get_dhcp_server_duid(self):
"""Retrieves the server DUID from the local DHCP server lease file."""
lease_filename = tsc.CONFIG_PATH + 'dnsmasq.leases'
with open(lease_filename, 'r') as lease_file:
for columns in (line.strip().split() for line in lease_file):
if len(columns) != 2:
continue
keyword, value = columns
if keyword.lower() == "duid":
return value
def _dhcp_release(self, interface, ip_address, mac_address, cid=None):
"""Release a given DHCP lease"""
params = [interface, ip_address, mac_address]
if cid:
params += [cid]
if IPAddress(ip_address).version == 6:
params = ["--ip", ip_address,
"--iface", interface,
"--server-id", self.get_dhcp_server_duid(),
"--client-id", cid,
"--iaid", str(cutils.get_dhcp_client_iaid(mac_address))]
LOG.warning("Invoking dhcp_release6 for {}".format(params))
subprocess.call(["dhcp_release6"] + params) # pylint: disable=not-callable
else:
LOG.warning("Invoking dhcp_release for {}".format(params))
subprocess.call(["dhcp_release"] + params) # pylint: disable=not-callable
def _find_networktype_for_address(self, ip_address):
for network in self.dbapi.networks_get_all():
pool = self.dbapi.address_pool_get(network.pool_uuid)
subnet = IPNetwork(pool.network + '/' + str(pool.prefix))
address = IPAddress(ip_address)
if address in subnet:
return network.type
def _find_local_interface_name(self, network_type):
"""Lookup the local interface name for a given network type."""
host_id = self.get_my_host_id()
interface_list = self.dbapi.iinterface_get_all(host_id, expunge=True)
ifaces = dict((i['ifname'], i) for i in interface_list)
port_list = self.dbapi.port_get_all(host_id)
ports = dict((p['interface_id'], p) for p in port_list)
for interface in interface_list:
if network_type in interface.networktypelist:
return cutils.get_interface_os_ifname(interface, ifaces, ports)
def _find_local_mgmt_interface_vlan_id(self):
"""Lookup the local interface name for a given network type."""
host_id = self.get_my_host_id()
interface_list = self.dbapi.iinterface_get_all(host_id, expunge=True)
for interface in interface_list:
if constants.NETWORK_TYPE_MGMT in interface.networktypelist:
if 'vlan_id' not in interface:
return 0
else:
return interface['vlan_id']
def _remove_leases_by_mac_address(self, mac_address):
"""Remove any leases that were added without a CID that we were not
able to delete. This is specifically looking for leases on the pxeboot
network that may still be present but will also handle the unlikely
event of deleting an old host during an upgrade. Hosts on previous
releases did not register a CID on the mgmt interface."""
lease_filename = tsc.CONFIG_PATH + 'dnsmasq.leases'
try:
with open(lease_filename, 'r') as lease_file:
for columns in (line.strip().split() for line in lease_file):
if len(columns) != 5:
continue
timestamp, address, ip_address, hostname, cid = columns
if address != mac_address:
continue
network_type = self._find_networktype_for_address(ip_address)
if not network_type:
# Not one of our managed networks
LOG.warning("Lease for unknown network found in "
"dnsmasq.leases file: {}".format(columns))
continue
interface_name = self._find_local_interface_name(
network_type
)
self._dhcp_release(interface_name, ip_address, mac_address)
except Exception as e:
LOG.error("Failed to remove leases for %s: %s" % (mac_address,
str(e)))
def _remove_lease_for_address(self, hostname, network_type):
"""Remove the lease for a given address"""
address_name = cutils.format_address_name(hostname, network_type)
try:
interface_name = self._find_local_interface_name(network_type)
if not interface_name:
return
address = self.dbapi.address_get_by_name(address_name)
interface_id = address.interface_id
ip_address = address.address
if interface_id:
interface = self.dbapi.iinterface_get(interface_id)
mac_address = interface.imac
elif network_type == constants.NETWORK_TYPE_MGMT:
ihost = self.dbapi.ihost_get_by_hostname(hostname)
mac_address = ihost.mgmt_mac
else:
return
cid = cutils.get_dhcp_cid(hostname, network_type, mac_address)
self._dhcp_release(interface_name, ip_address, mac_address, cid)
except Exception as e:
LOG.error("Failed to remove lease %s: %s" % (address_name,
str(e)))
def _unallocate_address(self, hostname, network_type):
"""Unallocate address if it exists"""
address_name = cutils.format_address_name(hostname, network_type)
if network_type == constants.NETWORK_TYPE_MGMT:
self._remove_lease_for_address(hostname, network_type)
try:
address_uuid = self.dbapi.address_get_by_name(address_name).uuid
self.dbapi.address_remove_interface(address_uuid)
except exception.AddressNotFoundByName:
pass
def _remove_address(self, hostname, network_type):
"""Remove address if it exists"""
address_name = cutils.format_address_name(hostname, network_type)
self._remove_lease_for_address(hostname, network_type)
try:
address_uuid = self.dbapi.address_get_by_name(address_name).uuid
self.dbapi.address_destroy(address_uuid)
except exception.AddressNotFoundByName:
pass
except exception.AddressNotFound:
pass
def _unallocate_addresses_for_host(self, host):
"""Unallocates management addresses for a given host.
:param host: host object
"""
hostname = host.hostname
self._unallocate_address(hostname, constants.NETWORK_TYPE_MGMT)
self._unallocate_address(hostname, constants.NETWORK_TYPE_CLUSTER_HOST)
if host.personality == constants.CONTROLLER:
self._unallocate_address(hostname, constants.NETWORK_TYPE_OAM)
self._unallocate_address(hostname, constants.NETWORK_TYPE_PXEBOOT)
self._remove_leases_by_mac_address(host.mgmt_mac)
self._generate_dnsmasq_hosts_file(deleted_host=host)
def _remove_addresses_for_host(self, host):
"""Removes management addresses for a given host.
:param host: host object
"""
hostname = host.hostname
self._remove_address(hostname, constants.NETWORK_TYPE_MGMT)
self._remove_leases_by_mac_address(host.mgmt_mac)
self._generate_dnsmasq_hosts_file(deleted_host=host)
def _update_host_lvm_config(self, context, host, force=False):
personalities = [host.personality]
# For rook must update lvm filter
config_dict = {
"host_uuids": [host.uuid],
}
if host.personality == constants.CONTROLLER:
config_dict["personalities"] = [constants.CONTROLLER]
config_dict["classes"] = ['platform::lvm::controller::runtime']
elif host.personality == constants.WORKER:
config_dict["personalities"] = [constants.WORKER]
config_dict["classes"] = ['platform::lvm::compute::runtime']
config_uuid = self._config_update_hosts(context, personalities,
host_uuids=[host.uuid])
self._config_apply_runtime_manifest(context,
config_uuid,
config_dict,
force=force)
def _configure_controller_host(self, context, host):
"""Configure a controller host with the supplied data.
Does the following tasks:
- Update the puppet hiera data configuration for host
- Allocates management address if none exists
- Set up PXE configuration to run installer
- Update grub for AIO before initial unlock
:param context: request context
:param host: host object
"""
if self.host_load_matches_sw_version(host):
# update the config if the host is running the same version as
# the active controller.
if (host.administrative == constants.ADMIN_UNLOCKED or
host.action == constants.FORCE_UNLOCK_ACTION or
host.action == constants.UNLOCK_ACTION):
# Update host configuration
self._puppet.update_host_config(host)
else:
# from active controller, update hieradata for upgrade
host_uuids = [host.uuid]
config_uuid = self._config_update_hosts(
context,
[constants.CONTROLLER],
host_uuids,
reboot=True)
host_upgrade = self.dbapi.host_upgrade_get_by_host(host.id)
target_load = self.dbapi.load_get(host_upgrade.target_load)
self._puppet.update_host_config_upgrade(
host,
target_load.software_version,
config_uuid
)
self._allocate_addresses_for_host(context, host)
# Set up the PXE config file for this host so it can run the installer
self._update_pxe_config(host)
self._ceph_mon_create(host)
if (os.path.isfile(constants.ANSIBLE_BOOTSTRAP_FLAG) and
host.availability == constants.AVAILABILITY_ONLINE):
# This must be the initial controller host unlock request.
personalities = [constants.CONTROLLER]
if not cutils.is_aio_system(self.dbapi):
# Standard system, touch the unlock ready flag
cutils.touch(constants.UNLOCK_READY_FLAG)
else:
# AIO, must update grub before the unlock. Sysinv agent expects
# this exact set of manifests in order to touch the unlock ready
# flag after they have been applied.
config_uuid = self._config_update_hosts(context, personalities,
host_uuids=[host.uuid])
if self._config_is_reboot_required(host.config_target):
config_uuid = self._config_set_reboot_required(config_uuid)
config_dict = {
"personalities": personalities,
"host_uuids": [host.uuid],
"classes": ['platform::compute::grub::runtime',
'platform::compute::config::runtime']
}
self._config_apply_runtime_manifest(
context, config_uuid, config_dict, force=True)
# Regenerate config target uuid, node is going for reboot!
config_uuid = self._config_update_hosts(context, personalities)
if self._config_is_reboot_required(host.config_target):
config_uuid = self._config_set_reboot_required(config_uuid)
self._puppet.update_host_config(host, config_uuid)
def _ceph_mon_create(self, host):
if not StorageBackendConfig.has_backend(
self.dbapi,
constants.CINDER_BACKEND_CEPH
):
return
if not self.dbapi.ceph_mon_get_by_ihost(host.uuid):
system = self.dbapi.isystem_get_one()
ceph_mon_gib = constants.SB_CEPH_MON_GIB
ceph_mons = self.dbapi.ceph_mon_get_list()
if ceph_mons:
ceph_mon_gib = ceph_mons[0].ceph_mon_gib
values = {'forisystemid': system.id,
'forihostid': host.id,
'ceph_mon_gib': ceph_mon_gib,
'state': constants.SB_STATE_CONFIGURED,
'task': constants.SB_TASK_NONE}
LOG.info("creating ceph_mon for host %s with ceph_mon_gib=%s."
% (host.hostname, ceph_mon_gib))
self.dbapi.ceph_mon_create(values)
def _remove_ceph_mon(self, host):
if not StorageBackendConfig.has_backend(
self.dbapi,
constants.CINDER_BACKEND_CEPH
):
return
mon = self.dbapi.ceph_mon_get_by_ihost(host.uuid)
if mon:
LOG.info("Deleting ceph monitor for host %s"
% str(host.hostname))
self.dbapi.ceph_mon_destroy(mon[0].uuid)
# At this point self._ceph should always be set, but we check
# just to be sure
if self._ceph is not None:
self._ceph.remove_ceph_monitor(host.hostname)
else:
# This should never happen, but if it does, log it so
# there is a trace of it
LOG.error("Error deleting ceph monitor")
else:
LOG.info("No ceph monitor present for host %s. "
"Skipping deleting ceph monitor."
% str(host.hostname))
def update_remotelogging_config(self, context):
"""Update the remotelogging configuration"""
personalities = [constants.CONTROLLER,
constants.WORKER,
constants.STORAGE]
config_uuid = self._config_update_hosts(context, personalities)
config_dict = {
"personalities": [constants.CONTROLLER],
"classes": ['platform::sysctl::controller::runtime',
'platform::remotelogging::runtime']
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
config_dict = {
"personalities": [constants.WORKER, constants.STORAGE],
"classes": ['platform::remotelogging::runtime'],
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
def docker_registry_image_list(self, context):
try:
image_list_response = docker_registry.docker_registry_get("_catalog")
except requests.exceptions.SSLError:
LOG.exception("Failed to get docker registry catalog")
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to get docker registry catalog")
raise exception.DockerRegistryAPIException()
if image_list_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% image_list_response.status_code)
return []
image_list_response = image_list_response.json()
images = []
# responses from the registry looks like this
# {u'repositories': [u'meliodas/satesatesate', ...]}
# we need to turn that into what we want to return:
# [{'name': u'meliodas/satesatesate'}]
if 'repositories' not in image_list_response:
return images
image_list_response = image_list_response['repositories']
for image in image_list_response:
images.append({'name': image})
return images
def docker_registry_image_tags(self, context, image_name):
try:
image_tags_response = docker_registry.docker_registry_get(
"%s/tags/list" % image_name)
except requests.exceptions.SSLError:
LOG.exception("Failed to get docker registry image tags")
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to get docker registry image tags")
raise exception.DockerRegistryAPIException()
if image_tags_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% image_tags_response.status_code)
return []
image_tags_response = image_tags_response.json()
tags = []
if 'tags' not in image_tags_response:
return tags
image_tags_response = image_tags_response['tags']
# in the case where all tags of an image is deleted but not
# garbage collected
# the response will contain "tags:null"
if image_tags_response is not None:
for tag in image_tags_response:
tags.append({'name': image_name, 'tag': tag})
return tags
# assumes image_name_and_tag is already error checked to contain "name:tag"
def docker_registry_image_delete(self, context, image_name_and_tag):
image_name_and_tag = image_name_and_tag.split(":")
# first get the image digest for the image name and tag provided
try:
digest_resp = docker_registry.docker_registry_get("%s/manifests/%s"
% (image_name_and_tag[0], image_name_and_tag[1]))
except requests.exceptions.SSLError:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistryAPIException()
if digest_resp.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% digest_resp.status_code)
return
image_digest = digest_resp.headers['Docker-Content-Digest']
# now delete the image
try:
image_delete_response = docker_registry.docker_registry_delete(
"%s/manifests/%s" % (image_name_and_tag[0], image_digest))
except requests.exceptions.SSLError:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistrySSLException()
except Exception:
LOG.exception("Failed to delete docker registry image %s" %
image_name_and_tag)
raise exception.DockerRegistryAPIException()
if image_delete_response.status_code != 202:
LOG.error("Bad response from docker registry: %s"
% digest_resp.status_code)
return
def docker_registry_garbage_collect(self, context):
"""Run garbage collector"""
active_controller = utils.HostHelper.get_active_controller(self.dbapi)
personalities = [constants.CONTROLLER]
config_uuid = self._config_update_hosts(context, personalities,
[active_controller.uuid])
config_dict = {
"personalities": personalities,
"host_uuids": [active_controller.uuid],
"classes": ['platform::dockerdistribution::garbagecollect']
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
def docker_get_apps_images(self, context):
"""
Return a dictionary of all apps and associated images from local registry.
"""
images = {}
try:
for kapp in self.dbapi.kube_app_get_all():
app = self._app.Application(kapp)
images_to_download = self._app.get_image_tags_by_charts(
app.sync_imgfile, app.sync_armada_mfile, app.sync_overrides_dir)
stripped_images = [x.replace(constants.DOCKER_REGISTRY_HOST + ':' +
constants.DOCKER_REGISTRY_PORT + '/', '')
for x in images_to_download]
images[kapp.name] = stripped_images
LOG.info("Application images for %s are: %s" % (kapp.name,
str(stripped_images)))
except Exception as e:
LOG.info("Get images for all apps error.")
LOG.exception(e)
return images
def _configure_worker_host(self, context, host):
"""Configure a worker host with the supplied data.
Does the following tasks:
- Create or update entries in address table
- Generate the configuration file for the host
- Allocates management address if none exists
- Set up PXE configuration to run installer
:param context: request context
:param host: host object
"""
# Only update the config if the host is running the same version as
# the active controller.
if self.host_load_matches_sw_version(host):
# Only generate the config files if the worker host is unlocked.
if (host.administrative == constants.ADMIN_UNLOCKED or
host.action == constants.FORCE_UNLOCK_ACTION or
host.action == constants.UNLOCK_ACTION):
# Generate host configuration file
self._puppet.update_host_config(host)
else:
LOG.info("Host %s is not running active load. "
"Skipping manifest generation" % host.hostname)
self._allocate_addresses_for_host(context, host)
# Set up the PXE config file for this host so it can run the installer
self._update_pxe_config(host)
def _configure_edgeworker_host(self, context, host):
"""Configure an edgeworker host with the supplied data.
Does the following tasks:
- Create or update entries in address table
- Allocates management address if none exists
:param context: request context
:param host: host object
"""
self._allocate_addresses_for_host(context, host)
def _configure_storage_host(self, context, host):
"""Configure a storage ihost with the supplied data.
Does the following tasks:
- Update the puppet hiera data configuration for host
- Allocates management address if none exists
- Set up PXE configuration to run installer
:param context: request context
:param host: host object
"""
# Update cluster and peers model.
# We call this function when setting the personality of a storage host.
# In cases where we configure the storage-backend before unlocking
# controller-0, and then configuring all other hosts, ceph will not be
# responsive (and self._ceph not be set) when setting the storage
# personality.
# But that's ok, because this function is also called when unlocking a
# storage node and we are guaranteed (by consistency checks) a
# responsive ceph cluster at that point in time and we can update the
# ceph cluster information succesfully.
if self._ceph is not None:
self._ceph.update_ceph_cluster(host)
else:
# It's ok, we just log a message for debug purposes
LOG.debug("Error updating cluster information")
# Only update the manifest if the host is running the same version as
# the active controller.
if self.host_load_matches_sw_version(host):
# Only generate the manifest files if the storage host is unlocked.
# At that point changes are no longer allowed to the hostname, so
# it is OK to allow the node to boot and configure the platform
# services.
if (host.administrative == constants.ADMIN_UNLOCKED or
host.action == constants.FORCE_UNLOCK_ACTION or
host.action == constants.UNLOCK_ACTION):
# Generate host configuration files
self._puppet.update_host_config(host)
else:
LOG.info("Host %s is not running active load. "
"Skipping manifest generation" % host.hostname)
self._allocate_addresses_for_host(context, host)
# Set up the PXE config file for this host so it can run the installer
self._update_pxe_config(host)
if host['hostname'] == constants.STORAGE_0_HOSTNAME:
self._ceph_mon_create(host)
def remove_host_config(self, context, host_uuid):
"""Remove configuration files for a host.
:param context: an admin context.
:param host_uuid: host uuid.
"""
host = self.dbapi.ihost_get(host_uuid)
self._puppet.remove_host_config(host)
def _unconfigure_controller_host(self, host):
"""Unconfigure a controller host.
Does the following tasks:
- Remove the puppet hiera data configuration for host
- Remove host entry in the dnsmasq hosts file
- Delete PXE configuration
:param host: a host object.
"""
self._unallocate_addresses_for_host(host)
self._puppet.remove_host_config(host)
self._remove_pxe_config(host)
# Create the simplex flag on this controller because our mate has
# been deleted.
cutils.touch(tsc.PLATFORM_SIMPLEX_FLAG)
if host.hostname == constants.CONTROLLER_0_HOSTNAME:
self.controller_0_posted = False
elif host.hostname == constants.CONTROLLER_1_HOSTNAME:
self.controller_1_posted = False
def _unconfigure_worker_host(self, host, is_cpe=False):
"""Unconfigure a worker host.
Does the following tasks:
- Remove the puppet hiera data configuration for host
- Remove the host entry from the dnsmasq hosts file
- Delete PXE configuration
:param host: a host object.
:param is_cpe: this node is a combined node
"""
if not is_cpe:
self._remove_addresses_for_host(host)
self._puppet.remove_host_config(host)
self._remove_pxe_config(host)
self._remove_ceph_mon(host)
def _unconfigure_edgeworker_host(self, host):
"""Unconfigure an edgeworker host.
:param host: a host object.
"""
self._remove_addresses_for_host(host)
def _unconfigure_storage_host(self, host):
"""Unconfigure a storage host.
Does the following tasks:
- Remove the puppet hiera data configuration for host
- Remove host entry in the dnsmasq hosts file
- Delete PXE configuration
:param host: a host object.
"""
self._unallocate_addresses_for_host(host)
self._puppet.remove_host_config(host)
self._remove_pxe_config(host)
def configure_ihost(self, context, host,
do_worker_apply=False):
"""Configure a host.
:param context: an admin context.
:param host: a host object.
:param do_worker_apply: configure the worker subfunctions of the host.
"""
LOG.info("configure_ihost %s" % host.hostname)
# Generate system configuration files
# TODO(mpeters): remove this once all system reconfigurations properly
# invoke this method
self._puppet.update_system_config()
self._puppet.update_secure_system_config()
if host.personality == constants.CONTROLLER:
self._configure_controller_host(context, host)
elif host.personality == constants.WORKER:
self._configure_worker_host(context, host)
elif host.personality == constants.EDGEWORKER:
self._configure_edgeworker_host(context, host)
elif host.personality == constants.STORAGE:
self._configure_storage_host(context, host)
else:
raise exception.SysinvException(_(
"Invalid method call: unsupported personality: %s") %
host.personality)
if do_worker_apply:
# Apply the manifests immediately
puppet_common.puppet_apply_manifest(host.mgmt_ip,
constants.WORKER,
do_reboot=True)
return host
def unconfigure_ihost(self, context, ihost_obj):
"""Unconfigure a host.
:param context: an admin context.
:param ihost_obj: a host object.
"""
LOG.debug("unconfigure_ihost %s." % ihost_obj.uuid)
# Configuring subfunctions of the node instead
if ihost_obj.subfunctions:
personalities = cutils.get_personalities(ihost_obj)
is_cpe = cutils.is_cpe(ihost_obj)
else:
personalities = (ihost_obj.personality,)
is_cpe = False
for personality in personalities:
if personality == constants.CONTROLLER:
self._unconfigure_controller_host(ihost_obj)
elif personality == constants.WORKER:
self._unconfigure_worker_host(ihost_obj, is_cpe)
elif personality == constants.EDGEWORKER:
self._unconfigure_edgeworker_host(ihost_obj)
elif personality == constants.STORAGE:
self._unconfigure_storage_host(ihost_obj)
else:
# allow a host with no personality to be unconfigured
pass
def _update_dependent_interfaces(self, interface, ihost,
phy_intf, newmac, depth=1):
""" Updates the MAC address for dependent logical interfaces.
:param interface: interface object
:param ihost: host object
:param phy_intf: physical interface name
:newmac: MAC address to be updated
"""
if depth > 5:
# be safe! dont loop for cyclic DB entries
LOG.error("Looping? [{}] {}:{}".format(depth, phy_intf, newmac))
return
label = constants.CLONE_ISO_MAC + ihost['hostname'] + phy_intf
if hasattr(interface, 'used_by'):
LOG.info("clone_mac_update: {} used_by {} on {}".format(
interface['ifname'], interface['used_by'], ihost['hostname']))
for i in interface['used_by']:
used_by_if = self.dbapi.iinterface_get(i, ihost['uuid'])
if used_by_if:
LOG.debug("clone_mac_update: Found used_by_if: {} {} --> {} [{}]"
.format(used_by_if['ifname'],
used_by_if['imac'],
newmac, label))
if label in used_by_if['imac']:
updates = {'imac': newmac}
self.dbapi.iinterface_update(used_by_if['uuid'], updates)
LOG.info("clone_mac_update: MAC updated: {} {} --> {} [{}]"
.format(used_by_if['ifname'],
used_by_if['imac'],
newmac, label))
# look for dependent interfaces of this one.
self._update_dependent_interfaces(used_by_if, ihost, phy_intf,
newmac, depth + 1)
def validate_cloned_interfaces(self, ihost_uuid):
"""Check if all the cloned interfaces are reported by the host.
:param ihost_uuid: ihost uuid unique id
"""
LOG.info("clone_mac_update: validate_cloned_interfaces %s" % ihost_uuid)
try:
iinterfaces = self.dbapi.iinterface_get_by_ihost(ihost_uuid,
expunge=True)
except exc.DetachedInstanceError:
# A rare DetachedInstanceError exception may occur, retry
LOG.warn("Detached Instance Error, retry "
"iinterface_get_by_ihost %s" % ihost_uuid)
iinterfaces = self.dbapi.iinterface_get_by_ihost(ihost_uuid,
expunge=True)
for interface in iinterfaces:
if constants.CLONE_ISO_MAC in interface['imac']:
LOG.warn("Missing interface [{},{}] on the cloned host"
.format(interface['ifname'], interface['id']))
raise exception.SysinvException(_(
"Missing interface on the cloned host"))
def iport_update_by_ihost(self, context,
ihost_uuid, inic_dict_array):
"""Create iports for an ihost with the supplied data.
This method allows records for iports for ihost to be created.
:param context: an admin context
:param ihost_uuid: ihost uuid unique id
:param inic_dict_array: initial values for iport objects
:returns: pass or fail
"""
LOG.debug("Entering iport_update_by_ihost %s %s" %
(ihost_uuid, inic_dict_array))
ihost_uuid.strip()
try:
ihost = self.dbapi.ihost_get(ihost_uuid)
except exception.ServerNotFound:
LOG.exception("Invalid ihost_uuid %s" % ihost_uuid)
return
try:
hostname = socket.gethostname()
except socket.error:
LOG.exception("Failed to get local hostname")
hostname = None
try:
iinterfaces = self.dbapi.iinterface_get_by_ihost(ihost_uuid,
expunge=True)
except exc.DetachedInstanceError:
# A rare DetachedInstanceError exception may occur, retry
LOG.warn("Detached Instance Error, retry "
"iinterface_get_by_ihost %s" % ihost_uuid)
iinterfaces = self.dbapi.iinterface_get_by_ihost(ihost_uuid,
expunge=True)
for i in iinterfaces:
if constants.NETWORK_TYPE_MGMT in i.networktypelist:
break
cloning = False
for inic in inic_dict_array:
LOG.debug("Processing inic %s" % inic)
interface_exists = False
networktype = None
ifclass = None
bootp = None
create_tagged_interface = False
new_interface = None
set_address_interface = False
mtu = constants.DEFAULT_MTU
port = None
vlan_id = self._find_local_mgmt_interface_vlan_id()
# ignore port if no MAC address present, this will
# occur for data port after they are configured via DPDK driver
if not inic['mac']:
continue
try:
inic_dict = {'host_id': ihost['id']}
inic_dict.update(inic)
ifname = inic['pname']
if cutils.is_valid_mac(inic['mac']):
# Is this the port that the management interface is on?
if inic['mac'].strip() == ihost['mgmt_mac'].strip():
if ihost['hostname'] != hostname:
# auto create management/pxeboot network for all
# nodes but the active controller
if vlan_id:
create_tagged_interface = True
networktype = constants.NETWORK_TYPE_PXEBOOT
ifname = 'pxeboot0'
else:
networktype = constants.NETWORK_TYPE_MGMT
ifname = 'mgmt0'
ifclass = constants.INTERFACE_CLASS_PLATFORM
set_address_interface = True
bootp = 'True'
clone_mac_updated = False
for interface in iinterfaces:
LOG.debug("Checking interface %s" % interface)
if interface['imac'] == inic['mac']:
# append to port attributes as well
inic_dict.update({
'interface_id': interface['id'], 'bootp': bootp
})
# interface already exists so don't create another
interface_exists = True
LOG.debug("interface mac match inic mac %s, inic_dict "
"%s, interface_exists %s" %
(interface['imac'], inic_dict,
interface_exists))
break
# If there are interfaces with clone labels as MAC addresses,
# this is a install-from-clone scenario. Update MAC addresses.
elif ((constants.CLONE_ISO_MAC + ihost['hostname'] + inic['pname']) ==
interface['imac']):
# Not checking for "interface['ifname'] == ifname",
# as it could be data0, bond0.100
updates = {'imac': inic['mac']}
self.dbapi.iinterface_update(interface['uuid'], updates)
LOG.info("clone_mac_update: updated if mac {} {} --> {}"
.format(ifname, interface['imac'], inic['mac']))
ports = self.dbapi.ethernet_port_get_by_interface(
interface['uuid'])
for p in ports:
# Update the corresponding ports too
LOG.debug("clone_mac_update: port={} mac={} for intf: {}"
.format(p['id'], p['mac'], interface['uuid']))
if constants.CLONE_ISO_MAC in p['mac']:
updates = {'mac': inic['mac']}
self.dbapi.ethernet_port_update(p['id'], updates)
LOG.info("clone_mac_update: updated port: {} {}-->{}"
.format(p['id'], p['mac'], inic['mac']))
# See if there are dependent interfaces.
# If yes, update them too.
self._update_dependent_interfaces(interface, ihost,
ifname, inic['mac'])
clone_mac_updated = True
if ((constants.CLONE_ISO_MAC + ihost['hostname'] + inic['pname'])
in ihost['mgmt_mac']):
LOG.info("clone_mac_update: mgmt_mac {}:{}"
.format(ihost['mgmt_mac'], inic['mac']))
values = {'mgmt_mac': inic['mac']}
self.dbapi.ihost_update(ihost['uuid'], values)
if clone_mac_updated:
# no need create any interfaces or ports for cloning scenario
cloning = True
continue
if not interface_exists:
interface_dict = {'forihostid': ihost['id'],
'ifname': ifname,
'imac': inic['mac'],
'imtu': mtu,
'iftype': 'ethernet',
'ifclass': ifclass,
}
# autocreate untagged interface
try:
LOG.debug("Attempting to create new interface %s" %
interface_dict)
new_interface = self.dbapi.iinterface_create(
ihost['id'],
interface_dict)
# append to port attributes as well
inic_dict.update(
{'interface_id': new_interface['id'],
'bootp': bootp
})
if networktype in [constants.NETWORK_TYPE_MGMT,
constants.NETWORK_TYPE_PXEBOOT]:
new_interface_networktype = networktype
network = self.dbapi.network_get_by_type(networktype)
# create interface network association
ifnet_dict = {
'interface_id': new_interface['id'],
'network_id': network['id']
}
try:
self.dbapi.interface_network_create(ifnet_dict)
except Exception:
LOG.exception(
"Failed to create interface %s "
"network %s association" %
(new_interface['id'], network['id']))
except Exception:
LOG.exception("Failed to create new interface %s" %
inic['mac'])
pass # at least create the port
if create_tagged_interface:
# autocreate tagged management interface
network = self.dbapi.network_get_by_type(constants.NETWORK_TYPE_MGMT)
interface_dict = {
'forihostid': ihost['id'],
'ifname': 'mgmt0',
'imac': inic['mac'],
'imtu': constants.DEFAULT_MTU,
'iftype': 'vlan',
'ifclass': constants.INTERFACE_CLASS_PLATFORM,
'uses': [ifname],
'vlan_id': vlan_id,
}
try:
LOG.debug("Attempting to create new interface %s" %
interface_dict)
new_interface = self.dbapi.iinterface_create(
ihost['id'], interface_dict
)
new_interface_networktype = constants.NETWORK_TYPE_MGMT
network = self.dbapi.network_get_by_type(
constants.NETWORK_TYPE_MGMT
)
# create interface network association
ifnet_dict = {
'interface_id': new_interface['id'],
'network_id': network['id']
}
try:
self.dbapi.interface_network_create(ifnet_dict)
except Exception:
LOG.exception(
"Failed to create interface %s "
"network %s association" %
(new_interface['id'], network['id']))
except Exception:
LOG.exception(
"Failed to create new vlan interface %s" %
inic['mac'])
pass # at least create the port
try:
LOG.debug("Attempting to create new port %s on host %s" %
(inic_dict, ihost['id']))
port = self.dbapi.ethernet_port_get_by_mac(inic['mac'])
# update existing port with updated attributes
try:
port_dict = {
'sriov_totalvfs': inic['sriov_totalvfs'],
'sriov_numvfs': inic['sriov_numvfs'],
'sriov_vfs_pci_address':
inic['sriov_vfs_pci_address'],
'sriov_vf_driver':
inic['sriov_vf_driver'],
'sriov_vf_pdevice_id':
inic['sriov_vf_pdevice_id'],
'driver': inic['driver'],
'dpdksupport': inic['dpdksupport'],
'speed': inic['speed'],
}
LOG.info("port %s update attr: %s" %
(port.uuid, port_dict))
self.dbapi.ethernet_port_update(port.uuid, port_dict)
# During WRL to CentOS upgrades the port name can
# change. This will update the db to reflect that
if port['name'] != inic['pname']:
self._update_port_name(port, inic['pname'])
except Exception:
LOG.exception("Failed to update port %s" % inic['mac'])
pass
except Exception:
# adjust for field naming differences between the NIC
# dictionary returned by the agent and the Port model
port_dict = inic_dict.copy()
port_dict['name'] = port_dict.pop('pname', None)
port_dict['namedisplay'] = port_dict.pop('pnamedisplay',
None)
LOG.info("Attempting to create new port %s "
"on host %s" % (inic_dict, ihost.uuid))
port = self.dbapi.ethernet_port_create(ihost.uuid, port_dict)
except exception.NodeNotFound:
raise exception.SysinvException(_(
"Invalid ihost_uuid: host not found: %s") %
ihost_uuid)
except Exception: # this info may have been posted previously, update ?
pass
# Set interface ID for management address
if set_address_interface:
if new_interface and 'id' in new_interface:
values = {'interface_id': new_interface['id']}
try:
addr_name = cutils.format_address_name(
ihost.hostname, new_interface_networktype)
address = self.dbapi.address_get_by_name(addr_name)
self.dbapi.address_update(address['uuid'], values)
except exception.AddressNotFoundByName:
pass
# Do any potential distributed cloud config
# We do this here where the interface is created.
cutils.perform_distributed_cloud_config(self.dbapi,
new_interface['id'])
if port:
values = {'interface_id': port.interface_id}
try:
addr_name = cutils.format_address_name(ihost.hostname,
networktype)
address = self.dbapi.address_get_by_name(addr_name)
if address['interface_id'] is None:
self.dbapi.address_update(address['uuid'], values)
except exception.AddressNotFoundByName:
pass
if ihost.invprovision not in [constants.PROVISIONED, constants.PROVISIONING]:
LOG.info("Updating %s host invprovision from %s to %s" %
(ihost.hostname, ihost.invprovision, constants.UNPROVISIONED))
value = {'invprovision': constants.UNPROVISIONED}
self.dbapi.ihost_update(ihost_uuid, value)
if cloning:
# if cloning scenario, check and log if there are lesser no:of interfaces
# on the host being installed with a cloned image. Comparison is against
# the DB which was backed up on the original system (used for cloning).
self.validate_cloned_interfaces(ihost_uuid)
def _update_port_name(self, port, updated_name):
"""
Sets the port name based on the updated name.
Will also set the ifname of any associated ethernet/vlan interfaces
We do not modify any AE interfaces. The names of AE interfaces should
not be related to any physical ports.
:param port: the db object of the port to update
:param updated_name: the new name
"""
port_name = port['name']
# Might need to update the associated interface and vlan names as well
interface = self.dbapi.iinterface_get(port['interface_id'])
if interface.ifname == port_name:
LOG.info("Updating interface name: %s to %s" %
(interface.ifname, updated_name))
self.dbapi.iinterface_update(interface.uuid,
{'ifname': updated_name})
used_by = interface['used_by']
for ifname in used_by:
vlan = self.dbapi.iinterface_get(ifname, port['forihostid'])
if vlan.get('iftype') != constants.INTERFACE_TYPE_VLAN:
continue
if vlan.ifname.startswith((port_name + ".")):
new_vlan_name = vlan.ifname.replace(
port_name, updated_name, 1)
LOG.info("Updating vlan interface name: %s to %s" %
(vlan.ifname, new_vlan_name))
self.dbapi.iinterface_update(vlan.uuid,
{'ifname': new_vlan_name})
LOG.info("Updating port name: %s to %s" % (port_name, updated_name))
self.dbapi.ethernet_port_update(port['uuid'], {'name': updated_name})
def lldp_id_to_port(self, id, ports):
ovs_id = re.sub(r'^{}'.format(constants.LLDP_OVS_PORT_PREFIX), '', id)
for port in ports:
if (port['name'] == id or
port['uuid'] == id or
port['uuid'].find(ovs_id) == 0):
return port
return None
def lldp_tlv_dict(self, agent_neighbour_dict):
tlv_dict = {}
for k, v in agent_neighbour_dict.items():
if v is not None and k in constants.LLDP_TLV_VALID_LIST:
tlv_dict.update({k: v})
return tlv_dict
def lldp_agent_tlv_update(self, tlv_dict, agent):
tlv_update_list = []
tlv_create_list = []
agent_id = agent['id']
agent_uuid = agent['uuid']
tlvs = self.dbapi.lldp_tlv_get_by_agent(agent_uuid)
for k, v in tlv_dict.items():
for tlv in tlvs:
if tlv['type'] == k:
tlv_value = tlv_dict.get(tlv['type'])
entry = {'type': tlv['type'],
'value': tlv_value}
if tlv['value'] != tlv_value:
tlv_update_list.append(entry)