Statistics
| Branch: | Tag: | Revision:

root / ganeti / hooks.py @ 737b0e28

History | View | Annotate | Download (7.8 kB)

1
#!/usr/bin/env python
2
#
3
# -*- coding: utf-8 -*-
4
#
5
# Copyright 2011 GRNET S.A. All rights reserved.
6
#
7
# Redistribution and use in source and binary forms, with or
8
# without modification, are permitted provided that the following
9
# conditions are met:
10
#
11
#   1. Redistributions of source code must retain the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer.
14
#
15
#   2. Redistributions in binary form must reproduce the above
16
#      copyright notice, this list of conditions and the following
17
#      disclaimer in the documentation and/or other materials
18
#      provided with the distribution.
19
#
20
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
21
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
23
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
24
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
27
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
28
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31
# POSSIBILITY OF SUCH DAMAGE.
32
#
33
# The views and conclusions contained in the software and
34
# documentation are those of the authors and should not be
35
# interpreted as representing official policies, either expressed
36
# or implied, of GRNET S.A.
37
#
38
"""Ganeti hooks for Synnefo
39

40
These are the individual Ganeti hooks for Synnefo.
41

42
"""
43

    
44
import sys
45
import os
46

    
47
import time
48
import json
49
import socket
50
import logging
51

    
52
from amqplib import client_0_8 as amqp
53

    
54
from synnefo.util.mac2eui64 import mac2eui64
55
import synnefo.settings as settings
56

    
57
def ganeti_net_status(logger, environ):
58
    """Produce notifications of type 'Ganeti-net-status'
59

60
    Process all GANETI_INSTANCE_NICx_y environment variables,
61
    where x is the NIC index, starting at 0,
62
    and y is one of "MAC", "IP", "BRIDGE".
63

64
    The result is returned as a single notification message
65
    of type 'ganeti-net-status', detailing the NIC configuration
66
    of a Ganeti instance.
67

68
    """
69
    nics = {}
70

    
71
    key_to_attr = { 'IP': 'ip', 'MAC': 'mac', 'BRIDGE': 'link' }
72

    
73
    for env in environ.keys():
74
        if env.startswith("GANETI_INSTANCE_NIC"):
75
            s = env.replace("GANETI_INSTANCE_NIC", "").split('_', 1)
76
            if len(s) == 2 and s[0].isdigit() and s[1] in ('MAC', 'IP', 'BRIDGE'):
77
                index = int(s[0])
78
                key = key_to_attr[s[1]]
79

    
80
                if nics.has_key(index):
81
                    nics[index][key] = environ[env]
82
                else:
83
                    nics[index] = { key: environ[env] }
84

    
85
                # IPv6 support:
86
                #
87
                # The IPv6 of NIC with index 0 [the public NIC]
88
                # is derived using an EUI64 scheme.
89
                if index == 0 and key == 'mac':
90
                    nics[0]['ipv6'] = mac2eui64(nics[0]['mac'],
91
                                                settings.PUBLIC_IPV6_PREFIX)
92

    
93
    # Amend notification with firewall settings
94
    tags = environ.get('GANETI_INSTANCE_TAGS', '')
95
    for tag in tags.split(' '):
96
        t = tag.split(':')
97
        if t[0:2] == ['synnefo', 'network']:
98
            if len(t) != 4:
99
                logger.error("Malformed synnefo tag %s", tag)
100
                continue
101
            try:
102
                index = int(t[2])
103
                nics[index]['firewall'] = t[3]
104
            except ValueError:
105
                logger.error("Malformed synnefo tag %s", tag)
106
            except KeyError:
107
                logger.error("Found tag %s for non-existent NIC %d",
108
                             tag, index)
109

    
110
    # Verify our findings are consistent with the Ganeti environment
111
    indexes = list(nics.keys())
112
    ganeti_nic_count = int(environ['GANETI_INSTANCE_NIC_COUNT'])
113
    if len(indexes) != ganeti_nic_count:
114
        logger.error("I have %d NICs, Ganeti says number of NICs is %d",
115
            len(indexes), ganeti_nic_count)
116
        raise Exception("Inconsistent number of NICs in Ganeti environment")
117

    
118
    if indexes != range(0, len(indexes)):
119
        logger.error("Ganeti NIC indexes are not consecutive starting at zero.");
120
        logger.error("NIC indexes are: %s. Environment is: %s", indexes, environ)
121
        raise Exception("Unexpected inconsistency in the Ganeti environment")
122

    
123
    # Construct the notification
124
    instance = environ['GANETI_INSTANCE_NAME']
125

    
126
    nics_list = []
127
    for i in indexes:
128
        nics_list.append(nics[i])
129

    
130
    msg = {
131
        "type": "ganeti-net-status",
132
        "instance": instance,
133
        "nics": nics_list
134
    }
135

    
136
    return msg
137

    
138

    
139
class GanetiHook():
140
    def __init__(self, logger, environ, instance, prefix):
141
        self.logger = logger
142
        self.environ = environ
143
        self.instance = instance
144
        self.prefix = prefix
145

    
146
    def on_master(self):
147
        """Return True if running on the Ganeti master"""
148
        return socket.getfqdn() == self.environ['GANETI_MASTER']
149

    
150
    def publish_msgs(self, msgs):
151
        for (msgtype, msg) in msgs:
152
            routekey = "ganeti.%s.event.%s" % (self.prefix, msgtype)
153
            self.logger.debug("Pushing message to RabbitMQ: %s (key = %s)",
154
                json.dumps(msg), routekey)
155
            msg = amqp.Message(json.dumps(msg))
156
            msg.properties["delivery_mode"] = 2  # Persistent
157

    
158
            # Retry up to five times to open a channel to RabbitMQ.
159
            # The hook needs to abort if this count is exceeded, because it
160
            # runs synchronously with VM creation inside Ganeti, and may only
161
            # run for a finite amount of time.
162
            #
163
            # FIXME: We need a reconciliation mechanism between the DB and
164
            #        Ganeti, for cases exactly like this.
165
            conn = None
166
            sent = False
167
            retry = 0
168
            while not sent and retry < 5:
169
                self.logger.debug("Attempting to publish to RabbitMQ at %s",
170
                    settings.RABBIT_HOST)
171
                try:
172
                    if not conn:
173
                        conn = amqp.Connection(host=settings.RABBIT_HOST,
174
                            userid=settings.RABBIT_USERNAME,
175
                            password=settings.RABBIT_PASSWORD,
176
                            virtual_host=settings.RABBIT_VHOST)
177
                        chann = conn.channel()
178
                        self.logger.debug("Successfully connected to RabbitMQ at %s",
179
                            settings.RABBIT_HOST)
180

    
181
                    chann.basic_publish(msg,
182
                        exchange=settings.EXCHANGE_GANETI,
183
                        routing_key=routekey)
184
                    sent = True
185
                    self.logger.debug("Successfully sent message to RabbitMQ")
186
                except socket.error:
187
                    conn = False
188
                    retry += 1
189
                    self.logger.exception("Publish to RabbitMQ failed, retry=%d in 1s",
190
                        retry)
191
                    time.sleep(1)
192

    
193
            if not sent:
194
                raise Exception("Publish to RabbitMQ failed after %d tries, aborting" % retry)
195

    
196

    
197
class PostStartHook(GanetiHook):
198
    """Post-instance-startup Ganeti Hook.
199

200
    Produce notifications to the rest of the Synnefo
201
    infrastructure in the post-instance-start phase of Ganeti.
202

203
    Currently, this list only contains a single message,
204
    detailing the net configuration of an instance.
205

206
    This hook only runs on the Ganeti master.
207

208
    """
209
    def run(self):
210
        if self.on_master():
211
            notifs = []
212
            notifs.append(("net", ganeti_net_status(self.logger, self.environ)))
213

    
214
            self.publish_msgs(notifs)
215

    
216
        return 0
217

    
218

    
219
class PostStopHook(GanetiHook):
220
    def run(self):
221
        return 0
222