fuel-devops/devops/models.py

448 lines
14 KiB
Python

# Copyright 2013 - 2014 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.
from datetime import datetime
import json
from django.conf import settings
from django.db import models
from django.utils.importlib import import_module
from ipaddr import IPNetwork
from devops.helpers.helpers import _tcp_ping
from devops.helpers.helpers import _wait
from devops.helpers.helpers import SSHClient
from devops import logger
def choices(*args, **kwargs):
defaults = {'max_length': 255, 'null': False}
defaults.update(kwargs)
defaults.update(choices=double_tuple(*args))
return models.CharField(**defaults)
def double_tuple(*args):
dict = []
for arg in args:
dict.append((arg, arg))
return tuple(dict)
class DriverModel(models.Model):
_driver = None
created = models.DateTimeField(auto_now_add=True, default=datetime.utcnow)
class Meta:
abstract = True
@classmethod
def get_driver(cls):
"""Get driver
:rtype : DevopsDriver
"""
driver = import_module(settings.DRIVER)
cls._driver = cls._driver or driver.DevopsDriver(
**settings.DRIVER_PARAMETERS)
return cls._driver
@property
def driver(self):
"""Driver object
:rtype : DevopsDriver
"""
return self.get_driver()
class Environment(DriverModel):
name = models.CharField(max_length=255, unique=True, null=False)
@property
def volumes(self):
return Volume.objects.filter(environment=self)
@property
def networks(self):
return Network.objects.filter(environment=self)
@property
def nodes(self):
return Node.objects.filter(environment=self)
def node_by_name(self, name):
return self.nodes.get(name=name, environment=self)
def nodes_by_role(self, role):
return self.nodes.filter(role=role, environment=self)
def network_by_name(self, name):
return self.networks.get(name=name, environment=self)
def has_snapshot(self, name):
return all(map(lambda x: x.has_snapshot(name), self.nodes))
def define(self):
for network in self.networks:
network.define()
for volume in self.volumes:
volume.define()
for node in self.nodes:
node.define()
def start(self, nodes=None):
for network in self.networks:
network.start()
for node in nodes or self.nodes:
node.start()
def destroy(self, verbose=False):
for node in self.nodes:
node.destroy(verbose=verbose)
def erase(self):
for node in self.nodes:
node.erase()
for network in self.networks:
network.erase()
for volume in self.volumes:
volume.erase()
self.delete()
@classmethod
def erase_empty(cls):
for env in cls.objects.all():
if len(env.nodes) == 0:
env.delete()
def suspend(self, verbose=False):
for node in self.nodes:
node.suspend(verbose)
def resume(self, verbose=False):
for node in self.nodes:
node.resume(verbose)
def snapshot(self, name=None, description=None, force=False):
for node in self.nodes:
node.snapshot(name=name, description=description, force=force)
def revert(self, name=None, destroy=True, flag=True):
if destroy:
for node in self.nodes:
node.destroy(verbose=False)
if (flag and
not all([node.has_snapshot(name) for node in self.nodes])):
raise Exception("some nodes miss snapshot,"
" test should be interrupted")
for node in self.nodes:
node.revert(name, destroy=False)
@classmethod
def synchronize_all(cls):
driver = cls.get_driver()
nodes = {driver._get_name(e.name, n.name): n
for e in cls.objects.all()
for n in e.nodes}
domains = set(driver.node_list())
# FIXME (AWoodward) This willy nilly wacks domains when you run this
# on domains that are outside the scope of devops, if anything this
# should cause domains to be imported into db instead of undefined.
# It also leaves network and volumes around too
# Disabled untill a safer implmentation arrives
# Undefine domains without devops nodes
#
# domains_to_undefine = domains - set(nodes.keys())
# for d in domains_to_undefine:
# driver.node_undefine_by_name(d)
# Remove devops nodes without domains
nodes_to_remove = set(nodes.keys()) - domains
for n in nodes_to_remove:
nodes[n].delete()
cls.erase_empty()
logger.info('Undefined domains: %s, removed nodes: %s' %
(0, len(nodes_to_remove)))
class ExternalModel(DriverModel):
name = models.CharField(max_length=255, unique=False, null=False)
uuid = models.CharField(max_length=255)
environment = models.ForeignKey(Environment, null=True)
class Meta:
abstract = True
unique_together = ('name', 'environment')
@classmethod
def get_allocated_networks(cls):
return cls.get_driver().get_allocated_networks()
class Network(ExternalModel):
_iterhosts = None
has_dhcp_server = models.BooleanField()
has_pxe_server = models.BooleanField()
has_reserved_ips = models.BooleanField(default=True)
tftp_root_dir = models.CharField(max_length=255)
forward = choices(
'nat', 'route', 'bridge', 'private', 'vepa',
'passthrough', 'hostdev', null=True)
ip_network = models.CharField(max_length=255, unique=True)
@property
def interfaces(self):
return Interface.objects.filter(network=self)
@property
def ip_pool_start(self):
return IPNetwork(self.ip_network)[2]
@property
def ip_pool_end(self):
return IPNetwork(self.ip_network)[-2]
def next_ip(self):
while True:
self._iterhosts = self._iterhosts or IPNetwork(
self.ip_network).iterhosts()
ip = self._iterhosts.next()
if ip < self.ip_pool_start or ip > self.ip_pool_end:
continue
if not Address.objects.filter(
interface__network=self,
ip_address=str(ip)).exists():
return ip
def bridge_name(self):
return self.driver.network_bridge_name(self)
def define(self):
self.driver.network_define(self)
self.save()
def start(self):
self.create(verbose=False)
def create(self, verbose=False):
if verbose or not self.driver.network_active(self):
self.driver.network_create(self)
def destroy(self):
self.driver.network_destroy(self)
def erase(self):
self.remove(verbose=False)
def remove(self, verbose=False):
if verbose or self.uuid:
if verbose or self.driver.network_exists(self):
if self.driver.network_active(self):
self.driver.network_destroy(self)
self.driver.network_undefine(self)
self.delete()
class Node(ExternalModel):
hypervisor = choices('kvm')
os_type = choices('hvm')
architecture = choices('x86_64', 'i686')
boot = models.CharField(max_length=255, null=False, default=json.dumps([]))
metadata = models.CharField(max_length=255, null=True)
role = models.CharField(max_length=255, null=True)
vcpu = models.PositiveSmallIntegerField(null=False, default=1)
memory = models.IntegerField(null=False, default=1024)
has_vnc = models.BooleanField(null=False, default=True)
def next_disk_name(self):
disk_names = ('sd' + c for c in list('abcdefghijklmnopqrstuvwxyz'))
while True:
disk_name = disk_names.next()
if not self.disk_devices.filter(target_dev=disk_name).exists():
return disk_name
def get_vnc_port(self):
return self.driver.node_get_vnc_port(node=self)
@property
def disk_devices(self):
return DiskDevice.objects.filter(node=self)
@property
def interfaces(self):
return Interface.objects.filter(node=self).order_by('id')
@property
def vnc_password(self):
return settings.VNC_PASSWORD
def interface_by_name(self, name):
self.interfaces.filter(name=name)
def get_ip_address_by_network_name(self, name, interface=None):
interface = interface or Interface.objects.filter(
network__name=name, node=self).order_by('id')[0]
return Address.objects.get(interface=interface).ip_address
def remote(self, network_name, login, password=None, private_keys=None):
"""Create SSH-connection to the network
:rtype : SSHClient
"""
return SSHClient(
self.get_ip_address_by_network_name(network_name),
username=login,
password=password, private_keys=private_keys)
def send_keys(self, keys):
self.driver.node_send_keys(self, keys)
def await(self, network_name, timeout=120, by_port=22):
_wait(
lambda: _tcp_ping(
self.get_ip_address_by_network_name(network_name), by_port),
timeout=timeout)
def define(self):
self.driver.node_define(self)
self.save()
def start(self):
self.create(verbose=False)
def create(self, verbose=False):
if verbose or not self.driver.node_active(self):
self.driver.node_create(self)
def destroy(self, verbose=False):
if verbose or self.driver.node_active(self):
self.driver.node_destroy(self)
def erase(self):
self.remove(verbose=False)
def remove(self, verbose=False):
if verbose or self.uuid:
if verbose or self.driver.node_exists(self):
self.destroy(verbose=False)
self.driver.node_undefine(self, undefine_snapshots=True)
self.delete()
def suspend(self, verbose=False):
if verbose or self.driver.node_active(self):
self.driver.node_suspend(self)
def resume(self, verbose=False):
if verbose or self.driver.node_active(self):
self.driver.node_resume(self)
def has_snapshot(self, name):
return self.driver.node_snapshot_exists(node=self, name=name)
def snapshot(self, name=None, force=False, description=None):
if force and self.has_snapshot(name):
self.driver.node_delete_snapshot(node=self, name=name)
self.driver.node_create_snapshot(
node=self, name=name, description=description)
def revert(self, name=None, destroy=True):
if destroy:
self.destroy(verbose=False)
if self.has_snapshot(name):
self.driver.node_revert_snapshot(node=self, name=name)
else:
print('Domain snapshot for {0} node not found: no domain '
'snapshot with matching'
' name {1}'.format(self.name, name))
def get_snapshots(self):
return self.driver.node_get_snapshots(node=self)
def erase_snapshot(self, name):
self.driver.node_delete_snapshot(node=self, name=name)
class Volume(ExternalModel):
capacity = models.BigIntegerField(null=False)
backing_store = models.ForeignKey('self', null=True)
format = models.CharField(max_length=255, null=False)
def define(self):
self.driver.volume_define(self)
self.save()
def erase(self):
self.remove(verbose=False)
def remove(self, verbose=False):
if verbose or self.uuid:
if verbose or self.driver.volume_exists(self):
self.driver.volume_delete(self)
self.delete()
def get_capacity(self):
return self.driver.volume_capacity(self)
def get_format(self):
return self.driver.volume_format(self)
def get_path(self):
return self.driver.volume_path(self)
def fill_from_exist(self):
self.capacity = self.get_capacity()
self.format = self.get_format()
def upload(self, path):
self.driver.volume_upload(self, path)
class DiskDevice(models.Model):
device = choices('disk', 'cdrom')
type = choices('file')
bus = choices('virtio')
target_dev = models.CharField(max_length=255, null=False)
node = models.ForeignKey(Node, null=False)
volume = models.ForeignKey(Volume, null=True)
class Interface(models.Model):
mac_address = models.CharField(max_length=255, unique=True, null=False)
network = models.ForeignKey(Network)
node = models.ForeignKey(Node)
type = models.CharField(max_length=255, null=False)
model = choices('virtio', 'e1000', 'pcnet', 'rtl8139', 'ne2k_pci')
@property
def target_dev(self):
return self.node.driver.node_get_interface_target_dev(
self.node, self.mac_address)
@property
def addresses(self):
return Address.objects.filter(interface=self)
def add_address(self, address):
Address.objects.create(ip_address=address, interface=self)
class Address(models.Model):
ip_address = models.GenericIPAddressField()
interface = models.ForeignKey(Interface)