Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-gtools / synnefo / ganeti / hook.py @ c59f7e0f

History | View | Annotate | Download (8.6 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
import json
47
import socket
48
import logging
49

    
50
from time import time
51

    
52
from synnefo import settings
53
from synnefo.lib.amqp import AMQPClient
54
from synnefo.lib.utils import split_time
55
from synnefo.util.mac2eui64 import mac2eui64
56

    
57

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

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

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

69
    """
70
    nics = {}
71

    
72
    key_to_attr = {'IP': 'ip',
73
                   'MAC': 'mac',
74
                   'BRIDGE': 'link',
75
                   'NETWORK': 'network'}
76

    
77
    for env in environ.keys():
78
        if env.startswith("GANETI_INSTANCE_NIC"):
79
            s = env.replace("GANETI_INSTANCE_NIC", "").split('_', 1)
80
            if len(s) == 2 and s[0].isdigit() and\
81
                    s[1] in ('MAC', 'IP', 'BRIDGE', 'NETWORK'):
82
                index = int(s[0])
83
                key = key_to_attr[s[1]]
84

    
85
                if index in nics:
86
                    nics[index][key] = environ[env]
87
                else:
88
                    nics[index] = {key: environ[env]}
89

    
90
                # IPv6 support:
91
                #
92
                # The IPv6 is derived using an EUI64 scheme.
93
                if key == 'mac':
94
                    subnet6 = environ.get("GANETI_INSTANCE_NIC" + s[0] +
95
                                          "_NETWORK_SUBNET6", None)
96
                    if subnet6:
97
                        nics[index]['ipv6'] = mac2eui64(nics[index]['mac'],
98
                                                        subnet6)
99

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

    
117
    # Verify our findings are consistent with the Ganeti environment
118
    indexes = list(nics.keys())
119
    ganeti_nic_count = int(environ['GANETI_INSTANCE_NIC_COUNT'])
120
    if len(indexes) != ganeti_nic_count:
121
        logger.error("I have %d NICs, Ganeti says number of NICs is %d",
122
                     len(indexes), ganeti_nic_count)
123
        raise Exception("Inconsistent number of NICs in Ganeti environment")
124

    
125
    if indexes != range(0, len(indexes)):
126
        msg = "Ganeti NIC indexes are not consecutive starting at zero."
127
        logger.error(msg)
128
        msg = "NIC indexes are: %s. Environment is: %s" % (indexes, environ)
129
        logger.error(msg)
130
        raise Exception("Unexpected inconsistency in the Ganeti environment")
131

    
132
    # Construct the notification
133
    instance = environ['GANETI_INSTANCE_NAME']
134

    
135
    nics_list = []
136
    for i in indexes:
137
        nics_list.append(nics[i])
138

    
139
    msg = {
140
        "event_time": split_time(time()),
141
        "type": "ganeti-net-status",
142
        "instance": instance,
143
        "nics": nics_list
144
    }
145

    
146
    return msg
147

    
148

    
149
class GanetiHook():
150
    def __init__(self, logger, environ, instance, prefix):
151
        self.logger = logger
152
        self.environ = environ
153
        self.instance = instance
154
        self.prefix = prefix
155
        # Retry up to two times(per host) to open a channel to RabbitMQ.
156
        # The hook needs to abort if this count is exceeded, because it
157
        # runs synchronously with VM creation inside Ganeti, and may only
158
        # run for a finite amount of time.
159

    
160
        # FIXME: We need a reconciliation mechanism between the DB and
161
        #        Ganeti, for cases exactly like this.
162
        self.client = AMQPClient(hosts=settings.AMQP_HOSTS,
163
                                 max_retries=2 * len(settings.AMQP_HOSTS),
164
                                 logger=logger)
165
        self.client.connect()
166

    
167
    def on_master(self):
168
        """Return True if running on the Ganeti master"""
169
        return socket.getfqdn() == self.environ['GANETI_MASTER']
170

    
171
    def publish_msgs(self, msgs):
172
        for (msgtype, msg) in msgs:
173
            routekey = "ganeti.%s.event.%s" % (self.prefix, msgtype)
174
            self.logger.debug("Pushing message to RabbitMQ: %s (key = %s)",
175
                              json.dumps(msg), routekey)
176
            msg = json.dumps(msg)
177
            self.client.basic_publish(exchange=settings.EXCHANGE_GANETI,
178
                                      routing_key=routekey,
179
                                      body=msg)
180
        self.client.close()
181

    
182

    
183
class PostStartHook(GanetiHook):
184
    """Post-instance-startup Ganeti Hook.
185

186
    Produce notifications to the rest of the Synnefo
187
    infrastructure in the post-instance-start phase of Ganeti.
188

189
    Currently, this list only contains a single message,
190
    detailing the net configuration of an instance.
191

192
    This hook only runs on the Ganeti master.
193

194
    """
195
    def run(self):
196
        if self.on_master():
197
            notifs = []
198
            notifs.append(("net",
199
                           ganeti_net_status(self.logger, self.environ))
200
                          )
201

    
202
            self.publish_msgs(notifs)
203

    
204
        return 0
205

    
206

    
207
class PostStopHook(GanetiHook):
208
    def run(self):
209
        return 0
210

    
211

    
212
def main():
213
    logging.basicConfig(level=logging.DEBUG)
214
    logger = logging.getLogger("synnefo.ganeti")
215

    
216
    try:
217
        instance = os.environ['GANETI_INSTANCE_NAME']
218
        op = os.environ['GANETI_HOOKS_PATH']
219
        phase = os.environ['GANETI_HOOKS_PHASE']
220
    except KeyError:
221
        raise Exception("Environment missing one of: "
222
                        "GANETI_INSTANCE_NAME, GANETI_HOOKS_PATH,"
223
                        " GANETI_HOOKS_PHASE")
224

    
225
    prefix = instance.split('-')[0]
226

    
227
    # FIXME: The hooks should only run for Synnefo instances.
228
    # Uncomment the following lines for a shared Ganeti deployment.
229
    # Currently, the following code is commented out because multiple
230
    # backend prefixes are used in the same Ganeti installation during
231
    # development.
232
    #if not instance.startswith(settings.BACKEND_PREFIX_ID):
233
    #    logger.warning("Ignoring non-Synnefo instance %s", instance)
234
    #    return 0
235

    
236
    hooks = {
237
        ("instance-add", "post"): PostStartHook,
238
        ("instance-start", "post"): PostStartHook,
239
        ("instance-reboot", "post"): PostStartHook,
240
        ("instance-stop", "post"): PostStopHook,
241
        ("instance-modify", "post"): PostStartHook
242
    }
243

    
244
    try:
245
        hook = hooks[(op, phase)](logger, os.environ, instance, prefix)
246
    except KeyError:
247
        raise Exception("No hook found for operation = '%s', phase = '%s'" %
248
                        (op, phase))
249
    return hook.run()
250

    
251
if __name__ == "__main__":
252
    sys.exit(main())