d3f203535a
1. Added an endpoint for adding machines. 2. Added user concept to machine model. Change-Id: I603296d30dbd7182678d98fd5f8f56503cbdc634
1912 lines
61 KiB
Python
1912 lines
61 KiB
Python
# Copyright 2014 Huawei Technologies Co. Ltd
|
|
#
|
|
# 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.
|
|
|
|
"""Database model"""
|
|
import copy
|
|
import datetime
|
|
import logging
|
|
import netaddr
|
|
import re
|
|
import simplejson as json
|
|
|
|
from sqlalchemy import BigInteger
|
|
from sqlalchemy import Boolean
|
|
from sqlalchemy import Column
|
|
from sqlalchemy import ColumnDefault
|
|
from sqlalchemy import DateTime
|
|
from sqlalchemy import Enum
|
|
from sqlalchemy.ext.declarative import declarative_base
|
|
from sqlalchemy.ext.hybrid import hybrid_property
|
|
from sqlalchemy import Float
|
|
from sqlalchemy import ForeignKey
|
|
from sqlalchemy import Integer
|
|
from sqlalchemy.orm import relationship, backref
|
|
from sqlalchemy import String
|
|
from sqlalchemy import Table
|
|
from sqlalchemy import Text
|
|
from sqlalchemy.types import TypeDecorator
|
|
from sqlalchemy import UniqueConstraint
|
|
|
|
from compass.db import exception
|
|
from compass.utils import util
|
|
|
|
|
|
BASE = declarative_base()
|
|
|
|
|
|
class JSONEncoded(TypeDecorator):
|
|
"""Represents an immutable structure as a json-encoded string."""
|
|
|
|
impl = Text
|
|
|
|
def process_bind_param(self, value, dialect):
|
|
if value is not None:
|
|
value = json.dumps(value)
|
|
return value
|
|
|
|
def process_result_value(self, value, dialect):
|
|
if value is not None:
|
|
value = json.loads(value)
|
|
return value
|
|
|
|
|
|
class TimestampMixin(object):
|
|
"""Provides table fields for each row created/updated timestamp."""
|
|
created_at = Column(DateTime, default=lambda: datetime.datetime.now())
|
|
updated_at = Column(DateTime, default=lambda: datetime.datetime.now(),
|
|
onupdate=lambda: datetime.datetime.now())
|
|
|
|
|
|
class HelperMixin(object):
|
|
"""Provides general fuctions for all compass table models."""
|
|
|
|
def initialize(self):
|
|
self.update()
|
|
|
|
def update(self):
|
|
pass
|
|
|
|
@staticmethod
|
|
def type_compatible(value, column_type):
|
|
"""Check if value type is compatible with the column type."""
|
|
if value is None:
|
|
return True
|
|
if not hasattr(column_type, 'python_type'):
|
|
return True
|
|
column_python_type = column_type.python_type
|
|
if isinstance(value, column_python_type):
|
|
return True
|
|
if issubclass(column_python_type, basestring):
|
|
return isinstance(value, basestring)
|
|
if column_python_type in [int, long]:
|
|
return type(value) in [int, long]
|
|
if column_python_type in [float]:
|
|
return type(value) in [float]
|
|
if column_python_type in [bool]:
|
|
return type(value) in [bool]
|
|
return False
|
|
|
|
def validate(self):
|
|
"""Generate validate function to make sure the record is legal."""
|
|
columns = self.__mapper__.columns
|
|
for key, column in columns.items():
|
|
value = getattr(self, key)
|
|
if not self.type_compatible(value, column.type):
|
|
raise exception.InvalidParameter(
|
|
'column %s value %r type is unexpected: %s' % (
|
|
key, value, column.type
|
|
)
|
|
)
|
|
|
|
def to_dict(self):
|
|
"""General function to convert record to dict.
|
|
|
|
Convert all columns not starting with '_' to
|
|
{<column_name>: <column_value>}
|
|
"""
|
|
keys = self.__mapper__.columns.keys()
|
|
dict_info = {}
|
|
for key in keys:
|
|
if key.startswith('_'):
|
|
continue
|
|
value = getattr(self, key)
|
|
if value is not None:
|
|
if isinstance(value, datetime.datetime):
|
|
value = util.format_datetime(value)
|
|
dict_info[key] = value
|
|
return dict_info
|
|
|
|
|
|
class StateMixin(TimestampMixin, HelperMixin):
|
|
"""Provides general fields and functions for state related table."""
|
|
|
|
state = Column(
|
|
Enum(
|
|
'UNINITIALIZED', 'INITIALIZED', 'UPDATE_PREPARING',
|
|
'INSTALLING', 'SUCCESSFUL', 'ERROR'
|
|
),
|
|
ColumnDefault('UNINITIALIZED')
|
|
)
|
|
percentage = Column(Float, default=0.0)
|
|
message = Column(Text, default='')
|
|
severity = Column(
|
|
Enum('INFO', 'WARNING', 'ERROR'),
|
|
ColumnDefault('INFO')
|
|
)
|
|
ready = Column(Boolean, default=False)
|
|
|
|
def update(self):
|
|
# In state table, some field information is redundant.
|
|
# The update function to make sure all related fields
|
|
# are set to correct state.
|
|
if self.ready:
|
|
self.state = 'SUCCESSFUL'
|
|
if self.state in ['UNINITIALIZED', 'INITIALIZED']:
|
|
self.percentage = 0.0
|
|
self.severity = 'INFO'
|
|
self.message = ''
|
|
if self.state == 'INSTALLING':
|
|
if self.severity == 'ERROR':
|
|
self.state = 'ERROR'
|
|
elif self.percentage >= 1.0:
|
|
self.state = 'SUCCESSFUL'
|
|
self.percentage = 1.0
|
|
if self.state == 'SUCCESSFUL':
|
|
self.percentage = 1.0
|
|
super(StateMixin, self).update()
|
|
|
|
|
|
class LogHistoryMixin(TimestampMixin, HelperMixin):
|
|
"""Provides general fields and functions for LogHistory related tables."""
|
|
position = Column(Integer, default=0)
|
|
partial_line = Column(Text, default='')
|
|
percentage = Column(Float, default=0.0)
|
|
message = Column(Text, default='')
|
|
severity = Column(
|
|
Enum('ERROR', 'WARNING', 'INFO'),
|
|
ColumnDefault('INFO')
|
|
)
|
|
line_matcher_name = Column(
|
|
String(80), default='start'
|
|
)
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
if not self.filename:
|
|
raise exception.InvalidParameter(
|
|
'filename is not set in %s' % self.id
|
|
)
|
|
|
|
|
|
class HostNetwork(BASE, TimestampMixin, HelperMixin):
|
|
"""Host network table."""
|
|
__tablename__ = 'host_network'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
host_id = Column(
|
|
Integer,
|
|
ForeignKey('host.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
interface = Column(
|
|
String(80), nullable=False)
|
|
subnet_id = Column(
|
|
Integer,
|
|
ForeignKey('subnet.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
ip_int = Column(BigInteger, unique=True, nullable=False)
|
|
is_mgmt = Column(Boolean, default=False)
|
|
is_promiscuous = Column(Boolean, default=False)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('host_id', 'interface', name='constraint'),
|
|
)
|
|
|
|
def __init__(self, host_id, interface, **kwargs):
|
|
self.host_id = host_id
|
|
self.interface = interface
|
|
super(HostNetwork, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'HostNetwork[%s=%s]' % (self.interface, self.ip)
|
|
|
|
@property
|
|
def ip(self):
|
|
return str(netaddr.IPAddress(self.ip_int))
|
|
|
|
@ip.setter
|
|
def ip(self, value):
|
|
self.ip_int = int(netaddr.IPAddress(value))
|
|
|
|
@property
|
|
def netmask(self):
|
|
return str(netaddr.IPNetwork(self.subnet.subnet).netmask)
|
|
|
|
def update(self):
|
|
self.host.config_validated = False
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(HostNetwork, self).validate()
|
|
if not self.subnet:
|
|
raise exception.InvalidParameter(
|
|
'subnet is not set in %s interface %s' % (
|
|
self.host_id, self.interface
|
|
)
|
|
)
|
|
if not self.ip_int:
|
|
raise exception.InvalidParameter(
|
|
'ip is not set in %s interface %s' % (
|
|
self.host_id, self.interface
|
|
)
|
|
)
|
|
ip = netaddr.IPAddress(self.ip_int)
|
|
subnet = netaddr.IPNetwork(self.subnet.subnet)
|
|
if ip not in subnet:
|
|
raise exception.InvalidParameter(
|
|
'ip %s is not in subnet %s' % (
|
|
str(ip), str(subnet)
|
|
)
|
|
)
|
|
|
|
def to_dict(self):
|
|
dict_info = super(HostNetwork, self).to_dict()
|
|
dict_info['ip'] = self.ip
|
|
dict_info['interface'] = self.interface
|
|
dict_info['netmask'] = self.netmask
|
|
dict_info['subnet'] = self.subnet.subnet
|
|
return dict_info
|
|
|
|
|
|
class ClusterHostLogHistory(BASE, LogHistoryMixin):
|
|
"""clusterhost installing log history for each file.
|
|
|
|
"""
|
|
__tablename__ = 'clusterhost_log_history'
|
|
|
|
clusterhost_id = Column(
|
|
'id', Integer,
|
|
ForeignKey('clusterhost.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True
|
|
)
|
|
filename = Column(String(80), primary_key=True, nullable=False)
|
|
cluster_id = Column(
|
|
Integer,
|
|
ForeignKey('cluster.id')
|
|
)
|
|
host_id = Column(
|
|
Integer,
|
|
ForeignKey('host.id')
|
|
)
|
|
|
|
def __init__(self, clusterhost_id, filename, **kwargs):
|
|
self.clusterhost_id = clusterhost_id
|
|
self.filename = filename
|
|
super(ClusterHostLogHistory, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'ClusterHostLogHistory[%s:%s]' % (
|
|
self.clusterhost_id, self.filename
|
|
)
|
|
|
|
def initialize(self):
|
|
self.cluster_id = self.clusterhost.cluster_id
|
|
self.host_id = self.clusterhost.host_id
|
|
super(ClusterHostLogHistory, self).initialize()
|
|
|
|
|
|
class HostLogHistory(BASE, LogHistoryMixin):
|
|
"""host installing log history for each file.
|
|
|
|
"""
|
|
__tablename__ = 'host_log_history'
|
|
|
|
id = Column(
|
|
Integer,
|
|
ForeignKey('host.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True)
|
|
filename = Column(String(80), primary_key=True, nullable=False)
|
|
|
|
def __init__(self, id, filename, **kwargs):
|
|
self.id = id
|
|
self.filename = filename
|
|
super(HostLogHistory, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'HostLogHistory[%s:%s]' % (self.id, self.filename)
|
|
|
|
|
|
class ClusterHostState(BASE, StateMixin):
|
|
"""ClusterHost state table."""
|
|
__tablename__ = 'clusterhost_state'
|
|
|
|
id = Column(
|
|
Integer,
|
|
ForeignKey(
|
|
'clusterhost.id',
|
|
onupdate='CASCADE', ondelete='CASCADE'
|
|
),
|
|
primary_key=True
|
|
)
|
|
|
|
def __str__(self):
|
|
return 'ClusterHostState[%s state %s percentage %s]' % (
|
|
self.id, self.state, self.percentage
|
|
)
|
|
|
|
def update(self):
|
|
"""Update clusterhost state.
|
|
|
|
When clusterhost state is updated, the underlying host state
|
|
may be updated accordingly.
|
|
"""
|
|
super(ClusterHostState, self).update()
|
|
host_state = self.clusterhost.host.state
|
|
if self.state == 'INITIALIZED':
|
|
if host_state.state in ['UNINITIALIZED', 'UPDATE_PREPARING']:
|
|
host_state.state = 'INITIALIZED'
|
|
host_state.update()
|
|
elif self.state == 'INSTALLING':
|
|
if host_state.state in [
|
|
'UNINITIALIZED', 'UPDATE_PREPARING', 'INITIALIZED'
|
|
]:
|
|
host_state.state = 'INSTALLING'
|
|
host_state.update()
|
|
elif self.state == 'SUCCESSFUL':
|
|
if host_state.state != 'SUCCESSFUL':
|
|
host_state.state = 'SUCCESSFUL'
|
|
host_state.update()
|
|
|
|
|
|
class ClusterHost(BASE, TimestampMixin, HelperMixin):
|
|
"""ClusterHost table."""
|
|
__tablename__ = 'clusterhost'
|
|
|
|
clusterhost_id = Column('id', Integer, primary_key=True)
|
|
cluster_id = Column(
|
|
Integer,
|
|
ForeignKey('cluster.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
host_id = Column(
|
|
Integer,
|
|
ForeignKey('host.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
# the list of role names.
|
|
_roles = Column('roles', JSONEncoded, default=[])
|
|
_patched_roles = Column('patched_roles', JSONEncoded, default=[])
|
|
config_step = Column(String(80), default='')
|
|
package_config = Column(JSONEncoded, default={})
|
|
config_validated = Column(Boolean, default=False)
|
|
deployed_package_config = Column(JSONEncoded, default={})
|
|
|
|
log_histories = relationship(
|
|
ClusterHostLogHistory,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('clusterhost')
|
|
)
|
|
|
|
__table_args__ = (
|
|
UniqueConstraint('cluster_id', 'host_id', name='constraint'),
|
|
)
|
|
|
|
state = relationship(
|
|
ClusterHostState,
|
|
uselist=False,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('clusterhost')
|
|
)
|
|
|
|
def __init__(self, cluster_id, host_id, **kwargs):
|
|
self.cluster_id = cluster_id
|
|
self.host_id = host_id
|
|
self.state = ClusterHostState()
|
|
super(ClusterHost, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'ClusterHost[%s:%s]' % (self.clusterhost_id, self.name)
|
|
|
|
def update(self):
|
|
if self.host.reinstall_os:
|
|
if self.state in ['SUCCESSFUL', 'ERROR']:
|
|
if self.config_validated:
|
|
self.state.state = 'INITIALIZED'
|
|
else:
|
|
self.state.state = 'UNINITIALIZED'
|
|
self.cluster.update()
|
|
self.host.update()
|
|
self.state.update()
|
|
super(ClusterHost, self).update()
|
|
|
|
@property
|
|
def name(self):
|
|
return '%s.%s' % (self.host.name, self.cluster.name)
|
|
|
|
@property
|
|
def patched_package_config(self):
|
|
return self.package_config
|
|
|
|
@patched_package_config.setter
|
|
def patched_package_config(self, value):
|
|
package_config = copy.deepcopy(self.package_config)
|
|
self.package_config = util.merge_dict(package_config, value)
|
|
logging.debug(
|
|
'patch clusterhost %s package_config: %s',
|
|
self.clusterhost_id, value
|
|
)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def put_package_config(self):
|
|
return self.package_config
|
|
|
|
@put_package_config.setter
|
|
def put_package_config(self, value):
|
|
package_config = copy.deepcopy(self.package_config)
|
|
package_config.update(value)
|
|
self.package_config = package_config
|
|
logging.debug(
|
|
'put clusterhost %s package_config: %s',
|
|
self.clusterhost_id, value
|
|
)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def patched_os_config(self):
|
|
return self.host.os_config
|
|
|
|
@patched_os_config.setter
|
|
def patched_os_config(self, value):
|
|
host = self.host
|
|
host.patched_os_config = value
|
|
|
|
@property
|
|
def put_os_config(self):
|
|
return self.host.os_config
|
|
|
|
@put_os_config.setter
|
|
def put_os_config(self, value):
|
|
host = self.host
|
|
host.put_os_config = value
|
|
|
|
@property
|
|
def deployed_os_config(self):
|
|
return self.host.deployed_os_config
|
|
|
|
@deployed_os_config.setter
|
|
def deployed_os_config(self, value):
|
|
host = self.host
|
|
host.deployed_os_config = value
|
|
|
|
@hybrid_property
|
|
def os_name(self):
|
|
return self.host.os_name
|
|
|
|
@os_name.expression
|
|
def os_name(cls):
|
|
return cls.host.os_name
|
|
|
|
@hybrid_property
|
|
def clustername(self):
|
|
return self.cluster.name
|
|
|
|
@clustername.expression
|
|
def clustername(cls):
|
|
return cls.cluster.name
|
|
|
|
@hybrid_property
|
|
def hostname(self):
|
|
return self.host.hostname
|
|
|
|
@hostname.expression
|
|
def hostname(cls):
|
|
return Host.hostname
|
|
|
|
@property
|
|
def distributed_system_installed(self):
|
|
return self.state.state == 'SUCCESSFUL'
|
|
|
|
@property
|
|
def resintall_os(self):
|
|
return self.host.reinstall_os
|
|
|
|
@property
|
|
def reinstall_distributed_system(self):
|
|
return self.cluster.reinstall_distributed_system
|
|
|
|
@property
|
|
def os_installed(self):
|
|
return self.host.os_installed
|
|
|
|
@property
|
|
def roles(self):
|
|
# only the role exists in flavor roles will be returned.
|
|
# the role will be sorted as the order defined in flavor
|
|
# roles.
|
|
# duplicate role names will be removed.
|
|
# The returned value is a list of dict like
|
|
# [{'name': 'allinone', 'optional': False}]
|
|
role_names = list(self._roles)
|
|
if not role_names:
|
|
return []
|
|
cluster_roles = self.cluster.flavor['roles']
|
|
if not cluster_roles:
|
|
return []
|
|
roles = []
|
|
for cluster_role in cluster_roles:
|
|
if cluster_role['name'] in role_names:
|
|
roles.append(cluster_role)
|
|
return roles
|
|
|
|
@roles.setter
|
|
def roles(self, value):
|
|
"""value should be a list of role name."""
|
|
self._roles = list(value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def patched_roles(self):
|
|
patched_role_names = list(self._patched_roles)
|
|
if not patched_role_names:
|
|
return []
|
|
cluster_roles = self.cluster.flavor['roles']
|
|
if not cluster_roles:
|
|
return []
|
|
roles = []
|
|
for cluster_role in cluster_roles:
|
|
if cluster_role['name'] in patched_role_names:
|
|
roles.append(cluster_role)
|
|
return roles
|
|
|
|
@patched_roles.setter
|
|
def patched_roles(self, value):
|
|
"""value should be a list of role name."""
|
|
# if value is an empty list, we empty the field
|
|
if value:
|
|
roles = list(self._roles)
|
|
roles.extend(value)
|
|
self._roles = roles
|
|
patched_roles = list(self._patched_roles)
|
|
patched_roles.extend(value)
|
|
self._patched_roles = patched_roles
|
|
self.config_validated = False
|
|
else:
|
|
self._patched_roles = list(value)
|
|
self.config_validated = False
|
|
|
|
@hybrid_property
|
|
def owner(self):
|
|
return self.cluster.owner
|
|
|
|
@owner.expression
|
|
def owner(cls):
|
|
return cls.cluster.owner
|
|
|
|
def state_dict(self):
|
|
"""Get clusterhost state dict.
|
|
|
|
The clusterhost state_dict is different from
|
|
clusterhost.state.to_dict. The main difference is state_dict
|
|
show the progress of both installing os on host and installing
|
|
distributed system on clusterhost. While clusterhost.state.to_dict
|
|
only shows the progress of installing distributed system on
|
|
clusterhost.
|
|
"""
|
|
cluster = self.cluster
|
|
host = self.host
|
|
host_state = host.state_dict()
|
|
if not cluster.flavor_name:
|
|
return host_state
|
|
clusterhost_state = self.state.to_dict()
|
|
if clusterhost_state['state'] in ['ERROR', 'SUCCESSFUL']:
|
|
return clusterhost_state
|
|
if (
|
|
clusterhost_state['state'] in 'INSTALLING' and
|
|
clusterhost_state['percentage'] > 0
|
|
):
|
|
clusterhost_state['percentage'] = min(
|
|
1.0, (
|
|
0.5 + clusterhost_state['percentage'] / 2
|
|
)
|
|
)
|
|
return clusterhost_state
|
|
|
|
host_state['percentage'] = host_state['percentage'] / 2
|
|
if host_state['state'] == 'SUCCESSFUL':
|
|
host_state['state'] = 'INSTALLING'
|
|
return host_state
|
|
|
|
def to_dict(self):
|
|
dict_info = self.host.to_dict()
|
|
dict_info.update(super(ClusterHost, self).to_dict())
|
|
state_dict = self.state_dict()
|
|
dict_info.update({
|
|
'distributed_system_installed': self.distributed_system_installed,
|
|
'reinstall_distributed_system': self.reinstall_distributed_system,
|
|
'owner': self.owner,
|
|
'clustername': self.clustername,
|
|
'name': self.name,
|
|
'state': state_dict['state']
|
|
})
|
|
dict_info['roles'] = self.roles
|
|
dict_info['patched_roles'] = self.patched_roles
|
|
return dict_info
|
|
|
|
|
|
class HostState(BASE, StateMixin):
|
|
"""Host state table."""
|
|
__tablename__ = 'host_state'
|
|
|
|
id = Column(
|
|
Integer,
|
|
ForeignKey('host.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True
|
|
)
|
|
|
|
def __str__(self):
|
|
return 'HostState[%s state %s percentage %s]' % (
|
|
self.id, self.state, self.percentage
|
|
)
|
|
|
|
def update(self):
|
|
"""Update host state.
|
|
|
|
When host state is updated, all clusterhosts on the
|
|
host will update their state if necessary.
|
|
"""
|
|
super(HostState, self).update()
|
|
host = self.host
|
|
if self.state == 'INSTALLING':
|
|
host.reinstall_os = False
|
|
for clusterhost in self.host.clusterhosts:
|
|
if clusterhost.state in [
|
|
'SUCCESSFUL', 'ERROR'
|
|
]:
|
|
clusterhost.state = 'INSTALLING'
|
|
clusterhost.state.update()
|
|
elif self.state == 'UNINITIALIZED':
|
|
for clusterhost in self.host.clusterhosts:
|
|
if clusterhost.state in [
|
|
'INITIALIZED', 'INSTALLING', 'SUCCESSFUL', 'ERROR'
|
|
]:
|
|
clusterhost.state = 'UNINITIALIZED'
|
|
clusterhost.state.update()
|
|
elif self.state == 'UPDATE_PREPARING':
|
|
for clusterhost in self.host.clusterhosts:
|
|
if clusterhost.state in [
|
|
'INITIALIZED', 'INSTALLING', 'SUCCESSFUL', 'ERROR'
|
|
]:
|
|
clusterhost.state = 'UPDATE_PREPARING'
|
|
clusterhost.state.update()
|
|
elif self.state == 'INITIALIZED':
|
|
for clusterhost in self.host.clusterhosts:
|
|
if clusterhost.state in [
|
|
'INSTALLING', 'SUCCESSFUL', 'ERROR'
|
|
]:
|
|
clusterhost.state = 'INITIALIZED'
|
|
clusterhost.state.update()
|
|
|
|
|
|
class Host(BASE, TimestampMixin, HelperMixin):
|
|
"""Host table."""
|
|
__tablename__ = 'host'
|
|
|
|
name = Column(String(80), unique=True, nullable=True)
|
|
config_step = Column(String(80), default='')
|
|
os_config = Column(JSONEncoded, default={})
|
|
config_validated = Column(Boolean, default=False)
|
|
deployed_os_config = Column(JSONEncoded, default={})
|
|
os_name = Column(String(80))
|
|
creator_id = Column(Integer, ForeignKey('user.id'))
|
|
owner = Column(String(80))
|
|
os_installer = Column(JSONEncoded, default={})
|
|
|
|
id = Column(
|
|
Integer,
|
|
ForeignKey('machine.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True
|
|
)
|
|
reinstall_os = Column(Boolean, default=True)
|
|
|
|
host_networks = relationship(
|
|
HostNetwork,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('host')
|
|
)
|
|
clusterhosts = relationship(
|
|
ClusterHost,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('host')
|
|
)
|
|
state = relationship(
|
|
HostState,
|
|
uselist=False,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('host')
|
|
)
|
|
log_histories = relationship(
|
|
HostLogHistory,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('host')
|
|
)
|
|
|
|
def __str__(self):
|
|
return 'Host[%s:%s]' % (self.id, self.name)
|
|
|
|
@hybrid_property
|
|
def mac(self):
|
|
machine = self.machine
|
|
if machine:
|
|
return machine.mac
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def os_id(self):
|
|
return self.os_name
|
|
|
|
@os_id.setter
|
|
def os_id(self, value):
|
|
self.os_name = value
|
|
|
|
@hybrid_property
|
|
def hostname(self):
|
|
return self.name
|
|
|
|
@hostname.expression
|
|
def hostname(cls):
|
|
return cls.name
|
|
|
|
@property
|
|
def patched_os_config(self):
|
|
return self.os_config
|
|
|
|
@patched_os_config.setter
|
|
def patched_os_config(self, value):
|
|
os_config = copy.deepcopy(self.os_config)
|
|
self.os_config = util.merge_dict(os_config, value)
|
|
logging.debug('patch host os config in %s: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def put_os_config(self):
|
|
return self.os_config
|
|
|
|
@put_os_config.setter
|
|
def put_os_config(self, value):
|
|
os_config = copy.deepcopy(self.os_config)
|
|
os_config.update(value)
|
|
self.os_config = os_config
|
|
logging.debug('put host os config in %s: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
def __init__(self, id, **kwargs):
|
|
self.id = id
|
|
self.state = HostState()
|
|
super(Host, self).__init__(**kwargs)
|
|
|
|
def update(self):
|
|
creator = self.creator
|
|
if creator:
|
|
self.owner = creator.email
|
|
if self.reinstall_os:
|
|
if self.state in ['SUCCESSFUL', 'ERROR']:
|
|
if self.config_validated:
|
|
self.state.state = 'INITIALIZED'
|
|
else:
|
|
self.state.state = 'UNINITIALIZED'
|
|
self.state.update()
|
|
self.state.update()
|
|
super(Host, self).update()
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to the column in future.
|
|
super(Host, self).validate()
|
|
creator = self.creator
|
|
if not creator:
|
|
raise exception.InvalidParameter(
|
|
'creator is not set in host %s' % self.id
|
|
)
|
|
os_name = self.os_name
|
|
if not os_name:
|
|
raise exception.InvalidParameter(
|
|
'os is not set in host %s' % self.id
|
|
)
|
|
os_installer = self.os_installer
|
|
if not os_installer:
|
|
raise exception.Invalidparameter(
|
|
'os_installer is not set in host %s' % self.id
|
|
)
|
|
|
|
@property
|
|
def os_installed(self):
|
|
return self.state.state == 'SUCCESSFUL'
|
|
|
|
@property
|
|
def clusters(self):
|
|
return [clusterhost.cluster for clusterhost in self.clusterhosts]
|
|
|
|
def state_dict(self):
|
|
return self.state.to_dict()
|
|
|
|
def to_dict(self):
|
|
"""Host dict contains its underlying machine dict."""
|
|
dict_info = self.machine.to_dict()
|
|
dict_info.update(super(Host, self).to_dict())
|
|
state_dict = self.state_dict()
|
|
ip = None
|
|
for host_network in self.host_networks:
|
|
if host_network.is_mgmt:
|
|
ip = host_network.ip
|
|
dict_info.update({
|
|
'machine_id': self.machine.id,
|
|
'os_installed': self.os_installed,
|
|
'hostname': self.hostname,
|
|
'ip': ip,
|
|
'networks': [
|
|
host_network.to_dict()
|
|
for host_network in self.host_networks
|
|
],
|
|
'os_id': self.os_id,
|
|
'clusters': [cluster.to_dict() for cluster in self.clusters],
|
|
'state': state_dict['state']
|
|
})
|
|
return dict_info
|
|
|
|
|
|
class ClusterState(BASE, StateMixin):
|
|
"""Cluster state table."""
|
|
__tablename__ = 'cluster_state'
|
|
|
|
id = Column(
|
|
Integer,
|
|
ForeignKey('cluster.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True
|
|
)
|
|
total_hosts = Column(
|
|
Integer,
|
|
default=0
|
|
)
|
|
installing_hosts = Column(
|
|
Integer,
|
|
default=0
|
|
)
|
|
completed_hosts = Column(
|
|
Integer,
|
|
default=0
|
|
)
|
|
failed_hosts = Column(
|
|
Integer,
|
|
default=0
|
|
)
|
|
|
|
def __init__(self, **kwargs):
|
|
super(ClusterState, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'ClusterState[%s state %s percentage %s]' % (
|
|
self.id, self.state, self.percentage
|
|
)
|
|
|
|
def to_dict(self):
|
|
dict_info = super(ClusterState, self).to_dict()
|
|
dict_info['status'] = {
|
|
'total_hosts': self.total_hosts,
|
|
'installing_hosts': self.installing_hosts,
|
|
'completed_hosts': self.completed_hosts,
|
|
'failed_hosts': self.failed_hosts
|
|
}
|
|
return dict_info
|
|
|
|
def update(self):
|
|
# all fields of cluster state should be calculated by
|
|
# its each underlying clusterhost state.
|
|
cluster = self.cluster
|
|
clusterhosts = cluster.clusterhosts
|
|
self.total_hosts = len(clusterhosts)
|
|
self.installing_hosts = 0
|
|
self.failed_hosts = 0
|
|
self.completed_hosts = 0
|
|
if not cluster.flavor_name:
|
|
for clusterhost in clusterhosts:
|
|
host = clusterhost.host
|
|
host_state = host.state.state
|
|
if host_state == 'INSTALLING':
|
|
self.installing_hosts += 1
|
|
elif host_state == 'ERROR':
|
|
self.failed_hosts += 1
|
|
elif host_state == 'SUCCESSFUL':
|
|
self.completed_hosts += 1
|
|
else:
|
|
for clusterhost in clusterhosts:
|
|
clusterhost_state = clusterhost.state.state
|
|
if clusterhost_state == 'INSTALLING':
|
|
self.installing_hosts += 1
|
|
elif clusterhost_state == 'ERROR':
|
|
self.failed_hosts += 1
|
|
elif clusterhost_state == 'SUCCESSFUL':
|
|
self.completed_hosts += 1
|
|
if self.total_hosts:
|
|
if self.completed_hosts == self.total_hosts:
|
|
self.percentage = 1.0
|
|
else:
|
|
self.percentage = (
|
|
float(self.completed_hosts)
|
|
/
|
|
float(self.total_hosts)
|
|
)
|
|
if self.state == 'SUCCESSFUL':
|
|
self.state = 'INSTALLING'
|
|
self.ready = False
|
|
self.message = (
|
|
'total %s, installing %s, completed: %s, error %s'
|
|
) % (
|
|
self.total_hosts, self.installing_hosts,
|
|
self.completed_hosts, self.failed_hosts
|
|
)
|
|
if self.failed_hosts:
|
|
self.severity = 'ERROR'
|
|
|
|
super(ClusterState, self).update()
|
|
if self.state == 'INSTALLING':
|
|
cluster.reinstall_distributed_system = False
|
|
|
|
|
|
class Cluster(BASE, TimestampMixin, HelperMixin):
|
|
"""Cluster table."""
|
|
__tablename__ = 'cluster'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(80), unique=True, nullable=False)
|
|
reinstall_distributed_system = Column(Boolean, default=True)
|
|
config_step = Column(String(80), default='')
|
|
os_name = Column(String(80))
|
|
flavor_name = Column(String(80), nullable=True)
|
|
# flavor dict got from flavor id.
|
|
flavor = Column(JSONEncoded, default={})
|
|
os_config = Column(JSONEncoded, default={})
|
|
package_config = Column(JSONEncoded, default={})
|
|
deployed_os_config = Column(JSONEncoded, default={})
|
|
deployed_package_config = Column(JSONEncoded, default={})
|
|
config_validated = Column(Boolean, default=False)
|
|
adapter_name = Column(String(80))
|
|
creator_id = Column(Integer, ForeignKey('user.id'))
|
|
owner = Column(String(80))
|
|
clusterhosts = relationship(
|
|
ClusterHost,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('cluster')
|
|
)
|
|
state = relationship(
|
|
ClusterState,
|
|
uselist=False,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('cluster')
|
|
)
|
|
|
|
def __init__(self, name, **kwargs):
|
|
self.name = name
|
|
self.state = ClusterState()
|
|
super(Cluster, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'Cluster[%s:%s]' % (self.id, self.name)
|
|
|
|
def update(self):
|
|
creator = self.creator
|
|
if creator:
|
|
self.owner = creator.email
|
|
if self.reinstall_distributed_system:
|
|
if self.state in ['SUCCESSFUL', 'ERROR']:
|
|
if self.config_validated:
|
|
self.state.state = 'INITIALIZED'
|
|
else:
|
|
self.state.state = 'UNINITIALIZED'
|
|
self.state.update()
|
|
self.state.update()
|
|
super(Cluster, self).update()
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(Cluster, self).validate()
|
|
creator = self.creator
|
|
if not creator:
|
|
raise exception.InvalidParameter(
|
|
'creator is not set in cluster %s' % self.id
|
|
)
|
|
os_name = self.os_name
|
|
if not os_name:
|
|
raise exception.InvalidParameter(
|
|
'os is not set in cluster %s' % self.id
|
|
)
|
|
adapter_name = self.adapter_name
|
|
if not adapter_name:
|
|
raise exception.InvalidParameter(
|
|
'adapter is not set in cluster %s' % self.id
|
|
)
|
|
flavor_name = self.flavor_name
|
|
if flavor_name:
|
|
if 'name' not in self.flavor:
|
|
raise exception.InvalidParameter(
|
|
'key name does not exist in flavor %s' % (
|
|
self.flavor
|
|
)
|
|
)
|
|
if flavor_name != self.flavor['name']:
|
|
raise exception.InvalidParameter(
|
|
'flavor name %s is not match '
|
|
'the name key in flavor %s' % (
|
|
flavor_name, self.flavor
|
|
)
|
|
)
|
|
else:
|
|
if self.flavor:
|
|
raise exception.InvalidParameter(
|
|
'flavor %s is not empty' % self.flavor
|
|
)
|
|
|
|
@property
|
|
def os_id(self):
|
|
return self.os_name
|
|
|
|
@os_id.setter
|
|
def os_id(self, value):
|
|
self.os_name = value
|
|
|
|
@property
|
|
def adapter_id(self):
|
|
return self.adapter_name
|
|
|
|
@adapter_id.setter
|
|
def adapter_id(self, value):
|
|
self.adapter_name = value
|
|
|
|
@property
|
|
def flavor_id(self):
|
|
if self.flavor_name:
|
|
return '%s:%s' % (self.adapter_name, self.flavor_name)
|
|
else:
|
|
return None
|
|
|
|
@flavor_id.setter
|
|
def flavor_id(self, value):
|
|
if value:
|
|
_, flavor_name = value.split(':', 1)
|
|
self.flavor_name = flavor_name
|
|
else:
|
|
self.flavor_name = value
|
|
|
|
@property
|
|
def patched_os_config(self):
|
|
return self.os_config
|
|
|
|
@patched_os_config.setter
|
|
def patched_os_config(self, value):
|
|
os_config = copy.deepcopy(self.os_config)
|
|
self.os_config = util.merge_dict(os_config, value)
|
|
logging.debug('patch cluster %s os config: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def put_os_config(self):
|
|
return self.os_config
|
|
|
|
@put_os_config.setter
|
|
def put_os_config(self, value):
|
|
os_config = copy.deepcopy(self.os_config)
|
|
os_config.update(value)
|
|
self.os_config = os_config
|
|
logging.debug('put cluster %s os config: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def patched_package_config(self):
|
|
return self.package_config
|
|
|
|
@patched_package_config.setter
|
|
def patched_package_config(self, value):
|
|
package_config = copy.deepcopy(self.package_config)
|
|
self.package_config = util.merge_dict(package_config, value)
|
|
logging.debug('patch cluster %s package config: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def put_package_config(self):
|
|
return self.package_config
|
|
|
|
@put_package_config.setter
|
|
def put_package_config(self, value):
|
|
package_config = dict(self.package_config)
|
|
package_config.update(value)
|
|
self.package_config = package_config
|
|
logging.debug('put cluster %s package config: %s', self.id, value)
|
|
self.config_validated = False
|
|
|
|
@property
|
|
def distributed_system_installed(self):
|
|
return self.state.state == 'SUCCESSFUL'
|
|
|
|
def state_dict(self):
|
|
return self.state.to_dict()
|
|
|
|
def to_dict(self):
|
|
dict_info = super(Cluster, self).to_dict()
|
|
dict_info['distributed_system_installed'] = (
|
|
self.distributed_system_installed
|
|
)
|
|
dict_info['os_id'] = self.os_id
|
|
dict_info['adapter_id'] = self.adapter_id
|
|
dict_info['flavor_id'] = self.flavor_id
|
|
return dict_info
|
|
|
|
|
|
# User, Permission relation table
|
|
class UserPermission(BASE, HelperMixin, TimestampMixin):
|
|
"""User permission table."""
|
|
__tablename__ = 'user_permission'
|
|
id = Column(Integer, primary_key=True)
|
|
user_id = Column(
|
|
Integer,
|
|
ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
permission_id = Column(
|
|
Integer,
|
|
ForeignKey('permission.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
__table_args__ = (
|
|
UniqueConstraint('user_id', 'permission_id', name='constraint'),
|
|
)
|
|
|
|
def __init__(self, user_id, permission_id, **kwargs):
|
|
self.user_id = user_id
|
|
self.permission_id = permission_id
|
|
|
|
def __str__(self):
|
|
return 'UserPermission[%s:%s]' % (self.id, self.name)
|
|
|
|
@hybrid_property
|
|
def name(self):
|
|
return self.permission.name
|
|
|
|
def to_dict(self):
|
|
dict_info = self.permission.to_dict()
|
|
dict_info.update(super(UserPermission, self).to_dict())
|
|
return dict_info
|
|
|
|
|
|
class Permission(BASE, HelperMixin, TimestampMixin):
|
|
"""Permission table."""
|
|
__tablename__ = 'permission'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(80), unique=True, nullable=False)
|
|
alias = Column(String(100))
|
|
description = Column(Text)
|
|
user_permissions = relationship(
|
|
UserPermission,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('permission')
|
|
)
|
|
|
|
def __init__(self, name, **kwargs):
|
|
self.name = name
|
|
super(Permission, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'Permission[%s:%s]' % (self.id, self.name)
|
|
|
|
|
|
class UserToken(BASE, HelperMixin):
|
|
"""user token table."""
|
|
__tablename__ = 'user_token'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
user_id = Column(
|
|
Integer,
|
|
ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
token = Column(String(256), unique=True, nullable=False)
|
|
expire_timestamp = Column(DateTime, nullable=True)
|
|
|
|
def __init__(self, token, **kwargs):
|
|
self.token = token
|
|
super(UserToken, self).__init__(**kwargs)
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(UserToken, self).validate()
|
|
if not self.user:
|
|
raise exception.InvalidParameter(
|
|
'user is not set in token: %s' % self.token
|
|
)
|
|
|
|
|
|
class UserLog(BASE, HelperMixin):
|
|
"""User log table."""
|
|
__tablename__ = 'user_log'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
user_id = Column(
|
|
Integer,
|
|
ForeignKey('user.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
action = Column(Text)
|
|
timestamp = Column(DateTime, default=lambda: datetime.datetime.now())
|
|
|
|
@hybrid_property
|
|
def user_email(self):
|
|
return self.user.email
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(UserLog, self).validate()
|
|
if not self.user:
|
|
raise exception.InvalidParameter(
|
|
'user is not set in user log: %s' % self.id
|
|
)
|
|
|
|
|
|
class User(BASE, HelperMixin, TimestampMixin):
|
|
"""User table."""
|
|
__tablename__ = 'user'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
email = Column(String(80), unique=True, nullable=False)
|
|
crypted_password = Column('password', String(225))
|
|
firstname = Column(String(80))
|
|
lastname = Column(String(80))
|
|
is_admin = Column(Boolean, default=False)
|
|
active = Column(Boolean, default=True)
|
|
user_permissions = relationship(
|
|
UserPermission,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('user')
|
|
)
|
|
user_logs = relationship(
|
|
UserLog,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('user')
|
|
)
|
|
user_tokens = relationship(
|
|
UserToken,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('user')
|
|
)
|
|
clusters = relationship(
|
|
Cluster,
|
|
backref=backref('creator')
|
|
)
|
|
hosts = relationship(
|
|
Host,
|
|
backref=backref('creator')
|
|
)
|
|
|
|
def __init__(self, email, **kwargs):
|
|
self.email = email
|
|
super(User, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'User[%s]' % self.email
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(User, self).validate()
|
|
if not self.crypted_password:
|
|
raise exception.InvalidParameter(
|
|
'password is not set in user : %s' % self.email
|
|
)
|
|
|
|
@property
|
|
def password(self):
|
|
return '***********'
|
|
|
|
@password.setter
|
|
def password(self, password):
|
|
# password stored in database is crypted.
|
|
self.crypted_password = util.encrypt(password)
|
|
|
|
@hybrid_property
|
|
def permissions(self):
|
|
permissions = []
|
|
for user_permission in self.user_permissions:
|
|
permissions.append(user_permission.permission)
|
|
|
|
return permissions
|
|
|
|
def to_dict(self):
|
|
dict_info = super(User, self).to_dict()
|
|
dict_info['permissions'] = [
|
|
permission.to_dict()
|
|
for permission in self.permissions
|
|
]
|
|
return dict_info
|
|
|
|
|
|
class SwitchMachine(BASE, HelperMixin, TimestampMixin):
|
|
"""Switch Machine table."""
|
|
__tablename__ = 'switch_machine'
|
|
switch_machine_id = Column(
|
|
'id', Integer, primary_key=True
|
|
)
|
|
switch_id = Column(
|
|
Integer,
|
|
ForeignKey('switch.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
machine_id = Column(
|
|
Integer,
|
|
ForeignKey('machine.id', onupdate='CASCADE', ondelete='CASCADE')
|
|
)
|
|
port = Column(String(80), nullable=True)
|
|
vlans = Column(JSONEncoded, default=[])
|
|
__table_args__ = (
|
|
UniqueConstraint('switch_id', 'machine_id', name='constraint'),
|
|
)
|
|
|
|
def __init__(self, switch_id, machine_id, **kwargs):
|
|
self.switch_id = switch_id
|
|
self.machine_id = machine_id
|
|
super(SwitchMachine, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'SwitchMachine[%s port %s]' % (
|
|
self.switch_machine_id, self.port
|
|
)
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(SwitchMachine, self).validate()
|
|
if not self.switch:
|
|
raise exception.InvalidParameter(
|
|
'switch is not set in %s' % self.id
|
|
)
|
|
if not self.machine:
|
|
raise exception.Invalidparameter(
|
|
'machine is not set in %s' % self.id
|
|
)
|
|
if not self.port:
|
|
raise exception.InvalidParameter(
|
|
'port is not set in %s' % self.id
|
|
)
|
|
|
|
@hybrid_property
|
|
def mac(self):
|
|
return self.machine.mac
|
|
|
|
@hybrid_property
|
|
def tag(self):
|
|
return self.machine.tag
|
|
|
|
@property
|
|
def switch_ip(self):
|
|
return self.switch.ip
|
|
|
|
@hybrid_property
|
|
def switch_ip_int(self):
|
|
return self.switch.ip_int
|
|
|
|
@switch_ip_int.expression
|
|
def switch_ip_int(cls):
|
|
return Switch.ip_int
|
|
|
|
@hybrid_property
|
|
def switch_vendor(self):
|
|
return self.switch.vendor
|
|
|
|
@switch_vendor.expression
|
|
def switch_vendor(cls):
|
|
return Switch.vendor
|
|
|
|
@property
|
|
def patched_vlans(self):
|
|
return self.vlans
|
|
|
|
@patched_vlans.setter
|
|
def patched_vlans(self, value):
|
|
if not value:
|
|
return
|
|
vlans = list(self.vlans)
|
|
for item in value:
|
|
if item not in vlans:
|
|
vlans.append(item)
|
|
self.vlans = vlans
|
|
|
|
@property
|
|
def filtered(self):
|
|
"""Check if switch machine should be filtered.
|
|
|
|
port should be composed with <port_prefix><port_number><port_suffix>
|
|
For each filter in switch machine filters,
|
|
if filter_type is allow and port match the pattern, the switch
|
|
machine is allowed to be got by api. If filter_type is deny and
|
|
port match the pattern, the switch machine is not allowed to be got
|
|
by api.
|
|
If not filter is matched, if the last filter is allow, deny all
|
|
unmatched switch machines, if the last filter is deny, allow all
|
|
unmatched switch machines.
|
|
If no filter defined, allow all switch machines.
|
|
if ports defined in filter and 'all' in ports, the switch machine is
|
|
matched. if ports defined in filter and 'all' not in ports,
|
|
the switch machine with the port name in ports will be matched.
|
|
If the port pattern matches
|
|
<<port_prefix><port_number><port_suffix> and port number is in the
|
|
range of [port_start, port_end], the switch machine is matched.
|
|
"""
|
|
filters = self.switch.machine_filters
|
|
port = self.port
|
|
unmatched_allowed = True
|
|
ports_pattern = re.compile(r'(\D*)(\d+)-(\d+)(\D*)')
|
|
port_pattern = re.compile(r'(\D*)(\d+)(\D*)')
|
|
port_match = port_pattern.match(port)
|
|
if port_match:
|
|
port_prefix = port_match.group(1)
|
|
port_number = int(port_match.group(2))
|
|
port_suffix = port_match.group(3)
|
|
else:
|
|
port_prefix = ''
|
|
port_number = 0
|
|
port_suffix = ''
|
|
for port_filter in filters:
|
|
filter_type = port_filter.get('filter_type', 'allow')
|
|
denied = filter_type != 'allow'
|
|
unmatched_allowed = denied
|
|
if 'ports' in port_filter:
|
|
if 'all' in port_filter['ports']:
|
|
return denied
|
|
if port in port_filter['ports']:
|
|
return denied
|
|
if port_match:
|
|
for port_or_ports in port_filter['ports']:
|
|
ports_match = ports_pattern.match(port_or_ports)
|
|
if ports_match:
|
|
filter_port_prefix = ports_match.group(1)
|
|
filter_port_start = int(ports_match.group(2))
|
|
filter_port_end = int(ports_match.group(3))
|
|
filter_port_suffix = ports_match.group(4)
|
|
if (
|
|
filter_port_prefix == port_prefix and
|
|
filter_port_suffix == port_suffix and
|
|
filter_port_start <= port_number and
|
|
port_number <= filter_port_end
|
|
):
|
|
return denied
|
|
else:
|
|
filter_port_prefix = port_filter.get('port_prefix', '')
|
|
filter_port_suffix = port_filter.get('port_suffix', '')
|
|
if (
|
|
port_match and
|
|
port_prefix == filter_port_prefix and
|
|
port_suffix == filter_port_suffix
|
|
):
|
|
if (
|
|
'port_start' not in port_filter or
|
|
port_number >= port_filter['port_start']
|
|
) and (
|
|
'port_end' not in port_filter or
|
|
port_number <= port_filter['port_end']
|
|
):
|
|
return denied
|
|
return not unmatched_allowed
|
|
|
|
def to_dict(self):
|
|
dict_info = self.machine.to_dict()
|
|
dict_info.update(super(SwitchMachine, self).to_dict())
|
|
dict_info['switch_ip'] = self.switch.ip
|
|
return dict_info
|
|
|
|
|
|
class Machine(BASE, HelperMixin, TimestampMixin):
|
|
"""Machine table."""
|
|
__tablename__ = 'machine'
|
|
id = Column(Integer, primary_key=True)
|
|
mac = Column(String(24), unique=True, nullable=False)
|
|
ipmi_credentials = Column(JSONEncoded, default={})
|
|
tag = Column(JSONEncoded, default={})
|
|
location = Column(JSONEncoded, default={})
|
|
owner_id = Column(Integer, ForeignKey('user.id'))
|
|
machine_attributes = Column(JSONEncoded, default={})
|
|
|
|
switch_machines = relationship(
|
|
SwitchMachine,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('machine')
|
|
)
|
|
host = relationship(
|
|
Host,
|
|
uselist=False,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('machine')
|
|
)
|
|
|
|
def __init__(self, mac, **kwargs):
|
|
self.mac = mac
|
|
super(Machine, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'Machine[%s:%s]' % (self.id, self.mac)
|
|
|
|
def validate(self):
|
|
# TODO(xicheng): some validation can be moved to column.
|
|
super(Machine, self).validate()
|
|
try:
|
|
netaddr.EUI(self.mac)
|
|
except Exception:
|
|
raise exception.InvalidParameter(
|
|
'mac address %s format uncorrect' % self.mac
|
|
)
|
|
|
|
@property
|
|
def patched_ipmi_credentials(self):
|
|
return self.ipmi_credentials
|
|
|
|
@patched_ipmi_credentials.setter
|
|
def patched_ipmi_credentials(self, value):
|
|
if not value:
|
|
return
|
|
ipmi_credentials = copy.deepcopy(self.ipmi_credentials)
|
|
self.ipmi_credentials = util.merge_dict(ipmi_credentials, value)
|
|
|
|
@property
|
|
def patched_tag(self):
|
|
return self.tag
|
|
|
|
@patched_tag.setter
|
|
def patched_tag(self, value):
|
|
if not value:
|
|
return
|
|
tag = copy.deepcopy(self.tag)
|
|
tag.update(value)
|
|
self.tag = value
|
|
|
|
@property
|
|
def patched_location(self):
|
|
return self.location
|
|
|
|
@patched_location.setter
|
|
def patched_location(self, value):
|
|
if not value:
|
|
return
|
|
location = copy.deepcopy(self.location)
|
|
location.update(value)
|
|
self.location = location
|
|
|
|
def to_dict(self):
|
|
# TODO(xicheng): move the filling of switches
|
|
# to db/api.
|
|
dict_info = {}
|
|
dict_info['switches'] = [
|
|
{
|
|
'switch_ip': switch_machine.switch_ip,
|
|
'port': switch_machine.port,
|
|
'vlans': switch_machine.vlans
|
|
}
|
|
for switch_machine in self.switch_machines
|
|
if not switch_machine.filtered
|
|
]
|
|
if dict_info['switches']:
|
|
dict_info.update(dict_info['switches'][0])
|
|
dict_info.update(super(Machine, self).to_dict())
|
|
return dict_info
|
|
|
|
|
|
class Switch(BASE, HelperMixin, TimestampMixin):
|
|
"""Switch table."""
|
|
__tablename__ = 'switch'
|
|
id = Column(Integer, primary_key=True)
|
|
ip_int = Column('ip', BigInteger, unique=True, nullable=False)
|
|
credentials = Column(JSONEncoded, default={})
|
|
vendor = Column(String(256), nullable=True)
|
|
state = Column(Enum('initialized', 'unreachable', 'notsupported',
|
|
'repolling', 'error', 'under_monitoring',
|
|
name='switch_state'),
|
|
ColumnDefault('initialized'))
|
|
# filters is json formatted list, each element has following format:
|
|
# keys: ['filter_type', 'ports', 'port_prefix', 'port_suffix',
|
|
# 'port_start', 'port_end'].
|
|
# each port name is divided into <port_prefix><port_number><port_suffix>
|
|
# filter_type is one of ['allow', 'deny'], default is 'allow'
|
|
# ports is a list of port name.
|
|
# port_prefix is the prefix that filtered port should start with.
|
|
# port_suffix is the suffix that filtered posrt should end with.
|
|
# port_start is integer that the port number should start with.
|
|
# port_end is the integer that the port number should end with.
|
|
_filters = Column('filters', JSONEncoded, default=[])
|
|
switch_machines = relationship(
|
|
SwitchMachine,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('switch')
|
|
)
|
|
|
|
def __str__(self):
|
|
return 'Switch[%s:%s]' % (self.id, self.ip)
|
|
|
|
@classmethod
|
|
def parse_filters(cls, filters):
|
|
"""parse filters set from outside to standard format.
|
|
|
|
api can set switch filters with the flexible format, this
|
|
function will parse the flexible format filters.
|
|
|
|
Supported format:
|
|
as string:
|
|
allow ports ae10,ae20
|
|
allow port_prefix ae port_start 30 port_end 40
|
|
deny ports all
|
|
as python object:
|
|
[{
|
|
'filter_type': 'allow',
|
|
'ports': ['ae10', 'ae20']
|
|
},{
|
|
'filter_type': 'allow',
|
|
'port_prefix': 'ae',
|
|
'port_suffix': '',
|
|
'port_start': 30,
|
|
'port_end': 40
|
|
},{
|
|
'filter_type': 'deny',
|
|
'ports': ['all']
|
|
}]
|
|
"""
|
|
if isinstance(filters, basestring):
|
|
filters = filters.replace('\r\n', '\n').replace('\n', ';')
|
|
filters = [
|
|
machine_filter for machine_filter in filters.split(';')
|
|
if machine_filter
|
|
]
|
|
if not isinstance(filters, list):
|
|
filters = [filters]
|
|
machine_filters = []
|
|
for machine_filter in filters:
|
|
if not machine_filter:
|
|
continue
|
|
if isinstance(machine_filter, basestring):
|
|
filter_dict = {}
|
|
filter_items = [
|
|
item for item in machine_filter.split() if item
|
|
]
|
|
if filter_items[0] in ['allow', 'deny']:
|
|
filter_dict['filter_type'] = filter_items[0]
|
|
filter_items = filter_items[1:]
|
|
elif filter_items[0] not in [
|
|
'ports', 'port_prefix', 'port_suffix',
|
|
'port_start', 'port_end'
|
|
]:
|
|
raise exception.InvalidParameter(
|
|
'unrecognized filter type %s' % filter_items[0]
|
|
)
|
|
while filter_items:
|
|
if len(filter_items) >= 2:
|
|
filter_dict[filter_items[0]] = filter_items[1]
|
|
filter_items = filter_items[2:]
|
|
else:
|
|
filter_dict[filter_items[0]] = ''
|
|
filter_items = filter_items[1:]
|
|
machine_filter = filter_dict
|
|
if not isinstance(machine_filter, dict):
|
|
raise exception.InvalidParameter(
|
|
'filter %s is not dict' % machine_filter
|
|
)
|
|
if 'filter_type' in machine_filter:
|
|
if machine_filter['filter_type'] not in ['allow', 'deny']:
|
|
raise exception.InvalidParameter(
|
|
'filter_type should be `allow` or `deny` in %s' % (
|
|
machine_filter
|
|
)
|
|
)
|
|
if 'ports' in machine_filter:
|
|
if isinstance(machine_filter['ports'], basestring):
|
|
machine_filter['ports'] = [
|
|
port_or_ports
|
|
for port_or_ports in machine_filter['ports'].split(',')
|
|
if port_or_ports
|
|
]
|
|
if not isinstance(machine_filter['ports'], list):
|
|
raise exception.InvalidParameter(
|
|
'`ports` type is not list in filter %s' % (
|
|
machine_filter
|
|
)
|
|
)
|
|
for port_or_ports in machine_filter['ports']:
|
|
if not isinstance(port_or_ports, basestring):
|
|
raise exception.InvalidParameter(
|
|
'%s type is not basestring in `ports` %s' % (
|
|
port_or_ports, machine_filter['ports']
|
|
)
|
|
)
|
|
for key in ['port_start', 'port_end']:
|
|
if key in machine_filter:
|
|
if isinstance(machine_filter[key], basestring):
|
|
if machine_filter[key].isdigit():
|
|
machine_filter[key] = int(machine_filter[key])
|
|
if not isinstance(machine_filter[key], (int, long)):
|
|
raise exception.InvalidParameter(
|
|
'`%s` type is not int in filer %s' % (
|
|
key, machine_filter
|
|
)
|
|
)
|
|
machine_filters.append(machine_filter)
|
|
return machine_filters
|
|
|
|
@classmethod
|
|
def format_filters(cls, filters):
|
|
"""format json formatted filters to string."""
|
|
filter_strs = []
|
|
for machine_filter in filters:
|
|
filter_properties = []
|
|
filter_properties.append(
|
|
machine_filter.get('filter_type', 'allow')
|
|
)
|
|
if 'ports' in machine_filter:
|
|
filter_properties.append(
|
|
'ports ' + ','.join(machine_filter['ports'])
|
|
)
|
|
if 'port_prefix' in machine_filter:
|
|
filter_properties.append(
|
|
'port_prefix ' + machine_filter['port_prefix']
|
|
)
|
|
if 'port_suffix' in machine_filter:
|
|
filter_properties.append(
|
|
'port_suffix ' + machine_filter['port_suffix']
|
|
)
|
|
if 'port_start' in machine_filter:
|
|
filter_properties.append(
|
|
'port_start ' + str(machine_filter['port_start'])
|
|
)
|
|
if 'port_end' in machine_filter:
|
|
filter_properties.append(
|
|
'port_end ' + str(machine_filter['port_end'])
|
|
)
|
|
filter_strs.append(' '.join(filter_properties))
|
|
return ';'.join(filter_strs)
|
|
|
|
def __init__(self, ip_int, **kwargs):
|
|
self.ip_int = ip_int
|
|
super(Switch, self).__init__(**kwargs)
|
|
|
|
@property
|
|
def ip(self):
|
|
return str(netaddr.IPAddress(self.ip_int))
|
|
|
|
@ip.setter
|
|
def ip(self, ipaddr):
|
|
self.ip_int = int(netaddr.IPAddress(ipaddr))
|
|
|
|
@property
|
|
def patched_credentials(self):
|
|
return self.credentials
|
|
|
|
@patched_credentials.setter
|
|
def patched_credentials(self, value):
|
|
if not value:
|
|
return
|
|
credentials = copy.deepcopy(self.credentials)
|
|
self.credentials = util.merge_dict(credentials, value)
|
|
|
|
@property
|
|
def machine_filters(self):
|
|
return self._filters
|
|
|
|
@machine_filters.setter
|
|
def machine_filters(self, value):
|
|
if not value:
|
|
return
|
|
self._filters = self.parse_filters(value)
|
|
|
|
@property
|
|
def put_machine_filters(self):
|
|
return self._filters
|
|
|
|
@put_machine_filters.setter
|
|
def put_machine_filters(self, value):
|
|
if not value:
|
|
return
|
|
self._filters = self.parse_filters(value)
|
|
|
|
@property
|
|
def patched_machine_filters(self):
|
|
return self._filters
|
|
|
|
@patched_machine_filters.setter
|
|
def patched_machine_filters(self, value):
|
|
if not value:
|
|
return
|
|
filters = list(self.machine_filters)
|
|
self._filters = self.parse_filters(value) + filters
|
|
|
|
def to_dict(self):
|
|
dict_info = super(Switch, self).to_dict()
|
|
dict_info['ip'] = self.ip
|
|
dict_info['filters'] = self.format_filters(self._filters)
|
|
return dict_info
|
|
|
|
|
|
class Subnet(BASE, TimestampMixin, HelperMixin):
|
|
"""network table."""
|
|
__tablename__ = 'subnet'
|
|
|
|
id = Column(Integer, primary_key=True)
|
|
name = Column(String(80), unique=True, nullable=True)
|
|
subnet = Column(String(80), unique=True, nullable=False)
|
|
|
|
host_networks = relationship(
|
|
HostNetwork,
|
|
passive_deletes=True, passive_updates=True,
|
|
cascade='all, delete-orphan',
|
|
backref=backref('subnet')
|
|
)
|
|
|
|
def __init__(self, subnet, **kwargs):
|
|
self.subnet = subnet
|
|
super(Subnet, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'Subnet[%s:%s]' % (self.id, self.subnet)
|
|
|
|
def to_dict(self):
|
|
dict_info = super(Subnet, self).to_dict()
|
|
if not self.name:
|
|
dict_info['name'] = self.subnet
|
|
return dict_info
|
|
|
|
|
|
# TODO(grace): move this global variable into HealthCheckReport.
|
|
HEALTH_REPORT_STATES = ('verifying', 'success', 'finished', 'error')
|
|
|
|
|
|
class HealthCheckReport(BASE, HelperMixin):
|
|
"""Health check report table."""
|
|
__tablename__ = 'health_check_report'
|
|
|
|
cluster_id = Column(
|
|
Integer,
|
|
ForeignKey('cluster.id', onupdate='CASCADE', ondelete='CASCADE'),
|
|
primary_key=True
|
|
)
|
|
name = Column(String(80), nullable=False, primary_key=True)
|
|
display_name = Column(String(100))
|
|
report = Column(JSONEncoded, default={})
|
|
category = Column(String(80), default='')
|
|
state = Column(
|
|
Enum(*HEALTH_REPORT_STATES, name='report_state'),
|
|
ColumnDefault('verifying'),
|
|
nullable=False
|
|
)
|
|
error_message = Column(Text, default='')
|
|
|
|
def __init__(self, cluster_id, name, **kwargs):
|
|
self.cluster_id = cluster_id
|
|
self.name = name
|
|
if 'state' in kwargs and kwargs['state'] not in HEALTH_REPORT_STATES:
|
|
err_msg = 'State value %s is not accepted.' % kwargs['state']
|
|
raise exception.InvalidParameter(err_msg)
|
|
|
|
super(HealthCheckReport, self).__init__(**kwargs)
|
|
|
|
def __str__(self):
|
|
return 'HealthCheckReport[cluster_id: %s, name: %s]' % (
|
|
self.cluster_id, self.name
|
|
)
|