477 lines
16 KiB
Python
477 lines
16 KiB
Python
# Copyright 2013 Mirantis, Inc.
|
|
#
|
|
# 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.
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import json
|
|
import types
|
|
|
|
import web
|
|
|
|
from nailgun.db import orm
|
|
from nailgun.logger import logger
|
|
from nailgun.settings import settings
|
|
from nailgun.api.models import Release
|
|
from nailgun.api.models import Cluster
|
|
from nailgun.api.models import ClusterChanges
|
|
from nailgun.api.models import Attributes
|
|
from nailgun.api.models import Node
|
|
from nailgun.api.models import NetworkGroup
|
|
from nailgun.api.models import Network
|
|
from nailgun.api.models import Notification
|
|
from nailgun.volumes.manager import VolumeManager
|
|
from netaddr import IPNetwork, AddrFormatError
|
|
|
|
|
|
class BasicValidator(object):
|
|
@classmethod
|
|
def validate_json(cls, data):
|
|
if data:
|
|
try:
|
|
res = json.loads(data)
|
|
except:
|
|
raise web.webapi.badrequest(
|
|
message="Invalid json format"
|
|
)
|
|
else:
|
|
raise web.webapi.badrequest(
|
|
message="Empty request received"
|
|
)
|
|
return res
|
|
|
|
@classmethod
|
|
def validate(cls, data):
|
|
raise NotImplementedError("You should override this method")
|
|
|
|
|
|
class MetaInterfacesValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, interfaces):
|
|
if not isinstance(interfaces, list):
|
|
raise web.webapi.badrequest(
|
|
message="Meta.interfaces should be list"
|
|
)
|
|
for nic in interfaces:
|
|
for key in ('mac', 'name'):
|
|
if key in nic and isinstance(nic[key], basestring) and\
|
|
nic[key]:
|
|
continue
|
|
raise web.webapi.badrequest(
|
|
message="Interface in meta.interfaces should have"
|
|
" key %r with nonempty string value" % key
|
|
)
|
|
for key in ('max_speed', 'current_speed'):
|
|
if key not in nic or isinstance(nic[key], types.NoneType) or\
|
|
(isinstance(nic[key], int) and nic[key] >= 0):
|
|
continue
|
|
raise web.webapi.badrequest(
|
|
message="Interface in meta.interfaces should have key %r"
|
|
" with positive integer or Null value" % key
|
|
)
|
|
|
|
|
|
class MetaValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, meta):
|
|
if not isinstance(meta, dict):
|
|
raise web.webapi.badrequest(message="Meta should be dict")
|
|
if 'interfaces' in meta:
|
|
MetaInterfacesValidator.validate(meta['interfaces'])
|
|
|
|
|
|
class ReleaseValidator(BasicValidator):
|
|
|
|
@classmethod
|
|
def validate(cls, data):
|
|
d = cls.validate_json(data)
|
|
if not "name" in d:
|
|
raise web.webapi.badrequest(
|
|
message="No release name specified"
|
|
)
|
|
if not "version" in d:
|
|
raise web.webapi.badrequest(
|
|
message="No release version specified"
|
|
)
|
|
if orm().query(Release).filter_by(
|
|
name=d["name"],
|
|
version=d["version"]
|
|
).first():
|
|
raise web.webapi.conflict
|
|
if "networks_metadata" in d:
|
|
for network in d["networks_metadata"]:
|
|
if not "name" in network or not "access" in network:
|
|
raise web.webapi.badrequest(
|
|
message="Invalid network data: %s" % str(network)
|
|
)
|
|
if network["access"] not in settings.NETWORK_POOLS:
|
|
raise web.webapi.badrequest(
|
|
message="Invalid access mode for network"
|
|
)
|
|
else:
|
|
d["networks_metadata"] = []
|
|
if not "attributes_metadata" in d:
|
|
d["attributes_metadata"] = {}
|
|
else:
|
|
try:
|
|
Attributes.validate_fixture(d["attributes_metadata"])
|
|
except:
|
|
raise web.webapi.badrequest(
|
|
message="Invalid logical structure of attributes metadata"
|
|
)
|
|
return d
|
|
|
|
|
|
class ClusterValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, data):
|
|
d = cls.validate_json(data)
|
|
if d.get("name"):
|
|
if orm().query(Cluster).filter_by(
|
|
name=d["name"]
|
|
).first():
|
|
c = web.webapi.conflict
|
|
c.message = "Environment with this name already exists"
|
|
raise c()
|
|
if d.get("release"):
|
|
release = orm().query(Release).get(d.get("release"))
|
|
if not release:
|
|
raise web.webapi.badrequest(message="Invalid release id")
|
|
return d
|
|
|
|
|
|
class AttributesValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, data):
|
|
d = cls.validate_json(data)
|
|
if "generated" in d:
|
|
raise web.webapi.badrequest(
|
|
message="It is not allowed to update generated attributes"
|
|
)
|
|
if "editable" in d and not isinstance(d["editable"], dict):
|
|
raise web.webapi.badrequest(
|
|
message="Editable attributes should be a dictionary"
|
|
)
|
|
return d
|
|
|
|
@classmethod
|
|
def validate_fixture(cls, data):
|
|
"""
|
|
Here we just want to be sure that data is logically valid.
|
|
We try to generate "generated" parameters. If there will not
|
|
be any error during generating then we assume data is
|
|
logically valid.
|
|
"""
|
|
d = cls.validate_json(data)
|
|
if "generated" in d:
|
|
cls.traverse(d["generated"])
|
|
|
|
|
|
class NodeValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, data):
|
|
d = cls.validate_json(data)
|
|
if not d:
|
|
raise web.webapi.badrequest(
|
|
message="No valid data received"
|
|
)
|
|
if not "mac" in d:
|
|
raise web.webapi.badrequest(
|
|
message="No mac address specified"
|
|
)
|
|
else:
|
|
q = orm().query(Node)
|
|
if q.filter(Node.mac == d["mac"]).first():
|
|
raise web.webapi.conflict()
|
|
if cls.validate_existent_node_mac(d):
|
|
raise web.webapi.conflict()
|
|
if "id" in d:
|
|
raise web.webapi.badrequest(
|
|
message="Manual ID setting is prohibited"
|
|
)
|
|
if 'meta' in d:
|
|
MetaValidator.validate(d['meta'])
|
|
return d
|
|
|
|
@classmethod
|
|
def validate_existent_node_mac(cls, data):
|
|
if 'meta' in data:
|
|
MetaValidator.validate(data['meta'])
|
|
if 'interfaces' in data['meta']:
|
|
existent_node = orm().query(Node).filter(Node.mac.in_(
|
|
[n['mac'] for n in data['meta']['interfaces']])).first()
|
|
return existent_node
|
|
|
|
@classmethod
|
|
def validate_update(cls, data):
|
|
d = cls.validate_json(data)
|
|
if not d:
|
|
raise web.webapi.badrequest(
|
|
message="No valid data received"
|
|
)
|
|
if "status" in d and d["status"] not in Node.NODE_STATUSES:
|
|
raise web.webapi.badrequest(
|
|
message="Invalid status for node"
|
|
)
|
|
if "id" in d:
|
|
raise web.webapi.badrequest(
|
|
message="Manual ID setting is prohibited"
|
|
)
|
|
if 'meta' in d:
|
|
MetaValidator.validate(d['meta'])
|
|
return d
|
|
|
|
@classmethod
|
|
def validate_collection_update(cls, data):
|
|
d = cls.validate_json(data)
|
|
if not isinstance(d, list):
|
|
raise web.badrequest(
|
|
"Invalid json list"
|
|
)
|
|
|
|
q = orm().query(Node)
|
|
for nd in d:
|
|
if not "mac" in nd and not "id" in nd:
|
|
raise web.badrequest(
|
|
"MAC or ID is not specified"
|
|
)
|
|
else:
|
|
if "mac" in nd:
|
|
existent_node = q.filter_by(mac=nd["mac"]).first() \
|
|
or cls.validate_existent_node_mac(nd)
|
|
if not existent_node:
|
|
raise web.badrequest(
|
|
"Invalid MAC specified"
|
|
)
|
|
if "id" in nd and not q.get(nd["id"]):
|
|
raise web.badrequest(
|
|
"Invalid ID specified"
|
|
)
|
|
if 'meta' in nd:
|
|
MetaValidator.validate(nd['meta'])
|
|
return d
|
|
|
|
|
|
class NodeAttributesValidator(BasicValidator):
|
|
pass
|
|
|
|
|
|
class NodeVolumesValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, data):
|
|
# Here we instantiate VolumeManager with data
|
|
# and during initialization it validates volumes.
|
|
# So we can get validated volumes just after
|
|
# VolumeManager initialization
|
|
vm = VolumeManager(data=data)
|
|
return vm.volumes
|
|
|
|
|
|
class NotificationValidator(BasicValidator):
|
|
@classmethod
|
|
def validate_update(cls, data):
|
|
|
|
valid = {}
|
|
d = cls.validate_json(data)
|
|
|
|
status = d.get("status", None)
|
|
if status in Notification.NOTIFICATION_STATUSES:
|
|
valid["status"] = status
|
|
else:
|
|
raise web.webapi.badrequest("Bad status")
|
|
|
|
return valid
|
|
|
|
@classmethod
|
|
def validate_collection_update(cls, data):
|
|
d = cls.validate_json(data)
|
|
if not isinstance(d, list):
|
|
raise web.badrequest(
|
|
"Invalid json list"
|
|
)
|
|
|
|
q = orm().query(Notification)
|
|
valid_d = []
|
|
for nd in d:
|
|
valid_nd = {}
|
|
if "id" not in nd:
|
|
raise web.badrequest("ID is not set correctly")
|
|
|
|
if "status" not in nd:
|
|
raise web.badrequest("ID is not set correctly")
|
|
|
|
if not q.get(nd["id"]):
|
|
raise web.badrequest("Invalid ID specified")
|
|
|
|
valid_nd["id"] = nd["id"]
|
|
valid_nd["status"] = nd["status"]
|
|
valid_d.append(valid_nd)
|
|
return valid_d
|
|
|
|
|
|
class NetworkConfigurationValidator(BasicValidator):
|
|
@classmethod
|
|
def validate_networks_update(cls, data):
|
|
d = cls.validate_json(data)
|
|
networks = d['networks']
|
|
|
|
if not d:
|
|
raise web.webapi.badrequest(
|
|
message="No valid data received"
|
|
)
|
|
if not isinstance(networks, list):
|
|
raise web.webapi.badrequest(
|
|
message="It's expected to receive array, not a single object"
|
|
)
|
|
for i in networks:
|
|
if not 'id' in i:
|
|
raise web.webapi.badrequest(
|
|
message="No 'id' param for '{0}'".format(i)
|
|
)
|
|
|
|
if i.get('name') == 'public':
|
|
try:
|
|
IPNetwork('0.0.0.0/' + i['netmask'])
|
|
except (AddrFormatError, KeyError):
|
|
raise web.webapi.badrequest(
|
|
message="Invalid netmask for public network")
|
|
return d
|
|
|
|
|
|
class NetAssignmentValidator(BasicValidator):
|
|
@classmethod
|
|
def validate(cls, node):
|
|
if not isinstance(node, dict):
|
|
raise web.webapi.badrequest(message="Each node should be dict")
|
|
if 'id' not in node:
|
|
raise web.webapi.badrequest(message="Each node should have ID")
|
|
if 'interfaces' not in node or \
|
|
not isinstance(node['interfaces'], list):
|
|
raise web.webapi.badrequest(
|
|
message="There is no 'interfaces' list in node '%d'" %
|
|
node['id']
|
|
)
|
|
|
|
net_ids = set()
|
|
for iface in node['interfaces']:
|
|
if not isinstance(iface, dict):
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d': each interface should be dict" %
|
|
node['id']
|
|
)
|
|
if 'id' not in iface:
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d': each interface should have ID" %
|
|
node['id']
|
|
)
|
|
if 'assigned_networks' not in iface or \
|
|
not isinstance(iface['assigned_networks'], list):
|
|
raise web.webapi.badrequest(
|
|
message="There is no 'assigned_networks' list"
|
|
" in interface '%d' in node '%d'" %
|
|
(iface['id'], node['id'])
|
|
)
|
|
|
|
for net in iface['assigned_networks']:
|
|
if not isinstance(net, dict):
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d', interface '%d':"
|
|
" each assigned network should be dict" %
|
|
(iface['id'], node['id'])
|
|
)
|
|
if 'id' not in net:
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d', interface '%d':"
|
|
" each assigned network should have ID" %
|
|
(iface['id'], node['id'])
|
|
)
|
|
if net['id'] in net_ids:
|
|
raise web.webapi.badrequest(
|
|
message="Assigned networks for node '%d' have"
|
|
" a duplicate network '%d' (second"
|
|
" occurrence in interface '%d')" %
|
|
(node['id'], net['id'], iface['id'])
|
|
)
|
|
net_ids.add(net['id'])
|
|
return node
|
|
|
|
@classmethod
|
|
def validate_structure(cls, webdata):
|
|
node_data = cls.validate_json(webdata)
|
|
return cls.validate(node_data)
|
|
|
|
@classmethod
|
|
def validate_collection_structure(cls, webdata):
|
|
data = cls.validate_json(webdata)
|
|
if not isinstance(data, list):
|
|
raise web.webapi.badrequest(message="Data should be list of nodes")
|
|
for node_data in data:
|
|
cls.validate(node_data)
|
|
return data
|
|
|
|
@classmethod
|
|
def verify_data_correctness(cls, node):
|
|
db_node = orm().query(Node).filter_by(id=node['id']).first()
|
|
if not db_node:
|
|
raise web.webapi.badrequest(
|
|
message="There is no node with ID '%d' in DB" % node['id']
|
|
)
|
|
interfaces = node['interfaces']
|
|
db_interfaces = db_node.interfaces
|
|
if len(interfaces) != len(db_interfaces):
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d' has different amount of interfaces" %
|
|
node['id']
|
|
)
|
|
# FIXIT: we should use not all networks but appropriate for this
|
|
# node only.
|
|
db_network_groups = orm().query(NetworkGroup).filter_by(
|
|
cluster_id=db_node.cluster_id
|
|
).all()
|
|
if not db_network_groups:
|
|
raise web.webapi.badrequest(
|
|
message="There are no networks related to"
|
|
" node '%d' in DB" % node['id']
|
|
)
|
|
network_group_ids = set([ng.id for ng in db_network_groups])
|
|
|
|
for iface in interfaces:
|
|
db_iface = filter(
|
|
lambda i: i.id == iface['id'],
|
|
db_interfaces
|
|
)
|
|
if not db_iface:
|
|
raise web.webapi.badrequest(
|
|
message="There is no interface with ID '%d'"
|
|
" for node '%d' in DB" %
|
|
(iface['id'], node['id'])
|
|
)
|
|
db_iface = db_iface[0]
|
|
|
|
for net in iface['assigned_networks']:
|
|
if net['id'] not in network_group_ids:
|
|
raise web.webapi.badrequest(
|
|
message="Node '%d' shouldn't be connected to"
|
|
" network with ID '%d'" %
|
|
(node['id'], net['id'])
|
|
)
|
|
network_group_ids.remove(net['id'])
|
|
|
|
# Check if there are unassigned networks for this node.
|
|
if network_group_ids:
|
|
raise web.webapi.badrequest(
|
|
message="Too few neworks to assign to node '%d'" %
|
|
node['id']
|
|
)
|