Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (8.3 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', 'MAC': 'mac', 'BRIDGE': 'link', 'NETWORK' : 'network' }
80

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

    
88
                if nics.has_key(index):
89
                    nics[index][key] = environ[env]
90
                else:
91
                    nics[index] = { key: environ[env] }
92

    
93
                # IPv6 support:
94
                #
95
                # The IPv6 of NIC with index 0 [the public NIC]
96
                # is derived using an EUI64 scheme.
97
                if index == 0 and key == 'mac':
98
                    nics[0]['ipv6'] = mac2eui64(nics[0]['mac'],
99
                                                settings.PUBLIC_IPV6_PREFIX)
100

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

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

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

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

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

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

    
145
    return msg
146

    
147

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

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

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

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

    
179
class PostStartHook(GanetiHook):
180
    """Post-instance-startup Ganeti Hook.
181

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

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

188
    This hook only runs on the Ganeti master.
189

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

    
196
            self.publish_msgs(notifs)
197

    
198
        return 0
199

    
200

    
201
class PostStopHook(GanetiHook):
202
    def run(self):
203
        return 0
204

    
205

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

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

    
218
    prefix = instance.split('-')[0]
219

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

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

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

    
242
if __name__ == "__main__":
243
    sys.exit(main())