config/sysinv/sysinv/sysinv/sysinv/api/controllers/v1/system.py

602 lines
24 KiB
Python

#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 Red Hat, 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.
#
# Copyright (c) 2013-2017 Wind River Systems, Inc.
#
from sqlalchemy.orm.exc import NoResultFound
import jsonpatch
import six
import os
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.api.controllers.v1 import link
from sysinv.api.controllers.v1 import host
from sysinv.api.controllers.v1 import types
from sysinv.api.controllers.v1 import utils as api_utils
from sysinv.api.controllers.v1 import controller_fs as controllerfs
from sysinv.common import constants
from sysinv.common import exception
from sysinv.common import utils as cutils
from sysinv import objects
from sysinv.openstack.common import log
from sysinv.openstack.common.gettextutils import _
LOG = log.getLogger(__name__)
class System(base.APIBase):
"""API representation of a system.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
a isystem.
"""
uuid = types.uuid
"The UUID of the isystem"
name = wtypes.text
"The name of the isystem"
system_type = wtypes.text
"The type of the isystem"
system_mode = wtypes.text
"The mode of the isystem"
description = wtypes.text
"The name of the isystem"
contact = wtypes.text
"The contact of the isystem"
location = wtypes.text
"The location of the isystem"
services = int
"The services of the isystem"
software_version = wtypes.text
"A textual description of the entity"
timezone = wtypes.text
"The timezone of the isystem"
links = [link.Link]
"A list containing a self link and associated isystem links"
ihosts = [link.Link]
"Links to the collection of ihosts contained in this isystem"
capabilities = {wtypes.text: api_utils.ValidTypes(wtypes.text, bool,
six.integer_types)}
"System defined capabilities"
region_name = wtypes.text
"The region name of the isystem"
distributed_cloud_role = wtypes.text
"The distributed cloud role of the isystem"
service_project_name = wtypes.text
"The service project name of the isystem"
security_feature = wtypes.text
"Kernel arguments associated with exnabled spectre/meltdown fix features"
def __init__(self, **kwargs):
self.fields = objects.system.fields.keys()
for k in self.fields:
# Translate any special internal representation of data to its
# customer facing form
if k == 'security_feature':
# look up which customer-facing-security-feature-string goes
# with the kernel arguments tracked in sysinv
kernel_args = kwargs.get(k)
translated_string = kernel_args
for user_string, args_string in \
constants.SYSTEM_SECURITY_FEATURE_SPECTRE_MELTDOWN_OPTS.iteritems():
if args_string == kernel_args:
translated_string = user_string
break
setattr(self, k, translated_string)
else:
# No translation required
setattr(self, k, kwargs.get(k))
@classmethod
def convert_with_links(cls, rpc_isystem, expand=True):
# isystem = isystem(**rpc_isystem.as_dict())
minimum_fields = ['id', 'uuid', 'name', 'system_type', 'system_mode',
'description', 'capabilities',
'contact', 'location', 'software_version',
'created_at', 'updated_at', 'timezone',
'region_name', 'service_project_name',
'distributed_cloud_role', 'security_feature']
fields = minimum_fields if not expand else None
iSystem = System.from_rpc_object(rpc_isystem, fields)
iSystem.links = [link.Link.make_link('self', pecan.request.host_url,
'isystems', iSystem.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'isystems', iSystem.uuid,
bookmark=True)
]
if expand:
iSystem.ihosts = [link.Link.make_link('self',
pecan.request.host_url,
'isystems',
iSystem.uuid + "/ihosts"),
link.Link.make_link(
'bookmark',
pecan.request.host_url,
'isystems',
iSystem.uuid + "/ihosts",
bookmark=True)
]
return iSystem
class SystemCollection(collection.Collection):
"""API representation of a collection of isystems."""
isystems = [System]
"A list containing isystem objects"
def __init__(self, **kwargs):
self._type = 'isystems'
@classmethod
def convert_with_links(cls, isystems, limit, url=None,
expand=False, **kwargs):
collection = SystemCollection()
collection.isystems = [System.convert_with_links(ch, expand)
for ch in isystems]
# url = url or None
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
LOCK_NAME = 'SystemController'
class SystemController(rest.RestController):
"""REST controller for isystem."""
ihosts = host.HostController(from_isystem=True)
"Expose ihosts as a sub-element of isystem"
controller_fs = controllerfs.ControllerFsController()
"Expose controller_fs as a sub-element of isystem"
_custom_actions = {
'detail': ['GET'],
'mgmtvlan': ['GET'],
}
def __init__(self):
self._bm_region = None
def bm_region_get(self):
if not self._bm_region:
networks = pecan.request.dbapi.networks_get_by_type(
constants.NETWORK_TYPE_BM)
if networks:
self._bm_region = constants.REGION_PRIMARY
else:
networks = pecan.request.dbapi.networks_get_by_type(
constants.NETWORK_TYPE_MGMT)
# During initial system install no networks assigned yet
if networks:
self._bm_region = constants.REGION_SECONDARY
return self._bm_region
def _get_updates(self, patch):
"""Retrieve the updated attributes from the patch request."""
updates = {}
for p in patch:
attribute = p['path'] if p['path'][0] != '/' else p['path'][1:]
updates[attribute] = p['value']
return updates
def _verify_sdn_disabled(self):
# Check if SDN controller is configured
sdn_controllers = pecan.request.dbapi.sdn_controller_get_list()
if sdn_controllers:
msg = _("SDN cannot be disabled when SDN controller is "
"configured.")
raise wsme.exc.ClientSideError(msg)
# Check if SDN Controller service parameters
neutron_parameters = []
for section in [constants.SERVICE_PARAM_SECTION_NETWORK_ML2,
constants.SERVICE_PARAM_SECTION_NETWORK_ML2_ODL,
constants.SERVICE_PARAM_SECTION_NETWORK_DEFAULT]:
try:
parm_list = pecan.request.dbapi.service_parameter_get_all(
service=constants.SERVICE_TYPE_NETWORK,
section=section)
neutron_parameters = neutron_parameters + parm_list
except NoResultFound:
continue
if neutron_parameters:
msg = _("SDN cannot be disabled when SDN service parameters "
"are configured.")
raise wsme.exc.ClientSideError(msg)
def _verify_sdn_enabled(self):
# If SDN is enabled then OAM and Management network
# must belong to the same Address Family
oam_network = pecan.request.dbapi.network_get_by_type(
constants.NETWORK_TYPE_OAM)
oam_address_pool = pecan.request.dbapi.address_pool_get(
oam_network.pool_uuid)
oam_ip_version = oam_address_pool.family
mgmt_network = pecan.request.dbapi.network_get_by_type(
constants.NETWORK_TYPE_MGMT)
mgmt_address_pool = pecan.request.dbapi.address_pool_get(
mgmt_network.pool_uuid)
mgmt_ip_version = mgmt_address_pool.family
if oam_ip_version != mgmt_ip_version:
msg = _("Invalid network address - OAM and Management Network IP"
" Families must be the same when SDN is enabled.")
raise wsme.exc.ClientSideError(msg)
def _check_hosts(self):
hosts = pecan.request.dbapi.ihost_get_list()
for h in hosts:
if api_utils.is_aio_simplex_host_unlocked(h):
raise wsme.exc.ClientSideError(
_("Host {} must be locked.".format(h['hostname'])))
elif (h['administrative'] != constants.ADMIN_LOCKED and
constants.COMPUTE in h['subfunctions'] and
not api_utils.is_host_active_controller(h) and
not api_utils.is_host_simplex_controller(h)):
raise wsme.exc.ClientSideError(
_("Host {} must be locked.".format(h['hostname'])))
def _get_isystem_collection(self, marker, limit, sort_key, sort_dir,
expand=False, resource_url=None):
limit = api_utils.validate_limit(limit)
sort_dir = api_utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.system.get_by_uuid(pecan.request.context,
marker)
isystem = pecan.request.dbapi.isystem_get_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
for i in isystem:
i.capabilities['bm_region'] = self.bm_region_get()
return SystemCollection.convert_with_links(isystem, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(SystemCollection, types.uuid,
int, wtypes.text, wtypes.text)
def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of isystems.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
return self._get_isystem_collection(marker, limit, sort_key, sort_dir)
@wsme_pecan.wsexpose(SystemCollection, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'):
"""Retrieve a list of isystem with detail.
:param marker: pagination marker for large data sets.
:param limit: maximum number of resources to return in a single result.
:param sort_key: column to sort results by. Default: id.
:param sort_dir: direction to sort. "asc" or "desc". Default: asc.
"""
# /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "isystem":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['isystem', 'detail'])
return self._get_isystem_collection(marker, limit, sort_key, sort_dir,
expand, resource_url)
@wsme_pecan.wsexpose(System, types.uuid)
def get_one(self, isystem_uuid):
"""Retrieve information about the given isystem.
:param isystem_uuid: UUID of a isystem.
"""
rpc_isystem = objects.system.get_by_uuid(pecan.request.context,
isystem_uuid)
rpc_isystem.capabilities['bm_region'] = self.bm_region_get()
return System.convert_with_links(rpc_isystem)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(System, body=System)
def post(self, isystem):
"""Create a new system."""
raise exception.OperationNotPermitted
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(System, types.uuid, body=[six.text_type])
def patch(self, isystem_uuid, patch):
"""Update an existing isystem.
:param isystem_uuid: UUID of a isystem.
:param patch: a json PATCH document to apply to this isystem.
"""
rpc_isystem = objects.system.get_by_uuid(pecan.request.context,
isystem_uuid)
system_dict = rpc_isystem.as_dict()
updates = self._get_updates(patch)
change_https = False
change_sdn = False
change_dc_role = False
vswitch_type = None
# prevent description field from being updated
for p in jsonpatch.JsonPatch(patch):
if p['path'] == '/software_version':
raise wsme.exc.ClientSideError(_("software_version field "
"cannot be modified."))
if p['path'] == '/system_type':
if rpc_isystem is not None:
if rpc_isystem.system_type is not None:
raise wsme.exc.ClientSideError(_("system_type field "
"cannot be "
"modified."))
if (p['path'] == '/system_mode' and p.get('value') !=
rpc_isystem.system_mode):
if rpc_isystem is not None and \
rpc_isystem.system_mode is not None:
if rpc_isystem.system_type != constants.TIS_AIO_BUILD:
raise wsme.exc.ClientSideError(
"system_mode can only be modified on an "
"AIO system")
system_mode_options = [constants.SYSTEM_MODE_DUPLEX,
constants.SYSTEM_MODE_DUPLEX_DIRECT]
new_system_mode = p['value']
if rpc_isystem.system_mode == \
constants.SYSTEM_MODE_SIMPLEX:
msg = _("Cannot modify system mode when it is "
"already set to %s." % rpc_isystem.system_mode)
raise wsme.exc.ClientSideError(msg)
elif new_system_mode == constants.SYSTEM_MODE_SIMPLEX:
msg = _("Cannot modify system mode to simplex when "
"it is set to %s " % rpc_isystem.system_mode)
raise wsme.exc.ClientSideError(msg)
if new_system_mode not in system_mode_options:
raise wsme.exc.ClientSideError(
"Invalid value for system_mode, it can only"
" be modified to '%s' or '%s'" %
(constants.SYSTEM_MODE_DUPLEX,
constants.SYSTEM_MODE_DUPLEX_DIRECT))
if p['path'] == '/timezone':
timezone = p['value']
if not os.path.isfile("/usr/share/zoneinfo/%s" % timezone):
raise wsme.exc.ClientSideError(_("Timezone file %s "
"does not exist." %
timezone))
if p['path'] == '/sdn_enabled':
sdn_enabled = p['value']
patch.remove(p)
if p['path'] == '/https_enabled':
https_enabled = p['value']
patch.remove(p)
if p['path'] == '/distributed_cloud_role':
distributed_cloud_role = p['value']
patch.remove(p)
if p['path'] == '/vswitch_type':
vswitch_type = p['value']
patch.remove(p)
if p['path'] == '/security_feature':
security_feature = p['value']
patch.remove(p)
try:
patched_system = jsonpatch.apply_patch(system_dict,
jsonpatch.JsonPatch(patch))
except api_utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
if 'sdn_enabled' in updates:
if sdn_enabled != rpc_isystem['capabilities']['sdn_enabled']:
self._check_hosts()
change_sdn = True
if sdn_enabled == 'true':
self._verify_sdn_enabled()
patched_system['capabilities']['sdn_enabled'] = True
else:
self._verify_sdn_disabled()
patched_system['capabilities']['sdn_enabled'] = False
if 'https_enabled' in updates:
if https_enabled != rpc_isystem['capabilities']['https_enabled']:
change_https = True
if https_enabled == 'true':
patched_system['capabilities']['https_enabled'] = True
else:
patched_system['capabilities']['https_enabled'] = False
else:
raise wsme.exc.ClientSideError(_("https_enabled is already set"
" as %s" % https_enabled))
if 'distributed_cloud_role' in updates:
# At this point dc role cannot be changed after config_controller
# and config_subcloud
if rpc_isystem['distributed_cloud_role'] is None and \
distributed_cloud_role in \
[constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER,
constants.DISTRIBUTED_CLOUD_ROLE_SUBCLOUD]:
change_dc_role = True
patched_system['distributed_cloud_role'] = distributed_cloud_role
else:
raise wsme.exc.ClientSideError(_("distributed_cloud_role is already set "
" as %s" % rpc_isystem['distributed_cloud_role']))
if 'vswitch_type' in updates:
if vswitch_type == rpc_isystem['capabilities']['vswitch_type']:
raise wsme.exc.ClientSideError(_("vswitch_type is already set"
" as %s" % vswitch_type))
patched_system['capabilities']['vswitch_type'] = vswitch_type
if 'security_feature' in updates:
# Security feature string must be translated from user values to
# kernel options
if (security_feature in
constants.SYSTEM_SECURITY_FEATURE_SPECTRE_MELTDOWN_OPTS):
security_feature_value = \
constants.SYSTEM_SECURITY_FEATURE_SPECTRE_MELTDOWN_OPTS[security_feature]
patched_system['security_feature'] = security_feature_value
else:
raise wsme.exc.ClientSideError(_("Unexpected value %s specified for "
"security_feature" % security_feature))
# Update only the fields that have changed
name = ""
contact = ""
location = ""
system_mode = ""
timezone = ""
capabilities = {}
distributed_cloud_role = ""
security_feature = ""
for field in objects.system.fields:
if rpc_isystem[field] != patched_system[field]:
rpc_isystem[field] = patched_system[field]
if field == 'name':
name = rpc_isystem[field]
if field == 'contact':
contact = rpc_isystem[field]
if field == 'location':
location = rpc_isystem[field]
if field == 'system_mode':
system_mode = rpc_isystem[field]
if field == 'timezone':
timezone = rpc_isystem[field]
if field == 'capabilities':
capabilities = rpc_isystem[field]
if field == 'distributed_cloud_role':
distributed_cloud_role = rpc_isystem[field]
if field == 'security_feature':
security_feature = rpc_isystem[field]
delta = rpc_isystem.obj_what_changed()
delta_handle = list(delta)
rpc_isystem.save()
if name:
LOG.info("update system name")
pecan.request.rpcapi.configure_isystemname(pecan.request.context,
name)
if name or location or contact:
LOG.info("update SNMP config")
pecan.request.rpcapi.update_snmp_config(pecan.request.context)
if 'system_mode' in delta_handle:
LOG.info("update system mode %s" % system_mode)
pecan.request.rpcapi.update_system_mode_config(
pecan.request.context)
if timezone:
LOG.info("update system timezone to %s" % timezone)
pecan.request.rpcapi.configure_system_timezone(
pecan.request.context)
if capabilities:
if change_sdn:
LOG.info("update sdn to %s" % capabilities)
pecan.request.rpcapi.update_sdn_enabled(pecan.request.context)
if change_https:
LOG.info("update https to %s" % capabilities)
pecan.request.rpcapi.configure_system_https(
pecan.request.context)
if vswitch_type:
LOG.info("update vswitch_type to %s" % capabilities)
pecan.request.rpcapi.update_vswitch_type(
pecan.request.context)
if distributed_cloud_role and change_dc_role:
LOG.info("update distributed cloud role to %s" % distributed_cloud_role)
pecan.request.rpcapi.update_distributed_cloud_role(
pecan.request.context)
if 'security_feature' in delta_handle:
LOG.info("update security_feature %s" % security_feature)
pecan.request.rpcapi.update_security_feature_config(
pecan.request.context)
return System.convert_with_links(rpc_isystem)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, isystem_uuid):
"""Delete a isystem.
:param isystem_uuid: UUID of a isystem.
"""
raise exception.OperationNotPermitted
@wsme_pecan.wsexpose(int)
def mgmtvlan(self):
local_hostname = cutils.get_local_controller_hostname()
controller = pecan.request.dbapi.ihost_get(local_hostname)
host_id = controller['id']
interface_list = pecan.request.dbapi.iinterface_get_by_ihost(host_id)
for interface in interface_list:
for network_id in interface['networks']:
network = pecan.request.dbapi.network_get_by_id(network_id)
if network.type == constants.NETWORK_TYPE_MGMT:
if 'vlan_id' not in interface:
return 0
else:
return interface['vlan_id']
return None