Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.5 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(max_retries=2 * len(settings.AMQP_HOSTS))
168
        self.client.connect()
169

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

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

    
185

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

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

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

195
    This hook only runs on the Ganeti master.
196

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

    
203
            self.publish_msgs(notifs)
204

    
205
        return 0
206

    
207

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

    
212

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

    
217
    try:
218
        instance = os.environ['GANETI_INSTANCE_NAME']
219
        op = os.environ['GANETI_HOOKS_PATH']
220
        phase = os.environ['GANETI_HOOKS_PHASE']
221
    except KeyError:
222
        raise Exception("Environment missing one of: " \
223
            "GANETI_INSTANCE_NAME, GANETI_HOOKS_PATH, 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 development.
231
    #if not instance.startswith(settings.BACKEND_PREFIX_ID):
232
    #    logger.warning("Ignoring non-Synnefo instance %s", instance)
233
    #    return 0
234

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

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

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