391 lines
13 KiB
Python
Executable File
391 lines
13 KiB
Python
Executable File
# -*- coding: utf-8 -*-
|
|
|
|
# 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.
|
|
import copy
|
|
import uuid
|
|
|
|
from sqlalchemy import Boolean
|
|
from sqlalchemy import Column
|
|
from sqlalchemy import DateTime
|
|
from sqlalchemy import Enum
|
|
from sqlalchemy import ForeignKey
|
|
from sqlalchemy import Integer
|
|
from sqlalchemy import String
|
|
from sqlalchemy import Text
|
|
from sqlalchemy import Unicode
|
|
from sqlalchemy.orm import relationship, backref
|
|
from sqlalchemy.dialects import postgresql as psql
|
|
|
|
from nailgun import consts
|
|
from nailgun.db import db
|
|
from nailgun.db.sqlalchemy.models.base import Base
|
|
from nailgun.db.sqlalchemy.models.fields import JSON
|
|
from nailgun.db.sqlalchemy.models.fields import LowercaseString
|
|
from nailgun.db.sqlalchemy.models.network import NetworkBondAssignment
|
|
from nailgun.db.sqlalchemy.models.network import NetworkNICAssignment
|
|
from nailgun.extensions.volume_manager.manager import VolumeManager
|
|
from nailgun.logger import logger
|
|
|
|
|
|
class NodeGroup(Base):
|
|
__tablename__ = 'nodegroups'
|
|
id = Column(Integer, primary_key=True)
|
|
cluster_id = Column(Integer, ForeignKey('clusters.id'))
|
|
name = Column(String(50), nullable=False)
|
|
nodes = relationship("Node")
|
|
networks = relationship(
|
|
"NetworkGroup",
|
|
backref="nodegroup",
|
|
cascade="delete"
|
|
)
|
|
|
|
|
|
class Node(Base):
|
|
__tablename__ = 'nodes'
|
|
id = Column(Integer, primary_key=True)
|
|
uuid = Column(String(36), nullable=False,
|
|
default=lambda: str(uuid.uuid4()), unique=True)
|
|
cluster_id = Column(Integer, ForeignKey('clusters.id'))
|
|
group_id = Column(Integer, ForeignKey('nodegroups.id'), nullable=True)
|
|
name = Column(Unicode(100))
|
|
status = Column(
|
|
Enum(*consts.NODE_STATUSES, name='node_status'),
|
|
nullable=False,
|
|
default=consts.NODE_STATUSES.discover
|
|
)
|
|
meta = Column(JSON, default={})
|
|
mac = Column(LowercaseString(17), nullable=False, unique=True)
|
|
ip = Column(String(15))
|
|
fqdn = Column(String(255))
|
|
manufacturer = Column(Unicode(50))
|
|
platform_name = Column(String(150))
|
|
kernel_params = Column(Text)
|
|
progress = Column(Integer, default=0)
|
|
os_platform = Column(String(150))
|
|
pending_addition = Column(Boolean, default=False)
|
|
pending_deletion = Column(Boolean, default=False)
|
|
changes = relationship("ClusterChanges", backref="node")
|
|
error_type = Column(Enum(*consts.NODE_ERRORS, name='node_error_type'))
|
|
error_msg = Column(String(255))
|
|
timestamp = Column(DateTime, nullable=False)
|
|
online = Column(Boolean, default=True)
|
|
roles = Column(psql.ARRAY(String(consts.ROLE_NAME_MAX_SIZE)),
|
|
default=[], nullable=False, server_default='{}')
|
|
pending_roles = Column(psql.ARRAY(String(consts.ROLE_NAME_MAX_SIZE)),
|
|
default=[], nullable=False, server_default='{}')
|
|
primary_roles = Column(psql.ARRAY(String(consts.ROLE_NAME_MAX_SIZE)),
|
|
default=[], nullable=False, server_default='{}')
|
|
attributes = relationship("NodeAttributes",
|
|
backref=backref("node"),
|
|
uselist=False,
|
|
cascade="all,delete")
|
|
nic_interfaces = relationship("NodeNICInterface", backref="node",
|
|
cascade="delete",
|
|
order_by="NodeNICInterface.name")
|
|
bond_interfaces = relationship("NodeBondInterface", backref="node",
|
|
cascade="delete",
|
|
order_by="NodeBondInterface.name")
|
|
# hash function from raw node agent request data - for caching purposes
|
|
agent_checksum = Column(String(40), nullable=True)
|
|
|
|
ip_addrs = relationship("IPAddr", viewonly=True)
|
|
replaced_deployment_info = Column(JSON, default=[])
|
|
replaced_provisioning_info = Column(JSON, default={})
|
|
|
|
@property
|
|
def interfaces(self):
|
|
return self.nic_interfaces + self.bond_interfaces
|
|
|
|
@property
|
|
def uid(self):
|
|
return str(self.id)
|
|
|
|
@property
|
|
def offline(self):
|
|
return not self.online
|
|
|
|
@property
|
|
def network_data(self):
|
|
# TODO(enchantner): move to object
|
|
from nailgun.network.manager import NetworkManager
|
|
return NetworkManager.get_node_networks(self)
|
|
|
|
@property
|
|
def volume_manager(self):
|
|
# TODO(eli): will be moved into an extension.
|
|
# Should be done as a part of blueprint:
|
|
# https://blueprints.launchpad.net/fuel/+spec
|
|
# /volume-manager-refactoring
|
|
return VolumeManager(self)
|
|
|
|
@property
|
|
def needs_reprovision(self):
|
|
return self.status == 'error' and self.error_type == 'provision' and \
|
|
not self.pending_deletion
|
|
|
|
@property
|
|
def needs_redeploy(self):
|
|
return (
|
|
self.status in ['error', 'provisioned'] or
|
|
len(self.pending_roles)) and not self.pending_deletion
|
|
|
|
@property
|
|
def needs_redeletion(self):
|
|
return self.status == 'error' and self.error_type == 'deletion'
|
|
|
|
@property
|
|
def human_readable_name(self):
|
|
return self.name or self.mac
|
|
|
|
@property
|
|
def full_name(self):
|
|
return u'%s (id=%s, mac=%s)' % (self.name, self.id, self.mac)
|
|
|
|
@property
|
|
def all_roles(self):
|
|
"""Returns all roles, self.roles and self.pending_roles."""
|
|
return set(self.pending_roles + self.roles)
|
|
|
|
def _check_interface_has_required_params(self, iface):
|
|
return bool(iface.get('name') and iface.get('mac'))
|
|
|
|
def _clean_iface(self, iface):
|
|
# cleaning up unnecessary fields - set to None if bad
|
|
for param in ["max_speed", "current_speed"]:
|
|
val = iface.get(param)
|
|
if not (isinstance(val, int) and val >= 0):
|
|
val = None
|
|
iface[param] = val
|
|
return iface
|
|
|
|
def update_meta(self, data):
|
|
# helper for basic checking meta before updation
|
|
result = []
|
|
if "interfaces" in data:
|
|
for iface in data["interfaces"]:
|
|
if not self._check_interface_has_required_params(iface):
|
|
logger.warning(
|
|
"Invalid interface data: {0}. "
|
|
"Interfaces are not updated.".format(iface)
|
|
)
|
|
data["interfaces"] = self.meta.get("interfaces")
|
|
self.meta = data
|
|
return
|
|
result.append(self._clean_iface(iface))
|
|
|
|
data["interfaces"] = result
|
|
self.meta = data
|
|
|
|
def create_meta(self, data):
|
|
# helper for basic checking meta before creation
|
|
result = []
|
|
if "interfaces" in data:
|
|
for iface in data["interfaces"]:
|
|
if not self._check_interface_has_required_params(iface):
|
|
logger.warning(
|
|
"Invalid interface data: {0}. "
|
|
"Skipping interface.".format(iface)
|
|
)
|
|
continue
|
|
result.append(self._clean_iface(iface))
|
|
|
|
data["interfaces"] = result
|
|
self.meta = data
|
|
|
|
def reset_name_to_default(self):
|
|
"""Reset name to default
|
|
TODO(el): move to node REST object which
|
|
will be introduced in 5.0 release
|
|
"""
|
|
self.name = u'Untitled ({0})'.format(self.mac[-5:])
|
|
|
|
@classmethod
|
|
def delete_by_ids(cls, ids):
|
|
db.query(Node).filter(Node.id.in_(ids)).delete('fetch')
|
|
|
|
|
|
class NodeAttributes(Base):
|
|
__tablename__ = 'node_attributes'
|
|
id = Column(Integer, primary_key=True)
|
|
node_id = Column(Integer, ForeignKey('nodes.id', ondelete='CASCADE'))
|
|
interfaces = Column(JSON, default={})
|
|
vms_conf = Column(JSON, default=[], server_default='[]')
|
|
|
|
|
|
class NodeNICInterface(Base):
|
|
__tablename__ = 'node_nic_interfaces'
|
|
id = Column(Integer, primary_key=True)
|
|
node_id = Column(
|
|
Integer,
|
|
ForeignKey('nodes.id', ondelete="CASCADE"),
|
|
nullable=False)
|
|
name = Column(String(128), nullable=False)
|
|
mac = Column(LowercaseString(17), nullable=False)
|
|
max_speed = Column(Integer)
|
|
current_speed = Column(Integer)
|
|
assigned_networks_list = relationship(
|
|
"NetworkGroup",
|
|
secondary=NetworkNICAssignment.__table__,
|
|
order_by="NetworkGroup.id")
|
|
ip_addr = Column(String(25))
|
|
netmask = Column(String(25))
|
|
state = Column(String(25))
|
|
interface_properties = Column(JSON, default={}, nullable=False,
|
|
server_default='{}')
|
|
parent_id = Column(Integer, ForeignKey('node_bond_interfaces.id'))
|
|
driver = Column(Text)
|
|
bus_info = Column(Text)
|
|
pxe = Column(Boolean, default=False, nullable=False)
|
|
|
|
offloading_modes = Column(JSON, default=[], nullable=False,
|
|
server_default='[]')
|
|
|
|
@property
|
|
def type(self):
|
|
return consts.NETWORK_INTERFACE_TYPES.ether
|
|
|
|
@property
|
|
def assigned_networks(self):
|
|
return [
|
|
{"id": n.id, "name": n.name}
|
|
for n in self.assigned_networks_list
|
|
]
|
|
|
|
@assigned_networks.setter
|
|
def assigned_networks(self, value):
|
|
self.assigned_networks_list = value
|
|
|
|
# TODO(fzhadaev): move to object
|
|
@classmethod
|
|
def offloading_modes_as_flat_dict(cls, modes):
|
|
"""Represents multilevel structure of offloading modes
|
|
as flat dictionary for easy merging.
|
|
:param modes: list of offloading modes
|
|
:return: flat dictionary {mode['name']: mode['state']}
|
|
"""
|
|
result = dict()
|
|
if modes is None:
|
|
return result
|
|
for mode in modes:
|
|
result[mode["name"]] = mode["state"]
|
|
if mode["sub"]:
|
|
result.update(cls.offloading_modes_as_flat_dict(mode["sub"]))
|
|
return result
|
|
|
|
|
|
class NodeBondInterface(Base):
|
|
__tablename__ = 'node_bond_interfaces'
|
|
id = Column(Integer, primary_key=True)
|
|
node_id = Column(
|
|
Integer,
|
|
ForeignKey('nodes.id', ondelete="CASCADE"),
|
|
nullable=False)
|
|
name = Column(String(32), nullable=False)
|
|
mac = Column(LowercaseString(17))
|
|
assigned_networks_list = relationship(
|
|
"NetworkGroup",
|
|
secondary=NetworkBondAssignment.__table__,
|
|
order_by="NetworkGroup.id")
|
|
state = Column(String(25))
|
|
interface_properties = Column(JSON, default={}, nullable=False,
|
|
server_default='{}')
|
|
mode = Column(
|
|
Enum(
|
|
*consts.BOND_MODES,
|
|
name='bond_mode'
|
|
),
|
|
nullable=False,
|
|
default=consts.BOND_MODES.active_backup
|
|
)
|
|
bond_properties = Column(JSON, default={}, nullable=False,
|
|
server_default='{}')
|
|
slaves = relationship("NodeNICInterface", backref="bond")
|
|
|
|
@property
|
|
def max_speed(self):
|
|
return None
|
|
|
|
@property
|
|
def current_speed(self):
|
|
return None
|
|
|
|
@property
|
|
def type(self):
|
|
return consts.NETWORK_INTERFACE_TYPES.bond
|
|
|
|
@property
|
|
def assigned_networks(self):
|
|
return [
|
|
{"id": n.id, "name": n.name}
|
|
for n in self.assigned_networks_list
|
|
]
|
|
|
|
@assigned_networks.setter
|
|
def assigned_networks(self, value):
|
|
self.assigned_networks_list = value
|
|
|
|
@property
|
|
def offloading_modes(self):
|
|
tmp = None
|
|
intersection_dict = {}
|
|
for interface in self.slaves:
|
|
modes = interface.offloading_modes
|
|
if tmp is None:
|
|
tmp = modes
|
|
intersection_dict = \
|
|
interface.offloading_modes_as_flat_dict(tmp)
|
|
continue
|
|
intersection_dict = self._intersect_offloading_dicts(
|
|
intersection_dict,
|
|
interface.offloading_modes_as_flat_dict(modes)
|
|
)
|
|
|
|
return self._apply_intersection(tmp, intersection_dict)
|
|
|
|
@offloading_modes.setter
|
|
def offloading_modes(self, new_modes):
|
|
new_modes_dict = \
|
|
NodeNICInterface.offloading_modes_as_flat_dict(new_modes)
|
|
for interface in self.slaves:
|
|
self._update_modes(interface.offloading_modes, new_modes_dict)
|
|
|
|
def _update_modes(self, modes, update_dict):
|
|
for mode in modes:
|
|
if mode['name'] in update_dict:
|
|
mode['state'] = update_dict[mode['name']]
|
|
if mode['sub']:
|
|
self._update_modes(mode['sub'], update_dict)
|
|
|
|
def _intersect_offloading_dicts(self, dict1, dict2):
|
|
result = dict()
|
|
for mode in dict1:
|
|
if mode in dict2:
|
|
result[mode] = dict1[mode] and dict2[mode]
|
|
return result
|
|
|
|
def _apply_intersection(self, modes, intersection_dict):
|
|
result = list()
|
|
if modes is None:
|
|
return result
|
|
for mode in copy.deepcopy(modes):
|
|
if mode["name"] not in intersection_dict:
|
|
continue
|
|
mode["state"] = intersection_dict[mode["name"]]
|
|
if mode["sub"]:
|
|
mode["sub"] = \
|
|
self._apply_intersection(mode["sub"], intersection_dict)
|
|
result.append(mode)
|
|
return result
|