Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

#!/usr/bin/python3 

 

# Copyright (C) 2010, 2011  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. 

 

""" 

Statistics daemon in BIND 10 

 

""" 

import sys; sys.path.append ('@@PYTHONPATH@@') 

import os 

from time import time, strftime, gmtime 

from optparse import OptionParser, OptionValueError 

 

import isc 

import isc.util.process 

import isc.log 

from isc.log_messages.stats_messages import * 

 

isc.log.init("b10-stats") 

logger = isc.log.Logger("stats") 

 

# Some constants for debug levels. 

DBG_STATS_MESSAGING = logger.DBGLVL_COMMAND 

 

# This is for boot_time of Stats 

_BASETIME = gmtime() 

 

# for setproctitle 

isc.util.process.rename() 

 

# If B10_FROM_SOURCE is set in the environment, we use data files 

# from a directory relative to that, otherwise we use the ones 

# installed on the system 

if "B10_FROM_SOURCE" in os.environ: 

    SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \ 

        "src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec" 

else: 

    PREFIX = "/home/jelte/opt/bind10" 

    DATAROOTDIR = "${prefix}/share" 

    SPECFILE_LOCATION = "${datarootdir}" + os.sep + "bind10-devel" + os.sep + "stats.spec" 

    SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\ 

        .replace("${prefix}", PREFIX) 

 

def get_timestamp(): 

    """ 

    get current timestamp 

    """ 

    return time() 

 

def get_datetime(gmt=None): 

    """ 

    get current datetime 

    """ 

    if not gmt: gmt = gmtime() 

    return strftime("%Y-%m-%dT%H:%M:%SZ", gmt) 

 

def get_spec_defaults(spec): 

    """ 

    extracts the default values of the items from spec specified in 

    arg, and returns the dict-type variable which is a set of the item 

    names and the default values 

    """ 

    if type(spec) is not list: return {} 

    def _get_spec_defaults(spec): 

        item_type = spec['item_type'] 

        if item_type == "integer": 

            return int(spec.get('item_default', 0)) 

        elif item_type == "real": 

            return float(spec.get('item_default', 0.0)) 

        elif item_type == "boolean": 

            return bool(spec.get('item_default', False)) 

        elif item_type == "string": 

            return str(spec.get('item_default', "")) 

        elif item_type == "list": 

            return spec.get( 

                    "item_default", 

                    [ _get_spec_defaults(spec["list_item_spec"]) ]) 

        elif item_type == "map": 

            return spec.get( 

                    "item_default", 

                    dict([ (s["item_name"], _get_spec_defaults(s)) for s in spec["map_item_spec"] ]) ) 

        else: 

            return spec.get("item_default", None) 

    return dict([ (s['item_name'], _get_spec_defaults(s)) for s in spec ]) 

 

class Callback(): 

    """ 

    A Callback handler class 

    """ 

    def __init__(self, command=None, args=(), kwargs={}): 

        self.command = command 

        self.args = args 

        self.kwargs = kwargs 

 

    def __call__(self, *args, **kwargs): 

        if not args: args = self.args 

        if not kwargs: kwargs = self.kwargs 

        if self.command: return self.command(*args, **kwargs) 

 

class StatsError(Exception): 

    """Exception class for Stats class""" 

    pass 

 

class Stats: 

    """ 

    Main class of stats module 

    """ 

    def __init__(self): 

        self.running = False 

        # create ModuleCCSession object 

        self.mccs = isc.config.ModuleCCSession(SPECFILE_LOCATION, 

                                               self.config_handler, 

                                               self.command_handler) 

        self.cc_session = self.mccs._session 

        # get module spec 

        self.module_name = self.mccs.get_module_spec().get_module_name() 

        self.modules = {} 

        self.statistics_data = {} 

        # statistics data by each pid 

        self.statistics_data_bypid = {} 

        # get commands spec 

        self.commands_spec = self.mccs.get_module_spec().get_commands_spec() 

        # add event handler related command_handler of ModuleCCSession 

        self.callbacks = {} 

        for cmd in self.commands_spec: 

            # add prefix "command_" 

            name = "command_" + cmd["command_name"] 

            try: 

                callback = getattr(self, name) 

                kwargs = get_spec_defaults(cmd["command_args"]) 

                self.callbacks[name] = Callback(command=callback, kwargs=kwargs) 

            except AttributeError: 

                raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"]) 

        self.mccs.start() 

 

    def start(self): 

        """ 

        Start stats module 

        """ 

        self.running = True 

        logger.info(STATS_STARTING) 

 

        # request Bob to send statistics data 

        logger.debug(DBG_STATS_MESSAGING, STATS_SEND_REQUEST_BOSS) 

        cmd = isc.config.ccsession.create_command("getstats", None) 

        seq = self.cc_session.group_sendmsg(cmd, 'Boss') 

        try: 

            answer, env = self.cc_session.group_recvmsg(False, seq) 

180            if answer: 

                rcode, args = isc.config.ccsession.parse_answer(answer) 

180                if rcode == 0: 

                    errors = self.update_statistics_data( 

                        args["owner"], **args["data"]) 

                    if errors: 

                        raise StatsError("boss spec file is incorrect: " 

                                         + ", ".join(errors)) 

                    errors = self.update_statistics_data( 

                                self.module_name, 

                                last_update_time=get_datetime()) 

174                    if errors: 

                        raise StatsError("stats spec file is incorrect: " 

                                         + ", ".join(errors)) 

        except isc.cc.session.SessionTimeout: 

            pass 

 

        # initialized Statistics data 

        errors = self.update_statistics_data( 

            self.module_name, 

            lname=self.cc_session.lname, 

            boot_time=get_datetime(_BASETIME) 

            ) 

186        if errors: 

            raise StatsError("stats spec file is incorrect: " 

                             + ", ".join(errors)) 

 

        try: 

            while self.running: 

                self.mccs.check_command(False) 

        finally: 

            self.mccs.send_stopping() 

 

    def config_handler(self, new_config): 

        """ 

        handle a configure from the cc channel 

        """ 

        logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG, 

                     new_config) 

        # do nothing currently 

        return isc.config.create_answer(0) 

 

    def command_handler(self, command, kwargs): 

        """ 

        handle commands from the cc channel 

        """ 

        name = 'command_' + command 

        if name in self.callbacks: 

            callback = self.callbacks[name] 

            if kwargs: 

                return callback(**kwargs) 

            else: 

                return callback() 

        else: 

            logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command) 

            return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'") 

 

    def update_modules(self): 

        """ 

        updates information of each module. This method gets each 

        module's information from the config manager and sets it into 

        self.modules. If its getting from the config manager fails, it 

        raises StatsError. 

        """ 

        modules = {} 

        seq = self.cc_session.group_sendmsg( 

            isc.config.ccsession.create_command( 

                isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC), 

            'ConfigManager') 

        (answer, env) = self.cc_session.group_recvmsg(False, seq) 

242        if answer: 

            (rcode, value) = isc.config.ccsession.parse_answer(answer) 

            if rcode == 0: 

                for mod in value: 

                    spec = { "module_name" : mod } 

                    if value[mod] and type(value[mod]) is list: 

                        spec["statistics"] = value[mod] 

                    modules[mod] = isc.config.module_spec.ModuleSpec(spec) 

            else: 

                raise StatsError("Updating module spec fails: " + str(value)) 

        modules[self.module_name] = self.mccs.get_module_spec() 

        self.modules = modules 

 

    def get_statistics_data(self, owner=None, name=None): 

        """ 

        returns statistics data which stats module has of each 

        module. If it can't find specified statistics data, it raises 

        StatsError. 

        """ 

        self.update_statistics_data() 

        if owner and name: 

            try: 

                return {owner:{name:self.statistics_data[owner][name]}} 

            except KeyError: 

                pass 

        elif owner: 

            try: 

                return {owner: self.statistics_data[owner]} 

            except KeyError: 

                pass 

        elif name: 

            pass 

        else: 

            return self.statistics_data 

        raise StatsError("No statistics data found: " 

                         + "owner: " + str(owner) + ", " 

                         + "name: " + str(name)) 

 

    def update_statistics_data(self, owner=None, pid=-1, **data): 

        """ 

        change statistics date of specified module into specified 

        data. It updates information of each module first, and it 

        updates statistics data. If specified data is invalid for 

        statistics spec of specified owner, it returns a list of error 

        messages. If there is no error or if neither owner nor data is 

        specified in args, it returns None. pid is the process id of 

        the sender module in order for stats to identify which 

        instance sends statistics data in the situation that multiple 

        instances are working. 

        """ 

        # Note: 

        # The fix of #1751 is for multiple instances working. It is 

        # assumed here that they send different statistics data with 

        # each PID. Stats should save their statistics data by 

        # PID. The statistics data, which is the existing variable, is 

        # preserved by accumlating from statistics data by PID. This 

        # is an ad-hoc fix because administrators can not see 

        # statistics by each instance via bindctl or HTTP/XML. These 

        # interfaces aren't changed in this fix. 

 

        def _accum_bymodule(statistics_data_bypid): 

            # This is an internal function for the superordinate 

            # function. It accumulates statistics data of each PID by 

            # module. It returns the accumulation result. 

            def _accum(a, b): 

                # If the first arg is dict or list type, two values 

                # would be merged and accumlated. 

                if type(a) is dict: 

                    return dict([ (k, _accum(v, b[k])) \ 

                                      if k in b else (k, v) \ 

                                      for (k, v) in a.items() ] \ 

                                    + [ (k, v) \ 

                                            for (k, v) in b.items() \ 

                                            if k not in a ]) 

                elif type(a) is list: 

                    return [ _accum(a[i], b[i]) \ 

                                 if len(b) > i else a[i] \ 

                                 for i in range(len(a)) ] \ 

                                 + [ b[i] \ 

                                         for i in range(len(b)) \ 

                                         if len(a) <= i ] 

                # If the first arg is integer or float type, two 

                # values are just added. 

                elif type(a) is int or type(a) is float: 

                    return a + b 

                # If the first arg is str or other types than above, 

                # then it just returns the first arg which is assumed 

                # to be the newer value. 

                return a 

            ret = {} 

            for data in statistics_data_bypid.values(): 

                ret.update(_accum(data, ret)) 

            return ret 

 

        # Firstly, it gets default statistics data in each spec file. 

        self.update_modules() 

        statistics_data = {} 

        for (name, module) in self.modules.items(): 

            value = get_spec_defaults(module.get_statistics_spec()) 

            if module.validate_statistics(True, value): 

                statistics_data[name] = value 

        self.statistics_data = statistics_data 

 

        # If the "owner" and "data" arguments in this function are 

        # specified, then the variable of statistics data of each pid 

        # would be updated. 

        errors = [] 

        if owner and data: 

            try: 

                if self.modules[owner].validate_statistics(False, data, errors): 

                    if owner in self.statistics_data_bypid: 

                        if pid in self.statistics_data_bypid[owner]: 

                            self.statistics_data_bypid[owner][pid].update(data) 

                        else: 

                            self.statistics_data_bypid[owner][pid] = data 

                    else: 

                        self.statistics_data_bypid[owner] = { pid : data } 

            except KeyError: 

                errors.append("unknown module name: " + str(owner)) 

 

        # If there are inactive instances, which was actually running 

        # on the system before, their statistics data would be 

        # removed. To find inactive instances, it invokes the 

        # "show_processes" command to Boss via the cc session. Then it 

        # gets active instance list and compares its PIDs with PIDs in 

        # statistics data which it already has. If inactive instances 

        # are found, it would remove their statistics data. 

        seq = self.cc_session.group_sendmsg( 

            isc.config.ccsession.create_command("show_processes", None), 

            "Boss") 

        (answer, env) = self.cc_session.group_recvmsg(False, seq) 

392        if answer: 

            (rcode, value) = isc.config.ccsession.parse_answer(answer) 

392            if rcode == 0: 

392   392                if type(value) is list and len(value) > 0 \ 

                        and type(value[0]) is list and len(value[0]) > 1: 

                    mlist = [ k for k in self.statistics_data_bypid.keys() ] 

                    for m in mlist: 

                        # PID list which it has before except for -1 

                        plist1 = [ p for p in self.statistics_data_bypid[m]\ 

                                       .keys() if p != -1] 

                        # PID list of active instances which is 

                        # received from Boss 

                        plist2 = [ v[0] for v in value \ 

                                       if v[1].lower().find(m.lower()) \ 

                                       >= 0 ] 

                        # get inactive instance list by the difference 

                        # between plist1 and plist2 

                        nplist = set(plist1).difference(set(plist2)) 

                        for p in nplist: 

                            self.statistics_data_bypid[m].pop(p) 

                        if self.statistics_data_bypid[m]: 

                            if m in self.statistics_data: 

                                self.statistics_data[m].update( 

                                    _accum_bymodule( 

                                        self.statistics_data_bypid[m])) 

                        # remove statistics data of the module with no 

                        # PID 

                        else: 

                            self.statistics_data_bypid.pop(m) 

        if errors: return errors 

 

    def command_status(self): 

        """ 

        handle status command 

        """ 

        logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND) 

        return isc.config.create_answer( 

            0, "Stats is up. (PID " + str(os.getpid()) + ")") 

 

    def command_shutdown(self, pid=None): 

        """ 

        handle shutdown command 

 

        The pid argument is ignored, it is here to match the signature. 

        """ 

        logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND) 

        self.running = False 

        return isc.config.create_answer(0) 

 

    def command_show(self, owner=None, name=None): 

        """ 

        handle show command 

        """ 

        if owner or name: 

            logger.debug(DBG_STATS_MESSAGING, 

                         STATS_RECEIVED_SHOW_NAME_COMMAND, 

                         str(owner)+", "+str(name)) 

        else: 

            logger.debug(DBG_STATS_MESSAGING, 

                         STATS_RECEIVED_SHOW_ALL_COMMAND) 

        errors = self.update_statistics_data( 

            self.module_name, 

            timestamp=get_timestamp(), 

            report_time=get_datetime() 

            ) 

        if errors: 

            raise StatsError("stats spec file is incorrect: " 

                             + ", ".join(errors)) 

        try: 

            return isc.config.create_answer( 

                0, self.get_statistics_data(owner, name)) 

        except StatsError: 

            return isc.config.create_answer( 

                1, "specified arguments are incorrect: " \ 

                    + "owner: " + str(owner) + ", name: " + str(name)) 

 

    def command_showschema(self, owner=None, name=None): 

        """ 

        handle show command 

        """ 

        if owner or name: 

            logger.debug(DBG_STATS_MESSAGING, 

                         STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND, 

                         str(owner)+", "+str(name)) 

        else: 

            logger.debug(DBG_STATS_MESSAGING, 

                         STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND) 

        self.update_modules() 

        schema = {} 

        schema_byname = {} 

        for mod in self.modules: 

            spec = self.modules[mod].get_statistics_spec() 

            schema_byname[mod] = {} 

            if spec: 

                schema[mod] = spec 

                for item in spec: 

                    schema_byname[mod][item['item_name']] = item 

        if owner: 

            try: 

                if name: 

                    return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]}) 

                else: 

                    return isc.config.create_answer(0, {owner:schema[owner]}) 

            except KeyError: 

                pass 

        else: 

            if name: 

                return isc.config.create_answer(1, "module name is not specified") 

            else: 

                return isc.config.create_answer(0, schema) 

        return isc.config.create_answer( 

                1, "specified arguments are incorrect: " \ 

                    + "owner: " + str(owner) + ", name: " + str(name)) 

 

    def command_set(self, owner, pid=-1, data={}): 

        """ 

        handle set command 

        """ 

        errors = self.update_statistics_data(owner, pid, **data) 

        if errors: 

            return isc.config.create_answer( 

                1, "errors while setting statistics data: " \ 

                    + ", ".join(errors)) 

        errors = self.update_statistics_data( 

            self.module_name, last_update_time=get_datetime() ) 

        if errors: 

            raise StatsError("stats spec file is incorrect: " 

                             + ", ".join(errors)) 

        return isc.config.create_answer(0) 

 

494if __name__ == "__main__": 

    try: 

        parser = OptionParser() 

        parser.add_option( 

            "-v", "--verbose", dest="verbose", action="store_true", 

            help="display more about what is going on") 

        (options, args) = parser.parse_args() 

        if options.verbose: 

            isc.log.init("b10-stats", "DEBUG", 99) 

        stats = Stats() 

        stats.start() 

    except OptionValueError as ove: 

        logger.fatal(STATS_BAD_OPTION_VALUE, ove) 

        sys.exit(1) 

    except isc.cc.session.SessionError as se: 

        logger.fatal(STATS_CC_SESSION_ERROR, se) 

        sys.exit(1) 

    except StatsError as se: 

        logger.fatal(STATS_START_ERROR, se) 

        sys.exit(1) 

    except KeyboardInterrupt as kie: 

        logger.info(STATS_STOPPED_BY_KEYBOARD)