Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.4 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
        logger.error("Ganeti NIC indexes are not consecutive starting at zero.");
127
        logger.error("NIC indexes are: %s. Environment is: %s", indexes, environ)
128
        raise Exception("Unexpected inconsistency in the Ganeti environment")
129

    
130
    # Construct the notification
131
    instance = environ['GANETI_INSTANCE_NAME']
132

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

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

    
144
    return msg
145

    
146

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

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

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

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

    
180

    
181
class PostStartHook(GanetiHook):
182
    """Post-instance-startup Ganeti Hook.
183

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

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

190
    This hook only runs on the Ganeti master.
191

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

    
198
            self.publish_msgs(notifs)
199

    
200
        return 0
201

    
202

    
203
class PostStopHook(GanetiHook):
204
    def run(self):
205
        return 0
206

    
207

    
208
def main():
209
    logging.basicConfig(level=logging.DEBUG)
210
    logger = logging.getLogger("synnefo.ganeti")
211

    
212
    try:
213
        instance = os.environ['GANETI_INSTANCE_NAME']
214
        op = os.environ['GANETI_HOOKS_PATH']
215
        phase = os.environ['GANETI_HOOKS_PHASE']
216
    except KeyError:
217
        raise Exception("Environment missing one of: " \
218
            "GANETI_INSTANCE_NAME, GANETI_HOOKS_PATH, GANETI_HOOKS_PHASE")
219

    
220
    prefix = instance.split('-')[0]
221

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

    
230
    hooks = {
231
        ("instance-add", "post"): PostStartHook,
232
        ("instance-start", "post"): PostStartHook,
233
        ("instance-reboot", "post"): PostStartHook,
234
        ("instance-stop", "post"): PostStopHook,
235
        ("instance-modify", "post"): PostStartHook
236
    }
237

    
238
    try:
239
        hook = hooks[(op, phase)](logger, os.environ, instance, prefix)
240
    except KeyError:
241
        raise Exception("No hook found for operation = '%s', phase = '%s'" % (op, phase))
242
    return hook.run()
243

    
244
if __name__ == "__main__":
245
    sys.exit(main())