Statistics
| Branch: | Tag: | Revision:

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

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 subprocess
47

    
48
import json
49
import socket
50
import logging
51

    
52
from time import time
53

    
54
from synnefo import settings
55
from synnefo.lib.amqp import AMQPClient
56
from synnefo.lib.utils import split_time
57

    
58

    
59
def mac2eui64(mac, prefixstr):
60
    process = subprocess.Popen(["mac2eui64", mac, prefixstr],
61
                                stdout=subprocess.PIPE)
62
    return process.stdout.read().rstrip()
63

    
64

    
65
def ganeti_net_status(logger, environ):
66
    """Produce notifications of type 'Ganeti-net-status'
67

68
    Process all GANETI_INSTANCE_NICx_y environment variables,
69
    where x is the NIC index, starting at 0,
70
    and y is one of "MAC", "IP", "BRIDGE".
71

72
    The result is returned as a single notification message
73
    of type 'ganeti-net-status', detailing the NIC configuration
74
    of a Ganeti instance.
75

76
    """
77
    nics = {}
78

    
79
    key_to_attr = {'IP': 'ip',
80
                   'MAC': 'mac',
81
                   'BRIDGE': 'link',
82
                   'NETWORK': 'network'}
83

    
84
    for env in environ.keys():
85
        if env.startswith("GANETI_INSTANCE_NIC"):
86
            s = env.replace("GANETI_INSTANCE_NIC", "").split('_', 1)
87
            if len(s) == 2 and s[0].isdigit() and\
88
               s[1] in ('MAC', 'IP', 'BRIDGE', 'NETWORK'):
89
                index = int(s[0])
90
                key = key_to_attr[s[1]]
91

    
92
                if index in nics:
93
                    nics[index][key] = environ[env]
94
                else:
95
                    nics[index] = {key: environ[env]}
96

    
97
                # IPv6 support:
98
                #
99
                # The IPv6 is derived using an EUI64 scheme.
100
                if key == 'mac':
101
                    subnet6 = environ.get("GANETI_INSTANCE_NIC" + s[0] +
102
                                          "_NETWORK_SUBNET6", None)
103
                    if subnet6:
104
                        nics[index]['ipv6'] = mac2eui64(nics[index]['mac'],
105
                                                        subnet6)
106

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

    
124
    # Verify our findings are consistent with the Ganeti environment
125
    indexes = list(nics.keys())
126
    ganeti_nic_count = int(environ['GANETI_INSTANCE_NIC_COUNT'])
127
    if len(indexes) != ganeti_nic_count:
128
        logger.error("I have %d NICs, Ganeti says number of NICs is %d",
129
            len(indexes), ganeti_nic_count)
130
        raise Exception("Inconsistent number of NICs in Ganeti environment")
131

    
132
    if indexes != range(0, len(indexes)):
133
        logger.error("Ganeti NIC indexes are not consecutive starting at zero.");
134
        logger.error("NIC indexes are: %s. Environment is: %s", indexes, environ)
135
        raise Exception("Unexpected inconsistency in the Ganeti environment")
136

    
137
    # Construct the notification
138
    instance = environ['GANETI_INSTANCE_NAME']
139

    
140
    nics_list = []
141
    for i in indexes:
142
        nics_list.append(nics[i])
143

    
144
    msg = {
145
        "event_time": split_time(time()),
146
        "type": "ganeti-net-status",
147
        "instance": instance,
148
        "nics": nics_list
149
    }
150

    
151
    return msg
152

    
153

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

    
165
        # FIXME: We need a reconciliation mechanism between the DB and
166
        #        Ganeti, for cases exactly like this.
167
        self.client = AMQPClient(hosts=settings.AMQP_HOSTS,
168
                                 max_retries = 2 * len(settings.AMQP_HOSTS))
169
        self.client.connect()
170

    
171
    def on_master(self):
172
        """Return True if running on the Ganeti master"""
173
        return socket.getfqdn() == self.environ['GANETI_MASTER']
174

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

    
186

    
187
class PostStartHook(GanetiHook):
188
    """Post-instance-startup Ganeti Hook.
189

190
    Produce notifications to the rest of the Synnefo
191
    infrastructure in the post-instance-start phase of Ganeti.
192

193
    Currently, this list only contains a single message,
194
    detailing the net configuration of an instance.
195

196
    This hook only runs on the Ganeti master.
197

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

    
204
            self.publish_msgs(notifs)
205

    
206
        return 0
207

    
208

    
209
class PostStopHook(GanetiHook):
210
    def run(self):
211
        return 0
212

    
213

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

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

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

    
228
    # FIXME: The hooks should only run for Synnefo instances.
229
    # Uncomment the following lines for a shared Ganeti deployment.
230
    # Currently, the following code is commented out because multiple
231
    # backend prefixes are used in the same Ganeti installation during 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'" % (op, phase))
248
    return hook.run()
249

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