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

396 lines
14 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright 2013 UnitedStack 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.
#
import jsonpatch
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 types
from sysinv.api.controllers.v1 import utils
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.gettextutils import _
from sysinv.openstack.common import log
from netaddr import IPAddress
from netaddr import AddrFormatError
LOG = log.getLogger(__name__)
class NTPPatchType(types.JsonPatchType):
@staticmethod
def mandatory_attrs():
return ['/ntpservers']
class NTP(base.APIBase):
"""API representation of NTP configuration.
This class enforces type checking and value constraints, and converts
between the internal object model and the API representation of
an ntp.
"""
uuid = types.uuid
"Unique UUID for this ntp"
enabled = types.boolean
"Represent the status of the intp."
ntpservers = wtypes.text
"Represent the ntpservers of the intp. csv list."
action = wtypes.text
"Represent the action on the intp."
forisystemid = int
"The isystemid that this intp belongs to"
isystem_uuid = types.uuid
"The UUID of the system this ntp belongs to"
links = [link.Link]
"A list containing a self link and associated ntp links"
created_at = wtypes.datetime.datetime
updated_at = wtypes.datetime.datetime
def __init__(self, **kwargs):
self.fields = list(objects.ntp.fields.keys())
for k in self.fields:
setattr(self, k, kwargs.get(k))
# 'action' is not part of objects.intp.fields
# (it's an API-only attribute)
self.fields.append('action')
setattr(self, 'action', kwargs.get('action', None))
@classmethod
def convert_with_links(cls, rpc_ntp, expand=True):
# fields = ['uuid', 'address'] if not expand else None
# ntp = intp.from_rpc_object(rpc_ntp, fields)
ntp = NTP(**rpc_ntp.as_dict())
if not expand:
ntp.unset_fields_except(['uuid',
'enabled',
'ntpservers',
'isystem_uuid',
'created_at',
'updated_at'])
# never expose the isystem_id attribute
ntp.isystem_id = wtypes.Unset
# never expose the isystem_id attribute, allow exposure for now
# ntp.forisystemid = wtypes.Unset
ntp.links = [link.Link.make_link('self', pecan.request.host_url,
'intps', ntp.uuid),
link.Link.make_link('bookmark',
pecan.request.host_url,
'intps', ntp.uuid,
bookmark=True)
]
return ntp
class intpCollection(collection.Collection):
"""API representation of a collection of ntps."""
intps = [NTP]
"A list containing ntp objects"
def __init__(self, **kwargs):
self._type = 'intps'
@classmethod
def convert_with_links(cls, rpc_ntps, limit, url=None,
expand=False, **kwargs):
collection = intpCollection()
collection.intps = [NTP.convert_with_links(p, expand)
for p in rpc_ntps]
collection.next = collection.get_next(limit, url=url, **kwargs)
return collection
##############
# UTILS
##############
def _check_ntp_data(op, ntp):
# Get data
enabled = ntp['enabled']
ntpservers = ntp['ntpservers']
intp_ntpservers_list = []
ntp_ntpservers = ""
idns_nameservers_list = []
MAX_S = 3
ptp_list = pecan.request.dbapi.ptp_get_by_isystem(ntp['forisystemid'])
if ptp_list:
if hasattr(ptp_list[0], 'enabled'):
if ptp_list[0].enabled is True and enabled is True:
raise wsme.exc.ClientSideError(_(
"NTP cannot be configured alongside with PTP."
" Please disable PTP before enabling NTP."))
dns_list = pecan.request.dbapi.idns_get_by_isystem(ntp['forisystemid'])
if dns_list:
if hasattr(dns_list[0], 'nameservers'):
if dns_list[0].nameservers:
idns_nameservers_list = dns_list[0].nameservers.split(',')
if ntpservers:
for ntpserver in [n.strip() for n in ntpservers.split(',')]:
# Semantic check each server as IP
try:
intp_ntpservers_list.append(str(IPAddress(ntpserver)))
except (AddrFormatError, ValueError):
if utils.is_valid_hostname(ntpserver):
# If server address in FQDN, and no DNS servers, raise error
if len(idns_nameservers_list) == 0 and ntpserver != 'NC':
raise wsme.exc.ClientSideError(_(
"A DNS server must be configured prior to "
"configuring any NTP server address as FQDN. "
"Alternatively, specify the NTP server as an IP"
" address"))
else:
if ntpserver == 'NC':
intp_ntpservers_list.append(str(""))
else:
intp_ntpservers_list.append(str(ntpserver))
else:
raise wsme.exc.ClientSideError(_(
"Invalid NTP server %s "
"Please configure a valid NTP "
"IP address or hostname.") % (ntpserver))
if len(intp_ntpservers_list) == 0 and enabled is None:
raise wsme.exc.ClientSideError(_("No NTP parameters provided."))
if len(intp_ntpservers_list) > MAX_S:
raise wsme.exc.ClientSideError(_(
"Maximum NTP servers supported: %s but provided: %s. "
"Please configure a valid list of NTP servers."
% (MAX_S, len(intp_ntpservers_list))))
ntp_ntpservers = ",".join(intp_ntpservers_list)
ntp['ntpservers'] = ntp_ntpservers
return ntp
LOCK_NAME = 'NTPController'
class NTPController(rest.RestController):
"""REST controller for intps."""
_custom_actions = {
'detail': ['GET'],
}
def __init__(self, from_isystems=False):
self._from_isystems = from_isystems
def _get_ntps_collection(self, isystem_uuid, marker, limit, sort_key,
sort_dir, expand=False, resource_url=None):
if self._from_isystems and not isystem_uuid:
raise exception.InvalidParameterValue(_(
"System id not specified."))
limit = utils.validate_limit(limit)
sort_dir = utils.validate_sort_dir(sort_dir)
marker_obj = None
if marker:
marker_obj = objects.ntp.get_by_uuid(pecan.request.context,
marker)
if isystem_uuid:
ntps = pecan.request.dbapi.intp_get_by_isystem(
isystem_uuid, limit,
marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
else:
ntps = pecan.request.dbapi.intp_get_list(limit, marker_obj,
sort_key=sort_key,
sort_dir=sort_dir)
return intpCollection.convert_with_links(ntps, limit,
url=resource_url,
expand=expand,
sort_key=sort_key,
sort_dir=sort_dir)
@wsme_pecan.wsexpose(intpCollection, types.uuid, types.uuid, int,
wtypes.text, wtypes.text)
def get_all(self, isystem_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of ntps. Only one per system"""
return self._get_ntps_collection(isystem_uuid, marker, limit,
sort_key, sort_dir)
@wsme_pecan.wsexpose(intpCollection, types.uuid, types.uuid, int,
wtypes.text, wtypes.text)
def detail(self, isystem_uuid=None, marker=None, limit=None,
sort_key='id', sort_dir='asc'):
"""Retrieve a list of ntps with detail."""
# NOTE(lucasagomes): /detail should only work agaist collections
parent = pecan.request.path.split('/')[:-1][-1]
if parent != "intps":
raise exception.HTTPNotFound
expand = True
resource_url = '/'.join(['ntps', 'detail'])
return self._get_ntps_collection(isystem_uuid,
marker, limit,
sort_key, sort_dir,
expand, resource_url)
@wsme_pecan.wsexpose(NTP, types.uuid)
def get_one(self, ntp_uuid):
"""Retrieve information about the given ntp."""
if self._from_isystems:
raise exception.OperationNotPermitted
rpc_ntp = objects.ntp.get_by_uuid(pecan.request.context, ntp_uuid)
return NTP.convert_with_links(rpc_ntp)
@wsme_pecan.wsexpose(NTP, body=NTP)
def post(self, ntp):
"""Create a new ntp."""
raise exception.OperationNotPermitted
@cutils.synchronized(LOCK_NAME)
@wsme.validate(types.uuid, [NTPPatchType])
@wsme_pecan.wsexpose(NTP, types.uuid,
body=[NTPPatchType])
def patch(self, ntp_uuid, patch):
"""Update the current NTP configuration."""
if self._from_isystems:
raise exception.OperationNotPermitted
rpc_ntp = objects.ntp.get_by_uuid(pecan.request.context, ntp_uuid)
action = None
for p in patch:
if '/action' in p['path']:
value = p['value']
patch.remove(p)
if value in (constants.APPLY_ACTION, constants.INSTALL_ACTION):
action = value
break
# replace isystem_uuid and intp_uuid with corresponding
patch_obj = jsonpatch.JsonPatch(patch)
state_rel_path = ['/uuid', '/id', 'forisystemid', 'isystem_uuid']
if any(p['path'] in state_rel_path for p in patch_obj):
raise wsme.exc.ClientSideError(_("The following fields can not be "
"modified: %s" %
state_rel_path))
for p in patch_obj:
if p['path'] == '/isystem_uuid':
isystem = objects.system.get_by_uuid(pecan.request.context,
p['value'])
p['path'] = '/forisystemid'
p['value'] = isystem.id
try:
# Keep an original copy of the ntp data
ntp_orig = rpc_ntp.as_dict()
ntp = NTP(**jsonpatch.apply_patch(rpc_ntp.as_dict(),
patch_obj))
except utils.JSONPATCH_EXCEPTIONS as e:
raise exception.PatchError(patch=patch, reason=e)
LOG.warn("ntp %s" % ntp.as_dict())
ntp = _check_ntp_data("modify", ntp.as_dict())
try:
# Update only the fields that have changed
for field in objects.ntp.fields:
if rpc_ntp[field] != ntp[field]:
rpc_ntp[field] = ntp[field]
delta = rpc_ntp.obj_what_changed()
delta_handle = list(delta)
if delta:
rpc_ntp.save()
if 'enabled' in delta_handle:
service_change = True
else:
service_change = False
if action == constants.APPLY_ACTION:
# perform rpc to conductor to perform config apply
pecan.request.rpcapi.update_ntp_config(pecan.request.context,
service_change)
else:
LOG.info("No NTP config changes")
return NTP.convert_with_links(rpc_ntp)
except Exception as e:
# rollback database changes
for field in ntp_orig:
if rpc_ntp[field] != ntp_orig[field]:
rpc_ntp[field] = ntp_orig[field]
rpc_ntp.save()
msg = _("Failed to update the NTP configuration")
if e == exception.HTTPNotFound:
msg = _("NTP update failed: system %s if %s : patch %s"
% (isystem['systemname'], ntp['ifname'], patch))
raise wsme.exc.ClientSideError(msg)
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
def delete(self, ntp_uuid):
"""Delete a ntp."""
raise exception.OperationNotPermitted