node.py 11.4 KB
Newer Older
1
from __future__ import absolute_import, unicode_literals
Őry Máté committed
2 3 4
from logging import getLogger

from django.db.models import (
5
    CharField, IntegerField, ForeignKey, BooleanField, ManyToManyField,
6
    FloatField, permalink,
Őry Máté committed
7 8 9 10 11 12 13
)
from django.utils.translation import ugettext_lazy as _

from celery.exceptions import TimeoutError
from model_utils.models import TimeStampedModel
from taggit.managers import TaggableManager

14
from common.models import method_cache, WorkerNotFound, HumanSortField
Őry Máté committed
15
from firewall.models import Host
Őry Máté committed
16
from ..tasks import vm_tasks, local_tasks
17
from .common import Trait
Őry Máté committed
18

19
from .activity import node_activity, NodeActivity
20

Gregory Nagy committed
21 22
from monitor.calvin.calvin import Query
from monitor.calvin.calvin import GraphiteHandler
23
from django.utils import timezone
24

Őry Máté committed
25 26 27
logger = getLogger(__name__)


28 29 30 31
def node_available(function):
    """Decorate methods to ignore disabled Nodes.
    """
    def decorate(self, *args, **kwargs):
32
        if self.enabled and self.online:
33 34 35 36 37 38
            return function(self, *args, **kwargs)
        else:
            return None
    return decorate


Őry Máté committed
39 40 41 42 43 44 45
class Node(TimeStampedModel):

    """A VM host machine, a hypervisor.
    """
    name = CharField(max_length=50, unique=True,
                     verbose_name=_('name'),
                     help_text=_('Human readable name of node.'))
46
    normalized_name = HumanSortField(monitor='name', max_length=100)
Őry Máté committed
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
    priority = IntegerField(verbose_name=_('priority'),
                            help_text=_('Node usage priority.'))
    host = ForeignKey(Host, verbose_name=_('host'),
                      help_text=_('Host in firewall.'))
    enabled = BooleanField(verbose_name=_('enabled'), default=False,
                           help_text=_('Indicates whether the node can '
                                       'be used for hosting.'))
    traits = ManyToManyField(Trait, blank=True,
                             help_text=_("Declared traits."),
                             verbose_name=_('traits'))
    tags = TaggableManager(blank=True, verbose_name=_("tags"))
    overcommit = FloatField(default=1.0, verbose_name=_("overcommit ratio"),
                            help_text=_("The ratio of total memory with "
                                        "to without overcommit."))

    class Meta:
        app_label = 'vm'
        db_table = 'vm_node'
        permissions = ()
66
        ordering = ('-enabled', 'normalized_name')
Őry Máté committed
67

Őry Máté committed
68 69 70
    def __unicode__(self):
        return self.name

71
    @method_cache(10)
72
    def get_online(self):
73
        """Check if the node is online.
Őry Máté committed
74

75
        Check if node is online by queue is available.
76 77
        """
        try:
78 79
            self.get_remote_queue_name("vm")
        except:
80
            return False
81 82
        else:
            return True
Őry Máté committed
83

84 85
    online = property(get_online)

86
    @node_available
Őry Máté committed
87
    @method_cache(300)
88
    def get_num_cores(self):
Őry Máté committed
89 90
        """Number of CPU threads available to the virtual machines.
        """
91

92
        return self.remote_query(vm_tasks.get_core_num, default=0)
93

94 95
    num_cores = property(get_num_cores)

96 97 98 99 100
    STATES = {False: {False: ('OFFLINE', _('offline')),
                      True: ('DISABLED', _('disabled'))},
              True: {False: ('MISSING', _('missing')),
                     True: ('ONLINE', _('online'))}}

Őry Máté committed
101
    def get_state(self):
102
        """The state combined of online and enabled attributes.
103
        """
104 105
        return self.STATES[self.enabled][self.online][0]

Őry Máté committed
106 107
    state = property(get_state)

108 109
    def get_status_display(self):
        return self.STATES[self.enabled][self.online][1]
110

111
    def disable(self, user=None, base_activity=None):
112
        ''' Disable the node.'''
113
        if self.enabled:
114 115 116 117 118
            if base_activity:
                act_ctx = base_activity.sub_activity('disable')
            else:
                act_ctx = node_activity('disable', node=self, user=user)
            with act_ctx:
119 120
                self.enabled = False
                self.save()
121

Őry Máté committed
122
    def flush(self, user=None, task_uuid=None):
Őry Máté committed
123 124
        """Disable node and move all instances to other ones.
        """
Őry Máté committed
125 126
        with node_activity('flush', node=self, user=user,
                           task_uuid=task_uuid) as act:
Őry Máté committed
127 128 129 130 131 132 133 134 135 136 137
            self.disable(user, act)
            for i in self.instance_set.all():
                with act.sub_activity('migrate_instance_%d' % i.pk):
                    i.migrate()

    def flush_async(self, user=None):
        """Execute flush asynchronously.
        """
        return local_tasks.flush.apply_async(args=[self, user],
                                             queue="localhost.man")

138 139
    def enable(self, user=None):
        ''' Enable the node. '''
140 141 142 143 144 145
        if self.enabled is not True:
            with node_activity(code_suffix='enable', node=self, user=user):
                self.enabled = True
                self.save()
            self.get_num_cores(invalidate_cache=True)
            self.get_ram_size(invalidate_cache=True)
Őry Máté committed
146

147
    @node_available
Őry Máté committed
148
    @method_cache(300)
149
    def get_ram_size(self):
Őry Máté committed
150 151
        """Bytes of total memory in the node.
        """
152
        return self.remote_query(vm_tasks.get_ram_size, default=0)
Őry Máté committed
153

154 155
    ram_size = property(get_ram_size)

Őry Máté committed
156
    @property
157
    @node_available
Őry Máté committed
158 159 160 161 162
    def ram_size_with_overcommit(self):
        """Bytes of total memory including overcommit margin.
        """
        return self.ram_size * self.overcommit

163
    @method_cache(30)
Őry Máté committed
164
    def get_remote_queue_name(self, queue_id):
165 166
        """Return the name of the remote celery queue for this node.

167
        throws Exception if there is no worker on the queue.
168
        Until the cache provide reult there can be dead queues.
169
        """
170

171
        if vm_tasks.check_queue(self.host.hostname, queue_id):
172
            self.node_online()
173 174
            return self.host.hostname + "." + queue_id
        else:
175
            if self.enabled:
176
                self.node_offline()
177
            raise WorkerNotFound()
Őry Máté committed
178

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
    def node_online(self):
        """Create activity and log entry when node reappears.
        """

        try:
            act = self.activity_log.order_by('-pk')[0]
        except IndexError:
            pass  # no monitoring activity at all
        else:
            logger.debug("The last activity was %s" % act)
            if act.activity_code.endswith("offline"):
                act = NodeActivity.create(code_suffix='monitor_succes_online',
                                          node=self, user=None)
                act.started = timezone.now()
                act.finished = timezone.now()
                act.succeeded = True
                act.save()
                logger.info("Node %s is ONLINE." % self.name)
                self.get_num_cores(invalidate_cache=True)
                self.get_ram_size(invalidate_cache=True)

    def node_offline(self):
        """Called when a node disappears.

        If the node is not already offline, record an activity and a log entry.
        """

        try:
            act = self.activity_log.order_by('-pk')[0]
        except IndexError:
            pass  # no activity at all
        else:
            logger.debug("The last activity was %s" % act)
            if act.activity_code.endswith("offline"):
                return
        act = NodeActivity.create(code_suffix='monitor_failed_offline',
                                  node=self, user=None)
        act.started = timezone.now()
        act.finished = timezone.now()
        act.succeeded = False
        act.save()
        logger.critical("Node %s is OFFLINE%s.", self.name,
                        ", but enabled" if self.enabled else "")
        # TODO: check if we should reschedule any VMs?

224
    def remote_query(self, task, timeout=30, raise_=False, default=None):
Őry Máté committed
225 226
        """Query the given task, and get the result.

227 228 229 230
        If the result is not ready or worker not reachable
        in timeout secs, return default value or raise a
        TimeoutError or WorkerNotFound exception.
        """
Őry Máté committed
231
        try:
232 233
            r = task.apply_async(
                queue=self.get_remote_queue_name('vm'), expires=timeout + 60)
Őry Máté committed
234
            return r.get(timeout=timeout)
235
        except (TimeoutError, WorkerNotFound):
Őry Máté committed
236 237 238 239 240
            if raise_:
                raise
            else:
                return default

241
    @node_available
242
    def get_monitor_info(self):
243 244
        try:
            handler = GraphiteHandler()
245 246 247
        except RuntimeError:
            return self.remote_query(vm_tasks.get_node_metrics, 30)

248
        query = Query()
Gregory Nagy committed
249 250 251
        query.set_target(self.host.hostname + ".circle")
        query.set_format("json")
        query.set_relative_start(5, "minutes")
252

253
        metrics = ["cpu.usage", "memory.usage"]
254
        for metric in metrics:
Gregory Nagy committed
255
            query.set_metric(metric)
256 257 258
            query.generate()
            handler.put(query)
            handler.send()
259 260

        collected = {}
261
        for metric in metrics:
Gregory Nagy committed
262
            response = handler.pop()
263 264 265 266
            try:
                cache = response[0]["datapoints"][-1][0]
            except (IndexError, KeyError):
                cache = 0
267 268 269 270 271
            if cache is None:
                cache = 0
            collected[metric] = cache
        return collected

272
    @property
273
    @node_available
274
    def cpu_usage(self):
275
        return float(self.get_monitor_info()["cpu.usage"]) / 100
276

277
    @property
278
    @node_available
279
    def ram_usage(self):
280
        return float(self.get_monitor_info()["memory.usage"]) / 100
281

282
    @property
283
    @node_available
284 285 286
    def byte_ram_usage(self):
        return self.ram_usage * self.ram_size

287
    @node_available
Őry Máté committed
288
    def update_vm_states(self):
289 290 291 292 293
        """Update state of Instances running on this Node.

        Query state of all libvirt domains, and notify Instances by their
        vm_state_changed hook.
        """
Őry Máté committed
294
        domains = {}
295 296 297 298 299
        domain_list = self.remote_query(vm_tasks.list_domains_info, timeout=5)
        if domain_list is None:
            logger.info("Monitoring failed at: %s", self.name)
            return
        for i in domain_list:
Őry Máté committed
300 301 302 303 304 305 306 307
            # [{'name': 'cloud-1234', 'state': 'RUNNING', ...}, ...]
            try:
                id = int(i['name'].split('-')[1])
            except:
                pass  # name format doesn't match
            else:
                domains[id] = i['state']

308 309
        instances = [{'id': i.id, 'state': i.state}
                     for i in self.instance_set.order_by('id').all()]
Őry Máté committed
310 311 312 313 314 315
        for i in instances:
            try:
                d = domains[i['id']]
            except KeyError:
                logger.info('Node %s update: instance %s missing from '
                            'libvirt', self, i['id'])
316 317
                # Set state to STOPPED when instance is missing
                self.instance_set.get(id=i['id']).vm_state_changed('STOPPED')
Őry Máté committed
318 319 320 321 322
            else:
                if d != i['state']:
                    logger.info('Node %s update: instance %s state changed '
                                '(libvirt: %s, db: %s)',
                                self, i['id'], d, i['state'])
323
                    self.instance_set.get(id=i['id']).vm_state_changed(d)
Őry Máté committed
324 325 326 327 328

                del domains[i['id']]
        for i in domains.keys():
            logger.info('Node %s update: domain %s in libvirt but not in db.',
                        self, i)
329 330 331

    @classmethod
    def get_state_count(cls, online, enabled):
332 333
        return len([1 for i in
                    cls.objects.filter(enabled=enabled).select_related('host')
334
                    if i.online == online])
335 336 337 338

    @permalink
    def get_absolute_url(self):
        return ('dashboard.views.node-detail', None, {'pk': self.id})