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

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

659

660

661

662

663

664

665

666

667

668

669

670

671

672

673

674

675

676

677

678

679

680

681

682

683

684

685

686

687

688

689

690

691

692

693

694

695

696

697

698

699

700

701

702

703

704

705

706

707

708

709

710

711

712

713

714

715

716

717

718

719

720

721

722

723

724

725

726

727

728

729

730

731

732

733

734

735

736

737

738

739

740

741

742

743

744

745

746

747

748

749

750

751

752

753

754

755

756

757

758

759

760

761

762

763

764

765

766

767

768

769

770

771

772

773

774

775

776

777

778

779

780

781

782

783

784

785

786

787

788

789

790

791

792

793

794

795

796

797

798

799

800

801

802

803

804

805

806

807

808

809

810

811

812

813

814

815

816

817

818

819

820

821

822

823

824

825

826

827

828

829

830

831

832

833

834

835

836

837

838

839

840

841

842

843

844

845

846

847

848

849

850

851

852

853

854

855

856

857

858

859

860

861

862

863

864

865

866

867

868

869

870

871

872

873

874

875

876

877

878

879

880

881

882

883

884

885

886

887

888

889

890

891

892

893

894

895

896

897

898

899

900

901

902

903

904

905

906

907

908

909

910

911

912

913

914

915

916

917

918

919

920

921

922

923

924

925

926

927

928

929

930

931

932

933

934

935

936

937

938

939

940

941

942

943

944

945

946

947

948

949

950

951

952

953

954

955

956

957

958

959

960

961

962

963

964

965

966

967

968

969

970

971

972

973

974

975

976

977

978

979

980

981

982

983

984

985

986

987

988

989

990

991

992

993

994

995

996

997

998

999

1000

1001

1002

1003

1004

1005

1006

1007

1008

1009

1010

1011

1012

1013

1014

1015

1016

1017

1018

1019

1020

1021

1022

1023

1024

1025

1026

1027

1028

1029

1030

1031

1032

1033

1034

1035

1036

1037

1038

1039

1040

1041

1042

1043

1044

1045

1046

1047

1048

1049

1050

1051

1052

1053

1054

1055

1056

1057

1058

1059

1060

1061

1062

1063

1064

1065

1066

1067

1068

1069

1070

1071

1072

1073

1074

1075

1076

1077

1078

1079

1080

1081

1082

1083

1084

1085

1086

1087

1088

1089

1090

1091

1092

1093

1094

1095

1096

1097

1098

1099

1100

1101

1102

1103

1104

1105

1106

1107

1108

1109

1110

1111

1112

1113

1114

1115

1116

1117

1118

1119

1120

1121

1122

1123

1124

1125

1126

1127

1128

1129

1130

1131

1132

1133

1134

1135

1136

1137

1138

1139

1140

1141

1142

1143

1144

1145

1146

1147

1148

1149

1150

1151

1152

1153

1154

1155

1156

1157

1158

1159

1160

1161

1162

1163

1164

1165

1166

1167

1168

1169

1170

1171

1172

1173

1174

1175

1176

1177

1178

1179

1180

1181

1182

1183

1184

1185

1186

1187

1188

1189

1190

1191

1192

1193

1194

1195

1196

1197

1198

1199

1200

1201

1202

1203

1204

1205

1206

1207

1208

1209

#!/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. 

 

""" 

This file implements the Boss of Bind (BoB, or bob) program. 

 

Its purpose is to start up the BIND 10 system, and then manage the 

processes, by starting and stopping processes, plus restarting 

processes that exit. 

 

To start the system, it first runs the c-channel program (msgq), then 

connects to that. It then runs the configuration manager, and reads 

its own configuration. Then it proceeds to starting other modules. 

 

The Python subprocess module is used for starting processes, but 

because this is not efficient for managing groups of processes, 

SIGCHLD signals are caught and processed using the signal module. 

 

Most of the logic is contained in the BoB class. However, since Python 

requires that signal processing happen in the main thread, we do 

signal handling outside of that class, in the code running for 

__main__. 

""" 

 

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

import os 

 

# 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 

46if "B10_FROM_SOURCE" in os.environ: 

    SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + "/src/bin/bind10/bob.spec" 

else: 

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

    DATAROOTDIR = "${prefix}/share" 

    SPECFILE_LOCATION = "${datarootdir}/bind10-devel/bob.spec".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX) 

 

import subprocess 

import signal 

import re 

import errno 

import time 

import select 

import random 

import socket 

from optparse import OptionParser, OptionValueError 

import io 

import pwd 

import posix 

import copy 

 

from bind10_config import LIBEXECPATH 

import isc.cc 

import isc.util.process 

import isc.net.parse 

import isc.log 

from isc.log_messages.bind10_messages import * 

import isc.bind10.component 

import isc.bind10.special_component 

import isc.bind10.socket_cache 

import libutil_io_python 

import tempfile 

 

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

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

 

# Pending system-wide debug level definitions, the ones we 

# use here are hardcoded for now 

DBG_PROCESS = logger.DBGLVL_TRACE_BASIC 

DBG_COMMANDS = logger.DBGLVL_TRACE_DETAIL 

 

# Messages sent over the unix domain socket to indicate if it is followed by a real socket 

CREATOR_SOCKET_OK = b"1\n" 

CREATOR_SOCKET_UNAVAILABLE = b"0\n" 

 

# RCodes of known exceptions for the get_token command 

CREATOR_SOCKET_ERROR = 2 

CREATOR_SHARE_ERROR = 3 

 

# Assign this process some longer name 

isc.util.process.rename(sys.argv[0]) 

 

# This is the version that gets displayed to the user. 

# The VERSION string consists of the module name, the module version 

# number, and the overall BIND 10 version number (set in configure.ac). 

VERSION = "bind10 20110223 (BIND 10 20120405)" 

 

# This is for boot_time of Boss 

_BASETIME = time.gmtime() 

 

class ProcessInfoError(Exception): pass 

 

class ProcessInfo: 

    """Information about a process""" 

 

    dev_null = open(os.devnull, "w") 

 

    def __init__(self, name, args, env={}, dev_null_stdout=False, 

                 dev_null_stderr=False): 

        self.name = name 

        self.args = args 

        self.env = env 

        self.dev_null_stdout = dev_null_stdout 

        self.dev_null_stderr = dev_null_stderr 

        self.process = None 

        self.pid = None 

 

    def _preexec_work(self): 

        """Function used before running a program that needs to run as a 

        different user.""" 

        # First, put us into a separate process group so we don't get 

        # SIGINT signals on Ctrl-C (the boss will shut everthing down by 

        # other means). 

        os.setpgrp() 

 

    def _spawn(self): 

        if self.dev_null_stdout: 

            spawn_stdout = self.dev_null 

        else: 

            spawn_stdout = None 

136        if self.dev_null_stderr: 

            spawn_stderr = self.dev_null 

        else: 

            spawn_stderr = None 

        # Environment variables for the child process will be a copy of those 

        # of the boss process with any additional specific variables given 

        # on construction (self.env). 

        spawn_env = copy.deepcopy(os.environ) 

        spawn_env.update(self.env) 

        spawn_env['PATH'] = LIBEXECPATH + ':' + spawn_env['PATH'] 

        self.process = subprocess.Popen(self.args, 

                                        stdin=subprocess.PIPE, 

                                        stdout=spawn_stdout, 

                                        stderr=spawn_stderr, 

                                        close_fds=True, 

                                        env=spawn_env, 

                                        preexec_fn=self._preexec_work) 

        self.pid = self.process.pid 

 

    # spawn() and respawn() are the same for now, but in the future they 

    # may have different functionality 

    def spawn(self): 

        self._spawn() 

 

    def respawn(self): 

        self._spawn() 

 

class CChannelConnectError(Exception): pass 

 

class ProcessStartError(Exception): pass 

 

class BoB: 

    """Boss of BIND class.""" 

 

    def __init__(self, msgq_socket_file=None, data_path=None, 

                 config_filename=None, clear_config=False, nocache=False, 

                 verbose=False, nokill=False, setuid=None, username=None, 

                 cmdctl_port=None, wait_time=10): 

        """ 

            Initialize the Boss of BIND. This is a singleton (only one can run). 

         

            The msgq_socket_file specifies the UNIX domain socket file that the 

            msgq process listens on.  If verbose is True, then the boss reports 

            what it is doing. 

 

            Data path and config filename are passed through to config manager 

            (if provided) and specify the config file to be used. 

 

            The cmdctl_port is passed to cmdctl and specify on which port it 

            should listen. 

 

            wait_time controls the amount of time (in seconds) that Boss waits 

            for selected processes to initialize before continuing with the 

            initialization.  Currently this is only the configuration manager. 

        """ 

        self.cc_session = None 

        self.ccs = None 

        self.curproc = None 

        self.msgq_socket_file = msgq_socket_file 

        self.nocache = nocache 

        self.component_config = {} 

        # Some time in future, it may happen that a single component has 

        # multple processes (like a pipeline-like component). If so happens, 

        # name "components" may be inapropriate. But as the code isn't probably 

        # completely ready for it, we leave it at components for now. We also 

        # want to support multiple instances of a single component. If it turns 

        # out that we'll have a single component with multiple same processes 

        # or if we start multiple components with the same configuration (we do 

        # this now, but it might change) is an open question. 

        self.components = {} 

        # Simply list of components that died and need to wait for a 

        # restart. Components manage their own restart schedule now 

        self.components_to_restart = [] 

        self.runnable = False 

        self.uid = setuid 

        self.username = username 

        self.verbose = verbose 

        self.nokill = nokill 

        self.data_path = data_path 

        self.config_filename = config_filename 

        self.clear_config = clear_config 

        self.cmdctl_port = cmdctl_port 

        self.wait_time = wait_time 

        self._component_configurator = isc.bind10.component.Configurator(self, 

            isc.bind10.special_component.get_specials()) 

        # The priorities here make them start in the correct order. First 

        # the socket creator (which would drop root privileges by then), 

        # then message queue and after that the config manager (which uses 

        # the config manager) 

        self.__core_components = { 

            'sockcreator': { 

                'kind': 'core', 

                'special': 'sockcreator', 

                'priority': 200 

            }, 

            'msgq': { 

                'kind': 'core', 

                'special': 'msgq', 

                'priority': 199 

            }, 

            'cfgmgr': { 

                'kind': 'core', 

                'special': 'cfgmgr', 

                'priority': 198 

            } 

        } 

        self.__started = False 

        self.exitcode = 0 

 

        # If -v was set, enable full debug logging. 

246        if self.verbose: 

            logger.set_severity("DEBUG", 99) 

        # This is set in init_socket_srv 

        self._socket_path = None 

        self._socket_cache = None 

        self._tmpdir = None 

        self._srv_socket = None 

        self._unix_sockets = {} 

 

    def __propagate_component_config(self, config): 

        comps = dict(config) 

        # Fill in the core components, so they stay alive 

        for comp in self.__core_components: 

            if comp in comps: 

                raise Exception(comp + " is core component managed by " + 

                                "bind10 boss, do not set it") 

            comps[comp] = self.__core_components[comp] 

        # Update the configuration 

        self._component_configurator.reconfigure(comps) 

 

    def config_handler(self, new_config): 

        # If this is initial update, don't do anything now, leave it to startup 

        if not self.runnable: 

            return 

        logger.debug(DBG_COMMANDS, BIND10_RECEIVED_NEW_CONFIGURATION, 

                     new_config) 

        try: 

            if 'components' in new_config: 

                self.__propagate_component_config(new_config['components']) 

            return isc.config.ccsession.create_answer(0) 

        except Exception as e: 

            return isc.config.ccsession.create_answer(1, str(e)) 

 

    def get_processes(self): 

        pids = list(self.components.keys()) 

        pids.sort() 

        process_list = [ ] 

        for pid in pids: 

            process_list.append([pid, self.components[pid].name()]) 

        return process_list 

 

    def _get_stats_data(self): 

        return { "owner": "Boss", 

                 "data": { 'boot_time': 

                               time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME) 

                           } 

                 } 

 

    def command_handler(self, command, args): 

        logger.debug(DBG_COMMANDS, BIND10_RECEIVED_COMMAND, command) 

        answer = isc.config.ccsession.create_answer(1, "command not implemented") 

        if type(command) != str: 

            answer = isc.config.ccsession.create_answer(1, "bad command") 

        else: 

            if command == "shutdown": 

                self.runnable = False 

                answer = isc.config.ccsession.create_answer(0) 

            elif command == "getstats": 

                answer = isc.config.ccsession.create_answer(0, self._get_stats_data()) 

            elif command == "sendstats": 

                # send statistics data to the stats daemon immediately 

                stats_data = self._get_stats_data() 

                valid = self.ccs.get_module_spec().validate_statistics( 

                    True, stats_data["data"]) 

319                if valid: 

                    cmd = isc.config.ccsession.create_command('set', stats_data) 

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

                    # Consume the answer, in case it becomes a orphan message. 

                    try: 

                        self.cc_session.group_recvmsg(False, seq) 

                    except isc.cc.session.SessionTimeout: 

                        pass 

                    answer = isc.config.ccsession.create_answer(0) 

                else: 

                    logger.fatal(BIND10_INVALID_STATISTICS_DATA); 

                    answer = isc.config.ccsession.create_answer( 

                        1, "specified statistics data is invalid") 

            elif command == "ping": 

                answer = isc.config.ccsession.create_answer(0, "pong") 

            elif command == "show_processes": 

                answer = isc.config.ccsession. \ 

                    create_answer(0, self.get_processes()) 

            elif command == "get_socket": 

                answer = self._get_socket(args) 

            elif command == "drop_socket": 

                if "token" not in args: 

                    answer = isc.config.ccsession. \ 

                        create_answer(1, "Missing token parameter") 

                else: 

                    try: 

                        self._socket_cache.drop_socket(args["token"]) 

                        answer = isc.config.ccsession.create_answer(0) 

                    except Exception as e: 

                        answer = isc.config.ccsession.create_answer(1, str(e)) 

            else: 

                answer = isc.config.ccsession.create_answer(1, 

                                                            "Unknown command") 

        return answer 

 

    def kill_started_components(self): 

        """ 

            Called as part of the exception handling when a process fails to 

            start, this runs through the list of started processes, killing 

            each one.  It then clears that list. 

        """ 

        logger.info(BIND10_KILLING_ALL_PROCESSES) 

 

        for pid in self.components: 

            logger.info(BIND10_KILL_PROCESS, self.components[pid].name()) 

            self.components[pid].kill(True) 

        self.components = {} 

 

    def _read_bind10_config(self): 

        """ 

            Reads the parameters associated with the BoB module itself. 

 

            This means the list of components we should start now. 

 

            This could easily be combined into start_all_processes, but 

            it stays because of historical reasons and because the tests 

            replace the method sometimes. 

        """ 

        logger.info(BIND10_READING_BOSS_CONFIGURATION) 

 

        config_data = self.ccs.get_full_config() 

        self.__propagate_component_config(config_data['components']) 

 

    def log_starting(self, process, port = None, address = None): 

        """ 

            A convenience function to output a "Starting xxx" message if the 

            logging is set to DEBUG with debuglevel DBG_PROCESS or higher. 

            Putting this into a separate method ensures 

            that the output form is consistent across all processes. 

 

            The process name (passed as the first argument) is put into 

            self.curproc, and is used to indicate which process failed to 

            start if there is an error (and is used in the "Started" message 

            on success).  The optional port and address information are 

            appended to the message (if present). 

        """ 

        self.curproc = process 

        if port is None and address is None: 

            logger.info(BIND10_STARTING_PROCESS, self.curproc) 

        elif address is None: 

            logger.info(BIND10_STARTING_PROCESS_PORT, self.curproc, 

                        port) 

        else: 

            logger.info(BIND10_STARTING_PROCESS_PORT_ADDRESS, 

                        self.curproc, address, port) 

 

    def log_started(self, pid = None): 

        """ 

            A convenience function to output a 'Started xxxx (PID yyyy)' 

            message.  As with starting_message(), this ensures a consistent 

            format. 

        """ 

        if pid is None: 

            logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS, self.curproc) 

        else: 

            logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS_PID, self.curproc, pid) 

 

    def process_running(self, msg, who): 

        """ 

            Some processes return a message to the Boss after they have 

            started to indicate that they are running.  The form of the 

            message is a dictionary with contents {"running:", "<process>"}. 

            This method checks the passed message and returns True if the 

            "who" process is contained in the message (so is presumably 

            running).  It returns False for all other conditions and will 

            log an error if appropriate. 

        """ 

        if msg is not None: 

            try: 

                if msg["running"] == who: 

                    return True 

                else: 

                    logger.error(BIND10_STARTUP_UNEXPECTED_MESSAGE, msg) 

            except: 

                logger.error(BIND10_STARTUP_UNRECOGNISED_MESSAGE, msg) 

 

        return False 

 

    # The next few methods start the individual processes of BIND-10.  They 

    # are called via start_all_processes().  If any fail, an exception is 

    # raised which is caught by the caller of start_all_processes(); this kills 

    # processes started up to that point before terminating the program. 

 

    def start_msgq(self): 

        """ 

            Start the message queue and connect to the command channel. 

        """ 

        self.log_starting("b10-msgq") 

        msgq_proc = ProcessInfo("b10-msgq", ["b10-msgq"], self.c_channel_env, 

                                True, not self.verbose) 

        msgq_proc.spawn() 

        self.log_started(msgq_proc.pid) 

 

        # Now connect to the c-channel 

        cc_connect_start = time.time() 

        while self.cc_session is None: 

            # if we have been trying for "a while" give up 

            if (time.time() - cc_connect_start) > 5: 

                raise CChannelConnectError("Unable to connect to c-channel after 5 seconds") 

 

            # try to connect, and if we can't wait a short while 

            try: 

                self.cc_session = isc.cc.Session(self.msgq_socket_file) 

            except isc.cc.session.SessionError: 

                time.sleep(0.1) 

 

        # Subscribe to the message queue.  The only messages we expect to receive 

        # on this channel are once relating to process startup. 

        self.cc_session.group_subscribe("Boss") 

 

        return msgq_proc 

 

    def start_cfgmgr(self): 

        """ 

            Starts the configuration manager process 

        """ 

        self.log_starting("b10-cfgmgr") 

        args = ["b10-cfgmgr"] 

        if self.data_path is not None: 

            args.append("--data-path=" + self.data_path) 

        if self.config_filename is not None: 

            args.append("--config-filename=" + self.config_filename) 

        if self.clear_config: 

            args.append("--clear-config") 

        bind_cfgd = ProcessInfo("b10-cfgmgr", args, 

                                self.c_channel_env) 

        bind_cfgd.spawn() 

        self.log_started(bind_cfgd.pid) 

 

        # Wait for the configuration manager to start up as subsequent initialization 

        # cannot proceed without it.  The time to wait can be set on the command line. 

        time_remaining = self.wait_time 

        msg, env = self.cc_session.group_recvmsg() 

        while time_remaining > 0 and not self.process_running(msg, "ConfigManager"): 

            logger.debug(DBG_PROCESS, BIND10_WAIT_CFGMGR) 

            time.sleep(1) 

            time_remaining = time_remaining - 1 

            msg, env = self.cc_session.group_recvmsg() 

 

        if not self.process_running(msg, "ConfigManager"): 

            raise ProcessStartError("Configuration manager process has not started") 

 

        return bind_cfgd 

 

    def start_ccsession(self, c_channel_env): 

        """ 

            Start the CC Session 

 

            The argument c_channel_env is unused but is supplied to keep the 

            argument list the same for all start_xxx methods. 

 

            With regards to logging, note that as the CC session is not a 

            process, the log_starting/log_started methods are not used. 

        """ 

        logger.info(BIND10_STARTING_CC) 

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

                                      self.config_handler, 

                                      self.command_handler, 

                                      socket_file = self.msgq_socket_file) 

        self.ccs.start() 

        logger.debug(DBG_PROCESS, BIND10_STARTED_CC) 

 

    # A couple of utility methods for starting processes... 

 

    def start_process(self, name, args, c_channel_env, port=None, address=None): 

        """ 

            Given a set of command arguments, start the process and output 

            appropriate log messages.  If the start is successful, the process 

            is added to the list of started processes. 

 

            The port and address arguments are for log messages only. 

        """ 

        self.log_starting(name, port, address) 

        newproc = ProcessInfo(name, args, c_channel_env) 

        newproc.spawn() 

        self.log_started(newproc.pid) 

        return newproc 

 

    def register_process(self, pid, component): 

        """ 

        Put another process into boss to watch over it.  When the process 

        dies, the component.failed() is called with the exit code. 

 

        It is expected the info is a isc.bind10.component.BaseComponent 

        subclass (or anything having the same interface). 

        """ 

        self.components[pid] = component 

 

    def start_simple(self, name): 

        """ 

            Most of the BIND-10 processes are started with the command: 

 

                <process-name> [-v] 

 

            ... where -v is appended if verbose is enabled.  This method 

            generates the arguments from the name and starts the process. 

 

            The port and address arguments are for log messages only. 

        """ 

        # Set up the command arguments. 

        args = [name] 

        if self.verbose: 

            args += ['-v'] 

 

        # ... and start the process 

        return self.start_process(name, args, self.c_channel_env) 

 

    # The next few methods start up the rest of the BIND-10 processes. 

    # Although many of these methods are little more than a call to 

    # start_simple, they are retained (a) for testing reasons and (b) as a place 

    # where modifications can be made if the process start-up sequence changes 

    # for a given process. 

 

    def start_auth(self): 

        """ 

            Start the Authoritative server 

        """ 

        if self.uid is not None and self.__started: 

            logger.warn(BIND10_START_AS_NON_ROOT_AUTH) 

        authargs = ['b10-auth'] 

        if self.nocache: 

            authargs += ['-n'] 

        if self.verbose: 

            authargs += ['-v'] 

 

        # ... and start 

        return self.start_process("b10-auth", authargs, self.c_channel_env) 

 

    def start_resolver(self): 

        """ 

            Start the Resolver.  At present, all these arguments and switches 

            are pure speculation.  As with the auth daemon, they should be 

            read from the configuration database. 

        """ 

        if self.uid is not None and self.__started: 

            logger.warn(BIND10_START_AS_NON_ROOT_RESOLVER) 

        self.curproc = "b10-resolver" 

        # XXX: this must be read from the configuration manager in the future 

        resargs = ['b10-resolver'] 

        if self.verbose: 

            resargs += ['-v'] 

 

        # ... and start 

        return self.start_process("b10-resolver", resargs, self.c_channel_env) 

 

    def start_cmdctl(self): 

        """ 

            Starts the command control process 

        """ 

        args = ["b10-cmdctl"] 

        if self.cmdctl_port is not None: 

            args.append("--port=" + str(self.cmdctl_port)) 

        if self.verbose: 

            args.append("-v") 

        return self.start_process("b10-cmdctl", args, self.c_channel_env, 

                                  self.cmdctl_port) 

 

    def start_all_components(self): 

        """ 

            Starts up all the components.  Any exception generated during the 

            starting of the components is handled by the caller. 

        """ 

        # Start the real core (sockcreator, msgq, cfgmgr) 

        self._component_configurator.startup(self.__core_components) 

 

        # Connect to the msgq. This is not a process, so it's not handled 

        # inside the configurator. 

        self.start_ccsession(self.c_channel_env) 

 

        # Extract the parameters associated with Bob.  This can only be 

        # done after the CC Session is started.  Note that the logging 

        # configuration may override the "-v" switch set on the command line. 

        self._read_bind10_config() 

 

        # TODO: Return the dropping of privileges 

 

    def startup(self): 

        """ 

            Start the BoB instance. 

 

            Returns None if successful, otherwise an string describing the 

            problem. 

        """ 

        # Try to connect to the c-channel daemon, to see if it is already 

        # running 

        c_channel_env = {} 

        if self.msgq_socket_file is not None: 

             c_channel_env["BIND10_MSGQ_SOCKET_FILE"] = self.msgq_socket_file 

        logger.debug(DBG_PROCESS, BIND10_CHECK_MSGQ_ALREADY_RUNNING) 

        # try to connect, and if we can't wait a short while 

        try: 

            self.cc_session = isc.cc.Session(self.msgq_socket_file) 

            logger.fatal(BIND10_MSGQ_ALREADY_RUNNING) 

            return "b10-msgq already running, or socket file not cleaned , cannot start" 

        except isc.cc.session.SessionError: 

            # this is the case we want, where the msgq is not running 

            pass 

 

        # Start all components.  If any one fails to start, kill all started 

        # components and exit with an error indication. 

        try: 

            self.c_channel_env = c_channel_env 

            self.start_all_components() 

        except Exception as e: 

            self.kill_started_components() 

            return "Unable to start " + self.curproc + ": " + str(e) 

 

        # Started successfully 

        self.runnable = True 

        self.__started = True 

        return None 

 

    def stop_process(self, process, recipient, pid): 

        """ 

        Stop the given process, friendly-like. The process is the name it has 

        (in logs, etc), the recipient is the address on msgq. The pid is the 

        pid of the process (if we have multiple processes of the same name, 

        it might want to choose if it is for this one). 

        """ 

        logger.info(BIND10_STOP_PROCESS, process) 

        self.cc_session.group_sendmsg(isc.config.ccsession. 

                                      create_command('shutdown', {'pid': pid}), 

                                      recipient, recipient) 

 

    def component_shutdown(self, exitcode=0): 

        """ 

        Stop the Boss instance from a components' request. The exitcode 

        indicates the desired exit code. 

 

        If we did not start yet, it raises an exception, which is meant 

        to propagate through the component and configurator to the startup 

        routine and abort the startup immediately. If it is started up already, 

        we just mark it so we terminate soon. 

 

        It does set the exit code in both cases. 

        """ 

        self.exitcode = exitcode 

        if not self.__started: 

            raise Exception("Component failed during startup"); 

        else: 

            self.runnable = False 

 

    def shutdown(self): 

        """Stop the BoB instance.""" 

        logger.info(BIND10_SHUTDOWN) 

        # If ccsession is still there, inform rest of the system this module 

        # is stopping. Since everything will be stopped shortly, this is not 

        # really necessary, but this is done to reflect that boss is also 

        # 'just' a module. 

        self.ccs.send_stopping() 

 

        # try using the BIND 10 request to stop 

        try: 

            self._component_configurator.shutdown() 

        except: 

            pass 

        # XXX: some delay probably useful... how much is uncertain 

        # I have changed the delay from 0.5 to 1, but sometime it's  

        # still not enough. 

        time.sleep(1) 

        self.reap_children() 

 

        # Send TERM and KILL signals to modules if we're not prevented 

        # from doing so 

        if not self.nokill: 

            # next try sending a SIGTERM 

            components_to_stop = list(self.components.values()) 

            for component in components_to_stop: 

                logger.info(BIND10_SEND_SIGTERM, component.name(), component.pid()) 

                try: 

                    component.kill() 

                except OSError: 

                    # ignore these (usually ESRCH because the child 

                    # finally exited) 

                    pass 

            # finally, send SIGKILL (unmaskable termination) until everybody dies 

            while self.components: 

                # XXX: some delay probably useful... how much is uncertain 

                time.sleep(0.1) 

                self.reap_children() 

                components_to_stop = list(self.components.values()) 

                for component in components_to_stop: 

                    logger.info(BIND10_SEND_SIGKILL, component.name(), 

                                component.pid()) 

                    try: 

                        component.kill(True) 

                    except OSError: 

                        # ignore these (usually ESRCH because the child 

                        # finally exited) 

                        pass 

            logger.info(BIND10_SHUTDOWN_COMPLETE) 

 

    def _get_process_exit_status(self): 

        return os.waitpid(-1, os.WNOHANG) 

 

    def reap_children(self): 

        """Check to see if any of our child processes have exited,  

        and note this for later handling.  

        """ 

        while True: 

            try: 

                (pid, exit_status) = self._get_process_exit_status() 

            except OSError as o: 

754   755                if o.errno == errno.ECHILD: break 

                # XXX: should be impossible to get any other error here 

                raise 

            if pid == 0: break 

            if pid in self.components: 

                # One of the components we know about.  Get information on it. 

                component = self.components.pop(pid) 

                logger.info(BIND10_PROCESS_ENDED, component.name(), pid, 

                            exit_status) 

                if component.running() and self.runnable: 

                    # Tell it it failed. But only if it matters (we are 

                    # not shutting down and the component considers itself 

                    # to be running. 

                    component_restarted = component.failed(exit_status); 

                    # if the process wants to be restarted, but not just yet, 

                    # it returns False 

                    if not component_restarted: 

                        self.components_to_restart.append(component) 

            else: 

                logger.info(BIND10_UNKNOWN_CHILD_PROCESS_ENDED, pid) 

 

    def restart_processes(self): 

        """ 

            Restart any dead processes: 

 

            * Returns the time when the next process is ready to be restarted.  

            * If the server is shutting down, returns 0. 

            * If there are no processes, returns None. 

 

            The values returned can be safely passed into select() as the  

            timeout value. 

 

        """ 

786        if not self.runnable: 

            return 0 

        still_dead = [] 

        # keep track of the first time we need to check this queue again, 

        # if at all 

        next_restart_time = None 

        now = time.time() 

793        for component in self.components_to_restart: 

            if not component.restart(now): 

                still_dead.append(component) 

                if next_restart_time is None or\ 

                   next_restart_time > component.get_restart_time(): 

                    next_restart_time = component.get_restart_time() 

        self.components_to_restart = still_dead 

 

        return next_restart_time 

 

    def _get_socket(self, args): 

        """ 

        Implementation of the get_socket CC command. It asks the cache 

        to provide the token and sends the information back. 

        """ 

        try: 

            try: 

                addr = isc.net.parse.addr_parse(args['address']) 

                port = isc.net.parse.port_parse(args['port']) 

                protocol = args['protocol'] 

                if protocol not in ['UDP', 'TCP']: 

                    raise ValueError("Protocol must be either UDP or TCP") 

                share_mode = args['share_mode'] 

                if share_mode not in ['ANY', 'SAMEAPP', 'NO']: 

                    raise ValueError("Share mode must be one of ANY, SAMEAPP" + 

                                     " or NO") 

                share_name = args['share_name'] 

            except KeyError as ke: 

                return \ 

                    isc.config.ccsession.create_answer(1, 

                                                       "Missing parameter " + 

                                                       str(ke)) 

 

            # FIXME: This call contains blocking IPC. It is expected to be 

            # short, but if it turns out to be problem, we'll need to do 

            # something about it. 

            token = self._socket_cache.get_token(protocol, addr, port, 

                                                 share_mode, share_name) 

            return isc.config.ccsession.create_answer(0, { 

                'token': token, 

                'path': self._socket_path 

            }) 

        except isc.bind10.socket_cache.SocketError as e: 

            return isc.config.ccsession.create_answer(CREATOR_SOCKET_ERROR, 

                                                      str(e)) 

        except isc.bind10.socket_cache.ShareError as e: 

            return isc.config.ccsession.create_answer(CREATOR_SHARE_ERROR, 

                                                      str(e)) 

        except Exception as e: 

            return isc.config.ccsession.create_answer(1, str(e)) 

 

    def socket_request_handler(self, token, unix_socket): 

        """ 

        This function handles a token that comes over a unix_domain socket. 

        The function looks into the _socket_cache and sends the socket 

        identified by the token back over the unix_socket. 

        """ 

        try: 

            token = str(token, 'ASCII') # Convert from bytes to str 

            fd = self._socket_cache.get_socket(token, unix_socket.fileno()) 

            # FIXME: These two calls are blocking in their nature. An OS-level 

            # buffer is likely to be large enough to hold all these data, but 

            # if it wasn't and the remote application got stuck, we would have 

            # a problem. If there appear such problems, we should do something 

            # about it. 

            unix_socket.sendall(CREATOR_SOCKET_OK) 

            libutil_io_python.send_fd(unix_socket.fileno(), fd) 

        except Exception as e: 

            logger.info(BIND10_NO_SOCKET, token, e) 

            unix_socket.sendall(CREATOR_SOCKET_UNAVAILABLE) 

 

    def socket_consumer_dead(self, unix_socket): 

        """ 

        This function handles when a unix_socket closes. This means all 

        sockets sent to it are to be considered closed. This function signals 

        so to the _socket_cache. 

        """ 

        logger.info(BIND10_LOST_SOCKET_CONSUMER, unix_socket.fileno()) 

        try: 

            self._socket_cache.drop_application(unix_socket.fileno()) 

        except ValueError: 

            # This means the application holds no sockets. It's harmless, as it 

            # can happen in real life - for example, it requests a socket, but 

            # get_socket doesn't find it, so the application dies. It should be 

            # rare, though. 

            pass 

 

    def set_creator(self, creator): 

        """ 

        Registeres a socket creator into the boss. The socket creator is not 

        used directly, but through a cache. The cache is created in this 

        method. 

 

        If called more than once, it raises a ValueError. 

        """ 

        if self._socket_cache is not None: 

            raise ValueError("A creator was inserted previously") 

        self._socket_cache = isc.bind10.socket_cache.Cache(creator) 

 

    def init_socket_srv(self): 

        """ 

        Creates and listens on a unix-domain socket to be able to send out 

        the sockets. 

 

        This method should be called after switching user, or the switched 

        applications won't be able to access the socket. 

        """ 

        self._srv_socket = socket.socket(socket.AF_UNIX) 

        # We create a temporary directory somewhere safe and unique, to avoid 

        # the need to find the place ourself or bother users. Also, this 

        # secures the socket on some platforms, as it creates a private 

        # directory. 

        self._tmpdir = tempfile.mkdtemp(prefix='sockcreator-') 

        # Get the name 

        self._socket_path = os.path.join(self._tmpdir, "sockcreator") 

        # And bind the socket to the name 

        self._srv_socket.bind(self._socket_path) 

        self._srv_socket.listen(5) 

 

    def remove_socket_srv(self): 

        """ 

        Closes and removes the listening socket and the directory where it 

        lives, as we created both. 

 

        It does nothing if the _srv_socket is not set (eg. it was not yet 

        initialized). 

        """ 

        if self._srv_socket is not None: 

            self._srv_socket.close() 

            os.remove(self._socket_path) 

            os.rmdir(self._tmpdir) 

 

    def _srv_accept(self): 

        """ 

        Accept a socket from the unix domain socket server and put it to the 

        others we care about. 

        """ 

        (socket, conn) = self._srv_socket.accept() 

        self._unix_sockets[socket.fileno()] = (socket, b'') 

 

    def _socket_data(self, socket_fileno): 

        """ 

        This is called when a socket identified by the socket_fileno needs 

        attention. We try to read data from there. If it is closed, we remove 

        it. 

        """ 

        (sock, previous) = self._unix_sockets[socket_fileno] 

        while True: 

            try: 

                data = sock.recv(1, socket.MSG_DONTWAIT) 

            except socket.error as se: 

                # These two might be different on some systems 

                if se.errno == errno.EAGAIN or se.errno == errno.EWOULDBLOCK: 

                    # No more data now. Oh, well, just store what we have. 

                    self._unix_sockets[socket_fileno] = (sock, previous) 

                    return 

                else: 

exit                    data = b'' # Pretend it got closed 

            if len(data) == 0: # The socket got to it's end 

                del self._unix_sockets[socket_fileno] 

                self.socket_consumer_dead(sock) 

                sock.close() 

                return 

            else: 

                if data == b"\n": 

                    # Handle this token and clear it 

                    self.socket_request_handler(previous, sock) 

                    previous = b'' 

                else: 

                    previous += data 

 

    def run(self, wakeup_fd): 

        """ 

        The main loop, waiting for sockets, commands and dead processes. 

        Runs as long as the runnable is true. 

 

        The wakeup_fd descriptor is the read end of pipe where CHLD signal 

        handler writes. 

        """ 

        ccs_fd = self.ccs.get_socket().fileno() 

        while self.runnable: 

            # clean up any processes that exited 

            self.reap_children() 

            next_restart = self.restart_processes() 

979            if next_restart is None: 

                wait_time = None 

            else: 

                wait_time = max(next_restart - time.time(), 0) 

 

            # select() can raise EINTR when a signal arrives, 

            # even if they are resumable, so we have to catch 

            # the exception 

            try: 

                (rlist, wlist, xlist) = \ 

                    select.select([wakeup_fd, ccs_fd, 

                                   self._srv_socket.fileno()] + 

                                   list(self._unix_sockets.keys()), [], [], 

                                  wait_time) 

            except select.error as err: 

                if err.args[0] == errno.EINTR: 

                    (rlist, wlist, xlist) = ([], [], []) 

                else: 

                    logger.fatal(BIND10_SELECT_ERROR, err) 

                    break 

 

            for fd in rlist + xlist: 

999                if fd == ccs_fd: 

                    try: 

                        self.ccs.check_command() 

                    except isc.cc.session.ProtocolError: 

                        logger.fatal(BIND10_MSGQ_DISAPPEARED) 

                        self.runnable = False 

                        break 

1006                elif fd == wakeup_fd: 

                    os.read(wakeup_fd, 32) 

                elif fd == self._srv_socket.fileno(): 

                    self._srv_accept() 

997                elif fd in self._unix_sockets: 

                    self._socket_data(fd) 

 

# global variables, needed for signal handlers 

options = None 

boss_of_bind = None 

 

def reaper(signal_number, stack_frame): 

    """A child process has died (SIGCHLD received).""" 

    # don't do anything...  

    # the Python signal handler has been set up to write 

    # down a pipe, waking up our select() bit 

    pass 

 

def get_signame(signal_number): 

    """Return the symbolic name for a signal.""" 

    for sig in dir(signal): 

        if sig.startswith("SIG") and sig[3].isalnum(): 

            if getattr(signal, sig) == signal_number: 

                return sig 

    return "Unknown signal %d" % signal_number 

 

# XXX: perhaps register atexit() function and invoke that instead 

def fatal_signal(signal_number, stack_frame): 

    """We need to exit (SIGINT or SIGTERM received).""" 

    global options 

    global boss_of_bind 

    logger.info(BIND10_RECEIVED_SIGNAL, get_signame(signal_number)) 

    signal.signal(signal.SIGCHLD, signal.SIG_DFL) 

    boss_of_bind.runnable = False 

 

def process_rename(option, opt_str, value, parser): 

    """Function that renames the process if it is requested by a option.""" 

    isc.util.process.rename(value) 

 

def parse_args(args=sys.argv[1:], Parser=OptionParser): 

    """ 

    Function for parsing command line arguments. Returns the 

    options object from OptionParser. 

    """ 

    parser = Parser(version=VERSION) 

    parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file", 

                      type="string", default=None, 

                      help="UNIX domain socket file the b10-msgq daemon will use") 

    parser.add_option("-n", "--no-cache", action="store_true", dest="nocache", 

                      default=False, help="disable hot-spot cache in authoritative DNS server") 

    parser.add_option("-i", "--no-kill", action="store_true", dest="nokill", 

                      default=False, help="do not send SIGTERM and SIGKILL signals to modules during shutdown") 

    parser.add_option("-u", "--user", dest="user", type="string", default=None, 

                      help="Change user after startup (must run as root)") 

    parser.add_option("-v", "--verbose", dest="verbose", action="store_true", 

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

    parser.add_option("--pretty-name", type="string", action="callback", 

                      callback=process_rename, 

                      help="Set the process name (displayed in ps, top, ...)") 

    parser.add_option("-c", "--config-file", action="store", 

                      dest="config_file", default=None, 

                      help="Configuration database filename") 

    parser.add_option("--clear-config", action="store_true", 

                      dest="clear_config", default=False, 

                      help="Create backup of the configuration file and " + 

                           "start with a clean configuration") 

    parser.add_option("-p", "--data-path", dest="data_path", 

                      help="Directory to search for configuration files", 

                      default=None) 

    parser.add_option("--cmdctl-port", dest="cmdctl_port", type="int", 

                      default=None, help="Port of command control") 

    parser.add_option("--pid-file", dest="pid_file", type="string", 

                      default=None, 

                      help="file to dump the PID of the BIND 10 process") 

    parser.add_option("-w", "--wait", dest="wait_time", type="int", 

                      default=10, help="Time (in seconds) to wait for config manager to start up") 

 

    (options, args) = parser.parse_args(args) 

 

    if options.cmdctl_port is not None: 

        try: 

            isc.net.parse.port_parse(options.cmdctl_port) 

        except ValueError as e: 

            parser.error(e) 

 

1091    if args: 

        parser.print_help() 

        sys.exit(1) 

 

    return options 

 

def dump_pid(pid_file): 

    """ 

    Dump the PID of the current process to the specified file.  If the given 

    file is None this function does nothing.  If the file already exists, 

    the existing content will be removed.  If a system error happens in 

    creating or writing to the file, the corresponding exception will be 

    propagated to the caller. 

    """ 

    if pid_file is None: 

        return 

    f = open(pid_file, "w") 

    f.write('%d\n' % os.getpid()) 

    f.close() 

 

def unlink_pid_file(pid_file): 

    """ 

    Remove the given file, which is basically expected to be the PID file 

    created by dump_pid().  The specified may or may not exist; if it 

    doesn't this function does nothing.  Other system level errors in removing 

    the file will be propagated as the corresponding exception. 

    """ 

    if pid_file is None: 

        return 

    try: 

        os.unlink(pid_file) 

    except OSError as error: 

1123        if error.errno is not errno.ENOENT: 

            raise 

 

 

def main(): 

    global options 

    global boss_of_bind 

    # Enforce line buffering on stdout, even when not a TTY 

    sys.stdout = io.TextIOWrapper(sys.stdout.detach(), line_buffering=True) 

 

    options = parse_args() 

 

    # Check user ID. 

    setuid = None 

    username = None 

    if options.user: 

        # Try getting information about the user, assuming UID passed. 

        try: 

            pw_ent = pwd.getpwuid(int(options.user)) 

            setuid = pw_ent.pw_uid 

            username = pw_ent.pw_name 

        except ValueError: 

            pass 

        except KeyError: 

            pass 

 

        # Next try getting information about the user, assuming user name  

        # passed. 

        # If the information is both a valid user name and user number, we 

        # prefer the name because we try it second. A minor point, hopefully. 

        try: 

            pw_ent = pwd.getpwnam(options.user) 

            setuid = pw_ent.pw_uid 

            username = pw_ent.pw_name 

        except KeyError: 

            pass 

 

        if setuid is None: 

            logger.fatal(BIND10_INVALID_USER, options.user) 

            sys.exit(1) 

 

    # Announce startup. 

    logger.info(BIND10_STARTING, VERSION) 

 

    # Create wakeup pipe for signal handlers 

    wakeup_pipe = os.pipe() 

    signal.set_wakeup_fd(wakeup_pipe[1]) 

 

    # Set signal handlers for catching child termination, as well 

    # as our own demise. 

    signal.signal(signal.SIGCHLD, reaper) 

    signal.siginterrupt(signal.SIGCHLD, False) 

    signal.signal(signal.SIGINT, fatal_signal) 

    signal.signal(signal.SIGTERM, fatal_signal) 

 

    # Block SIGPIPE, as we don't want it to end this process 

    signal.signal(signal.SIGPIPE, signal.SIG_IGN) 

 

    try: 

        # Go bob! 

        boss_of_bind = BoB(options.msgq_socket_file, options.data_path, 

                           options.config_file, options.clear_config, 

                           options.nocache, options.verbose, options.nokill, 

                           setuid, username, options.cmdctl_port, 

                           options.wait_time) 

        startup_result = boss_of_bind.startup() 

        if startup_result: 

            logger.fatal(BIND10_STARTUP_ERROR, startup_result) 

            sys.exit(1) 

        boss_of_bind.init_socket_srv() 

        logger.info(BIND10_STARTUP_COMPLETE) 

        dump_pid(options.pid_file) 

 

        # Let it run 

        boss_of_bind.run(wakeup_pipe[0]) 

 

        # shutdown 

        signal.signal(signal.SIGCHLD, signal.SIG_DFL) 

        boss_of_bind.shutdown() 

    finally: 

        # Clean up the filesystem 

        unlink_pid_file(options.pid_file) 

        if boss_of_bind is not None: 

            boss_of_bind.remove_socket_srv() 

    sys.exit(boss_of_bind.exitcode) 

 

1209if __name__ == "__main__": 

    main()