444 lines
14 KiB
Python
444 lines
14 KiB
Python
# Copyright 2013 - 2016 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 functools
|
|
import warnings
|
|
|
|
from django.db import models
|
|
from django.utils import functional
|
|
import six
|
|
|
|
from devops import error
|
|
from devops.helpers import helpers
|
|
from devops.helpers import loader
|
|
from devops.helpers import ssh_client
|
|
from devops import logger
|
|
from devops.models import base
|
|
from devops.models import network
|
|
from devops.models import volume
|
|
|
|
|
|
class ExtendableNodeType(base.ParamedModelType):
|
|
"""Atomatically installs hooks on Node subclasses
|
|
|
|
This class dynamically installs hooks for specified methods,
|
|
to invoke pre_* and post_* methods from node role extensions (if such
|
|
methods exist).
|
|
|
|
The following methods with custom logic can be added to the node
|
|
role extensions:
|
|
|
|
def pre_define(self):
|
|
def post_define(self):
|
|
def pre_start(self):
|
|
def post_start(self):
|
|
def pre_destroy(self):
|
|
def post_destroy(self):
|
|
def pre_remove(self):
|
|
def post_remove(self):
|
|
|
|
For example, if some method should be called *before* each invocation of
|
|
Node.start() for the role 'fuel_slave', then:
|
|
- add a pre_start(self) method to
|
|
the devops.models.node_ext.fuel_slave module
|
|
- Every time when Node.start() invoked for the role 'fuel_slave',
|
|
execution will be performed like the following (simplified
|
|
explanation):
|
|
|
|
.. code-block::
|
|
|
|
def hook(func):
|
|
@functools.wraps(func)
|
|
def wrapper(*args, **kwargs):
|
|
Node.ext.pre_start() # <- method that was added
|
|
result = func(*args, **kwargs)
|
|
# Node.ext.post_start() # post_start() wasn't added to the
|
|
# node role extension, so in this
|
|
# case will be called 'dumb' method
|
|
# that do nothing.
|
|
return result
|
|
return wrapper
|
|
|
|
@hook # This hook is dynamically installed for each instance of Node
|
|
# depending on the role.
|
|
def start(self):
|
|
...
|
|
"""
|
|
|
|
METHOD_NAMES = ('define', 'start', 'destroy', 'remove')
|
|
|
|
# pylint: disable=bad-mcs-classmethod-argument
|
|
# noinspection PyMethodParameters
|
|
def __new__(cls, name, bases, attrs):
|
|
super_new = super(ExtendableNodeType, cls).__new__
|
|
|
|
# skip if not a Node subclass
|
|
if 'Node' not in [c.__name__ for c in bases]:
|
|
return super_new(cls, name, bases, attrs)
|
|
|
|
# add method to subclass if there is no such
|
|
for attr_name in cls.METHOD_NAMES:
|
|
if attr_name in attrs:
|
|
continue
|
|
# if there is no method in subclass
|
|
# then we can't install hooks
|
|
attrs[attr_name] = cls._create_method(attr_name)
|
|
|
|
# install ext hooks on Node subclasses
|
|
for attr_name in attrs:
|
|
if attr_name not in cls.METHOD_NAMES:
|
|
continue
|
|
attrs[attr_name] = cls._install_ext_hook(attrs[attr_name])
|
|
|
|
return super_new(cls, name, bases, attrs)
|
|
|
|
# pylint: enable=bad-mcs-classmethod-argument
|
|
|
|
@staticmethod
|
|
def _install_ext_hook(node_method):
|
|
"""Installs pre/post hooks on Node method"""
|
|
|
|
@functools.wraps(node_method)
|
|
def wrapper(*args, **kwargs):
|
|
node = args[0]
|
|
name = node_method.__name__
|
|
|
|
pre_method = getattr(node.ext, 'pre_{}'.format(name), None)
|
|
post_method = getattr(node.ext, 'post_{}'.format(name), None)
|
|
|
|
if pre_method is not None:
|
|
pre_method()
|
|
|
|
result = node_method(*args, **kwargs)
|
|
|
|
if post_method is not None:
|
|
post_method()
|
|
|
|
return result
|
|
|
|
return wrapper
|
|
|
|
@staticmethod
|
|
def _create_method(name):
|
|
"""Creates a simple method which just calls super method"""
|
|
def method(self, *args, **kwargs):
|
|
return getattr(super(self.__class__, self), name)(*args, **kwargs)
|
|
method.__name__ = name
|
|
return method
|
|
|
|
|
|
class Node(
|
|
six.with_metaclass(
|
|
ExtendableNodeType,
|
|
base.ParamedModel,
|
|
base.BaseModel)):
|
|
class Meta(object):
|
|
unique_together = ('name', 'group')
|
|
db_table = 'devops_node'
|
|
app_label = 'devops'
|
|
|
|
group = models.ForeignKey('Group', null=True)
|
|
name = models.CharField(max_length=255, unique=False, null=False)
|
|
role = models.CharField(max_length=255, null=True)
|
|
|
|
kernel_cmd = base.ParamField()
|
|
ssh_port = base.ParamField(default=22)
|
|
bootstrap_timeout = base.ParamField(default=600)
|
|
deploy_timeout = base.ParamField(default=3600)
|
|
deploy_check_cmd = base.ParamField()
|
|
|
|
@property
|
|
def driver(self):
|
|
drv = self.group.driver
|
|
|
|
# LEGACY (fuel-qa compatibility requires), TO REMOVE
|
|
def node_active(node):
|
|
return node.is_active()
|
|
drv.node_active = node_active
|
|
|
|
return drv
|
|
|
|
@functional.cached_property
|
|
def ext(self):
|
|
try:
|
|
# noinspection PyPep8Naming
|
|
ExtCls = loader.load_class(
|
|
'devops.models.node_ext.{ext_name}:NodeExtension'
|
|
''.format(ext_name=self.role or 'default'))
|
|
return ExtCls(node=self)
|
|
except ImportError:
|
|
logger.debug('NodeExtension is not found for role: {!r}'
|
|
''.format(self.role))
|
|
return None
|
|
|
|
def define(self, *args, **kwargs):
|
|
for iface in self.interfaces:
|
|
iface.define()
|
|
self.save()
|
|
|
|
def start(self, *args, **kwargs):
|
|
pass
|
|
|
|
def destroy(self, *args, **kwargs):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
def erase(self, *args, **kwargs):
|
|
self.remove()
|
|
|
|
def remove(self, *args, **kwargs):
|
|
ssh_client.SSHClient.close_connections()
|
|
self.erase_volumes()
|
|
for iface in self.interfaces:
|
|
iface.remove()
|
|
self.delete()
|
|
|
|
def suspend(self, *args, **kwargs):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
def resume(self, *args, **kwargs):
|
|
pass
|
|
|
|
def is_active(self):
|
|
return False
|
|
|
|
def snapshot(self, *args, **kwargs):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
def revert(self, *args, **kwargs):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
# for fuel-qa compatibility
|
|
def has_snapshot(self, *args, **kwargs):
|
|
return True
|
|
|
|
def reboot(self):
|
|
pass
|
|
|
|
def shutdown(self):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
def reset(self):
|
|
ssh_client.SSHClient.close_connections()
|
|
|
|
def get_vnc_port(self):
|
|
return None
|
|
|
|
# for fuel-qa compatibility
|
|
def get_snapshots(self):
|
|
"""Return full snapshots objects"""
|
|
return []
|
|
|
|
@property
|
|
def disk_devices(self):
|
|
return self.diskdevice_set.all()
|
|
|
|
@property
|
|
def interfaces(self):
|
|
return self.interface_set.order_by('id')
|
|
|
|
@property
|
|
def network_configs(self):
|
|
return self.networkconfig_set.all()
|
|
|
|
# LEGACY, for fuel-qa compatibility
|
|
@property
|
|
def is_admin(self):
|
|
return 'master' in str(self.role)
|
|
|
|
# LEGACY, for fuel-qa compatibility
|
|
@property
|
|
def is_slave(self):
|
|
return self.role == 'fuel_slave'
|
|
|
|
def next_disk_name(self):
|
|
disk_names = ('sd' + c for c in list('abcdefghijklmnopqrstuvwxyz'))
|
|
for disk_name in disk_names:
|
|
if not self.disk_devices.filter(target_dev=disk_name).exists():
|
|
return disk_name
|
|
|
|
# TODO(astudenov): LEGACY, TO REMOVE
|
|
def interface_by_network_name(self, network_name):
|
|
logger.warning('interface_by_network_name is deprecated in favor of '
|
|
'get_interface_by_network_name')
|
|
warnings.warn(
|
|
"'Node.interface_by_network_name' is deprecated. "
|
|
"Use 'Node.get_interface_by_network_name' instead.",
|
|
DeprecationWarning
|
|
)
|
|
return self.get_interface_by_network_name(network_name=network_name)
|
|
|
|
def get_interface_by_network_name(self, network_name):
|
|
return self.interface_set.get(
|
|
l2_network_device__name=network_name)
|
|
|
|
def get_interface_by_nailgun_network_name(self, name):
|
|
for net_conf in self.networkconfig_set.all():
|
|
if name in net_conf.networks:
|
|
label = net_conf.label
|
|
break
|
|
else:
|
|
return None
|
|
return self.interface_set.get(label=label)
|
|
|
|
def get_ip_address_by_network_name(self, name, interface=None):
|
|
interface = interface or self.interface_set.filter(
|
|
l2_network_device__name=name).order_by('id')[0]
|
|
return interface.address_set.get(interface=interface).ip_address
|
|
|
|
# NOTE: this method works only for Fuel master node
|
|
def get_ip_address_by_nailgun_network_name(self, name):
|
|
interface = self.get_interface_by_nailgun_network_name(name)
|
|
return interface.address_set.first().ip_address
|
|
|
|
# LEGACY
|
|
def remote(
|
|
self, network_name, login=None, password=None, private_keys=None,
|
|
auth=None):
|
|
"""Create SSH-connection to the network
|
|
|
|
NOTE: this method works only for master node
|
|
|
|
:rtype : SSHClient
|
|
"""
|
|
return ssh_client.SSHClient(
|
|
self.get_ip_address_by_network_name(network_name),
|
|
username=login,
|
|
password=password, private_keys=private_keys, auth=auth)
|
|
|
|
# LEGACY
|
|
def await(self, network_name, timeout=120, by_port=22):
|
|
helpers.wait_pass(
|
|
lambda: helpers.tcp_ping_(
|
|
self.get_ip_address_by_network_name(network_name), by_port),
|
|
timeout=timeout)
|
|
|
|
# NEW
|
|
def add_interfaces(self, interfaces):
|
|
for interface in interfaces:
|
|
label = interface['label']
|
|
l2_network_device_name = interface.get('l2_network_device')
|
|
interface_model = interface.get('interface_model', 'virtio')
|
|
mac_address = interface.get('mac_address')
|
|
features = interface.get('features', None)
|
|
self.add_interface(
|
|
label=label,
|
|
l2_network_device_name=l2_network_device_name,
|
|
mac_address=mac_address,
|
|
interface_model=interface_model,
|
|
features=features)
|
|
|
|
# NEW
|
|
def add_interface(self, label, l2_network_device_name,
|
|
interface_model, mac_address=None,
|
|
features=None):
|
|
if l2_network_device_name:
|
|
env = self.group.environment
|
|
l2_network_device = env.get_env_l2_network_device(
|
|
name=l2_network_device_name)
|
|
else:
|
|
l2_network_device = None
|
|
|
|
cls = self.driver.get_model_class('Interface')
|
|
return cls.interface_create(
|
|
node=self,
|
|
label=label,
|
|
l2_network_device=l2_network_device,
|
|
mac_address=mac_address,
|
|
model=interface_model,
|
|
features=features,
|
|
)
|
|
|
|
# NEW
|
|
def add_network_configs(self, network_configs):
|
|
for label, data in network_configs.items():
|
|
self.add_network_config(
|
|
label=label,
|
|
networks=data.get('networks', []),
|
|
aggregation=data.get('aggregation'),
|
|
parents=data.get('parents', []),
|
|
)
|
|
|
|
# NEW
|
|
def add_network_config(self, label, networks=None, aggregation=None,
|
|
parents=None):
|
|
if networks is None:
|
|
networks = []
|
|
if parents is None:
|
|
parents = []
|
|
network.NetworkConfig.objects.create(
|
|
node=self,
|
|
label=label,
|
|
networks=networks,
|
|
aggregation=aggregation,
|
|
parents=parents,
|
|
)
|
|
|
|
# NEW
|
|
def add_volumes(self, volumes):
|
|
for vol_params in volumes:
|
|
self.add_volume(
|
|
**vol_params
|
|
)
|
|
|
|
# NEW
|
|
def add_volume(self, name, device='disk', bus='virtio', **params):
|
|
cls = self.driver.get_model_class('Volume')
|
|
|
|
if 'backing_store' in params:
|
|
# Backing storage volume have to be defined in group
|
|
params['backing_store'] = self.group.get_volume(
|
|
name=params['backing_store'])
|
|
|
|
volume = cls.objects.create(
|
|
node=self,
|
|
name=name,
|
|
**params
|
|
)
|
|
# TODO(astudenov): make a separete section in template for disk devices
|
|
self.attach_volume(
|
|
volume=volume,
|
|
device=device,
|
|
bus=bus,
|
|
)
|
|
return volume
|
|
|
|
# NEW
|
|
def attach_volume(self, volume, device='disk', type='file',
|
|
bus='virtio', target_dev=None):
|
|
"""Attach volume to node
|
|
|
|
:rtype : DiskDevice
|
|
"""
|
|
cls = self.driver.get_model_class('DiskDevice')
|
|
return cls.objects.create(
|
|
device=device, type=type, bus=bus,
|
|
target_dev=target_dev or self.next_disk_name(),
|
|
volume=volume, node=self)
|
|
|
|
# NEW
|
|
def get_volume(self, **kwargs):
|
|
try:
|
|
return self.volume_set.get(**kwargs)
|
|
except volume.Volume.DoesNotExist:
|
|
raise error.DevopsObjNotFound(volume.Volume, **kwargs)
|
|
|
|
# NEW
|
|
def get_volumes(self, **kwargs):
|
|
return self.volume_set.filter(**kwargs).order_by('id')
|
|
|
|
# NEW
|
|
def erase_volumes(self):
|
|
for vol in self.get_volumes():
|
|
vol.erase()
|