Statistics
| Branch: | Tag: | Revision:

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

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
                                 logger=logger)
170
        self.client.connect()
171

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

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

    
187

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

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

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

197
    This hook only runs on the Ganeti master.
198

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

    
205
            self.publish_msgs(notifs)
206

    
207
        return 0
208

    
209

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

    
214

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

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

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

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

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

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

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