loadzone.py.in 6.82 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!@PYTHON@

# Copyright (C) 2012  Internet Systems Consortium.
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

import sys
sys.path.append('@@PYTHONPATH@@')
from optparse import OptionParser
21
from isc.dns import *
JINMEI Tatuya's avatar
JINMEI Tatuya committed
22
from isc.datasrc import *
23
import isc.log
24
from isc.log_messages.loadzone_messages import *
25

26
isc.log.init("b10-loadzone", buffer=False)
27
28
logger = isc.log.Logger("loadzone")

29
30
31
32
33
34
class BadArgument(Exception):
    '''An exception indicating an error in command line argument.

    '''
    pass

35
36
37
38
39
40
class LoadFailure(Exception):
    '''An exception indicating failure in loading operation.

    '''
    pass

41
42
43
44
def set_cmd_options(parser):
    '''Helper function to set command-line options.

    '''
JINMEI Tatuya's avatar
JINMEI Tatuya committed
45
46
47
48
49
50
51
52
    parser.add_option("-c", "--datasrc-conf", dest="conf", action="store",
                      help="""(Mandatory) configuration of datasrc to load
the zone in.  Example:
'{"database_file": "/path/to/dbfile/db.sqlite3"}'""",
                      metavar='CONFIG')
    parser.add_option("-t", "--datasrc-type", dest="datasrc_type",
                      action="store", default='sqlite3',
                      help="type of data source (e.g., 'sqlite3')")
53
54
55
    parser.add_option("-C", "--class", dest="zone_class", action="store",
                      default='IN',
                      help="RR class of the zone; currently must be 'IN'")
56
57

class LoadZoneRunner:
JINMEI Tatuya's avatar
JINMEI Tatuya committed
58
59
60
    '''Main logic for the loadzone.

    This is implemented as a class mainly for the convenience of tests.
61
62
63

    '''
    def __init__(self, command_args):
64
        self.__command_args = command_args
65
66
        self.__load_iteration_limit = 100000 # arbitrary choice for now
        self.__loaded_rrs = 0
67
68
69
70
71

        # These are essentially private, and defined as "protected" for the
        # convenience of tests inspecting them
        self._zone_class = None
        self._zone_name = None
JINMEI Tatuya's avatar
JINMEI Tatuya committed
72
        self._zone_file = None
JINMEI Tatuya's avatar
JINMEI Tatuya committed
73
74
        self._datasrc_config = None
        self._datasrc_type = None
75
76

    def _parse_args(self):
77
78
79
80
81
82
        '''Parse command line options and other arguments.

        This is essentially private, but defined as "protected" for tests.

        '''

83
84
85
        usage_txt = 'usage: %prog [options] zonename zonefile'
        parser = OptionParser(usage=usage_txt)
        set_cmd_options(parser)
86
        (options, args) = parser.parse_args(args=self.__command_args)
JINMEI Tatuya's avatar
JINMEI Tatuya committed
87
88
89
90
91

        if options.conf is None:
            raise BadArgument('data source config option cannot be omitted')
        self._datasrc_config = options.conf
        self._datasrc_type = options.datasrc_type
92
93
94
95
96
97
98
        try:
            self._zone_class = RRClass(options.zone_class)
        except isc.dns.InvalidRRClass as ex:
            raise BadArgument('Invalid zone class: ' + str(ex))
        if self._zone_class != RRClass.IN():
            raise BadArgument("RR class is not supported: " +
                              str(self._zone_class))
JINMEI Tatuya's avatar
JINMEI Tatuya committed
99

100
101
102
103
104
105
106
107
        if len(args) != 2:
            raise BadArgument('Unexpected number of arguments: %d (must be 2)'
                              % (len(args)))
        try:
            self._zone_name = Name(args[0])
        except Exception as ex: # too broad, but there's no better granurality
            raise BadArgument("Invalid zone name '" + args[0] + "': " +
                              str(ex))
JINMEI Tatuya's avatar
JINMEI Tatuya committed
108
        self._zone_file = args[1]
109

110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
    def __cancel_create(self):
        '''sqlite3-only hack: delete the zone just created on load failure.

        This should eventually be done via generic datasrc API, but right now
        we don't have that interface.  Leaving the zone in this situation
        is too bad, so we handle it with a workaround.

        '''
        if self._datasrc_type is not 'sqlite3':
            return

        import sqlite3          # we need the module only here
        import json

        # If we are here, the following should basically succeed; since
        # this is considered a temporary workaround we don't bother to catch
        # and recover rare failure cases.
        dbfile = json.loads(self._datasrc_config)['database_file']
        with sqlite3.connect(dbfile) as conn:
            cur = conn.cursor()
            cur.execute("DELETE FROM zones WHERE name = ?",
                        [self._zone_name.to_text()])

JINMEI Tatuya's avatar
JINMEI Tatuya committed
133
    def _do_load(self):
134
135
136
137
138
        '''Main part of the load logic.

        This is essentially private, but defined as "protected" for tests.

        '''
139
        created = False
140
141
142
143
144
145
146
147
148
        try:
            datasrc_client = DataSourceClient(self._datasrc_type,
                                              self._datasrc_config)
            created = datasrc_client.create_zone(self._zone_name)
            if created:
                logger.info(LOADZONE_ZONE_CREATED, self._zone_name,
                            self._zone_class)
            loader = ZoneLoader(datasrc_client, self._zone_name,
                                self._zone_file)
149
150
151
152
            while not loader.load_incremental(self.__load_iteration_limit):
                self.__loaded_rrs += self.__load_iteration_limit
                logger.info(LOADZONE_LOADING, self.__loaded_rrs,
                            self._zone_name, self._zone_class)
153
        except Exception as ex:
154
155
156
157
158
159
160
            # release any remaining lock held in the client/loader
            loader, datasrc_client = None, None
            if created:
                self.__cancel_create()
                logger.error(LOADZONE_CANCEL_CREATE_ZONE, self._zone_name,
                             self._zone_class)
            raise LoadFailure(str(ex))
JINMEI Tatuya's avatar
JINMEI Tatuya committed
161

162
    def run(self):
163
164
        '''Top-level method, simply calling other helpers'''

165
166
        try:
            self._parse_args()
167
            self._do_load()
168
            logger.info(LOADZONE_DONE, self._zone_name, self._zone_class)
JINMEI Tatuya's avatar
JINMEI Tatuya committed
169
            return 0
170
171
        except BadArgument as ex:
            logger.error(LOADZONE_ARGUMENT_ERROR, ex)
172
173
174
        except LoadFailure as ex:
            logger.error(LOADZONE_LOAD_ERROR, self._zone_name,
                         self._zone_class, ex)
JINMEI Tatuya's avatar
JINMEI Tatuya committed
175
176
177
        except Exception as ex:
            logger.error(LOADZONE_UNEXPECTED_FAILURE, ex)
        return 1
178
179
180

if '__main__' == __name__:
    runner = LoadZoneRunner(sys.argv[1:])
JINMEI Tatuya's avatar
JINMEI Tatuya committed
181
182
    ret = runner.run()
    sys.exit(ret)
183
184
185
186

## Local Variables:
## mode: python
## End: