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

# Authors: 

#   Jason Gerard DeRose <jderose@redhat.com> 

# 

# Copyright (C) 2008  Red Hat 

# see file 'COPYING' for use and warranty information 

# 

# This program is free software; you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# This program is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the GNU General Public License 

# along with this program.  If not, see <http://www.gnu.org/licenses/>. 

 

 

''' 

Package containing the core library. 

 

============================= 

Tutorial for Plugin Authors 

============================= 

 

This tutorial will introduce you to writing plugins for freeIPA v2. It does 

not cover every detail, but it provides enough to get you started and is 

heavily cross-referenced with further documentation that (hopefully) fills 

in the missing details. 

 

In addition to this tutorial, the many built-in plugins in `ipalib.plugins` 

and `ipaserver.plugins` provide real-life examples of how to write good 

plugins. 

 

 

---------------------------- 

How this tutorial is written 

---------------------------- 

 

The code examples in this tutorial are presented as if entered into a Python 

interactive interpreter session.  As such, when you create a real plugin in 

a source file, a few details will be different (in addition to the fact that 

you will never include the ``>>>`` nor ``...`` that the interpreter places at 

the beginning of each line of code). 

 

The tutorial examples all have this pattern: 

 

    :: 

 

        >>> from ipalib import Command, create_api 

        >>> api = create_api() 

        >>> class my_command(Command): 

        ...     pass 

        ... 

        >>> api.register(my_command) 

        >>> api.finalize() 

 

In the tutorial we call `create_api()` to create an *example* instance 

of `plugable.API` to work with.  But a real plugin will simply use 

``ipalib.api``, the standard run-time instance of `plugable.API`. 

 

A real plugin will have this pattern: 

 

    :: 

 

        from ipalib import Command, api 

 

        class my_command(Command): 

            pass 

        api.register(my_command) 

 

As seen above, also note that in a real plugin you will *not* call 

`plugable.API.finalize()`.  When in doubt, look at some of the built-in 

plugins for guidance, like those in `ipalib.plugins`. 

 

If you don't know what the Python *interactive interpreter* is, or are 

confused about what this *Python* is in the first place, then you probably 

should start with the Python tutorial: 

 

    http://docs.python.org/tutorial/index.html 

 

 

------------------------------------ 

First steps: A simple command plugin 

------------------------------------ 

 

Our first example will create the most basic command plugin possible.  This 

command will be seen in the list of command plugins, but it wont be capable 

of actually doing anything yet. 

 

A command plugin simultaneously adds a new command that can be called through 

the command-line ``ipa`` script *and* adds a new XML-RPC method... the two are 

one in the same, simply invoked in different ways. 

 

A freeIPA plugin is a Python class, and when you create a plugin, you register 

this class itself (instead of an instance of the class).  To be a command 

plugin, your plugin must subclass from `frontend.Command` (or from a subclass 

thereof).  Here is our first example: 

 

>>> from ipalib import Command, create_api 

>>> api = create_api() 

>>> class my_command(Command): # Step 1, define class 

...     """My example plugin.""" 

... 

>>> api.register(my_command) # Step 2, register class 

 

Notice that we are registering the ``my_command`` class itself, not an 

instance of ``my_command``. 

 

Until `plugable.API.finalize()` is called, your plugin class has not been 

instantiated nor does the ``Command`` namespace yet exist.  For example: 

 

>>> hasattr(api, 'Command') 

False 

>>> api.finalize() # plugable.API.finalize() 

>>> hasattr(api.Command, 'my_command') 

True 

>>> api.Command.my_command.doc 

Gettext('My example plugin.', domain='ipa', localedir=None) 

 

Notice that your plugin instance is accessed through an attribute named 

``my_command``, the same name as your plugin class name. 

 

 

------------------------------ 

Make your command do something 

------------------------------ 

 

This simplest way to make your example command plugin do something is to 

implement a ``run()`` method, like this: 

 

>>> class my_command(Command): 

...     """My example plugin with run().""" 

... 

...     def run(self, **options): 

...         return dict(result='My run() method was called!') 

... 

>>> api = create_api() 

>>> api.register(my_command) 

>>> api.finalize() 

>>> api.Command.my_command() # Call your command 

{'result': 'My run() method was called!'} 

 

When `frontend.Command.__call__()` is called, it first validates any arguments 

and options your command plugin takes (if any) and then calls its ``run()`` 

method. 

 

 

------------------------ 

Forwarding vs. execution 

------------------------ 

 

However, unlike the example above, a typical command plugin will implement an 

``execute()`` method instead of a ``run()`` method.  Your command plugin can 

be loaded in two distinct contexts: 

 

    1. In a *client* context - Your command plugin is only used to validate 

       any arguments and options it takes, and then ``self.forward()`` is 

       called, which forwards the call over XML-RPC to an IPA server where 

       the actual work is done. 

 

    2. In a *server* context - Your same command plugin validates any 

       arguments and options it takes, and then ``self.execute()`` is called, 

       which you should implement to perform whatever work your plugin does. 

 

The base `frontend.Command.run()` method simply dispatches the call to 

``self.execute()`` if ``self.env.in_server`` is True, or otherwise 

dispatches the call to ``self.forward()``. 

 

For example, say you have a command plugin like this: 

 

>>> class my_command(Command): 

...     """Forwarding vs. execution.""" 

... 

...     def forward(self, **options): 

...         return dict( 

...             result='forward(): in_server=%r' % self.env.in_server 

...         ) 

... 

...     def execute(self, **options): 

...         return dict( 

...             result='execute(): in_server=%r' % self.env.in_server 

...         ) 

... 

 

The ``options`` will contain a dict of command options. One option is added 

automatically: ``version``. It contains the API version of the client. 

In order to maintain forward compatibility, you should always specify the 

API version current at the time you're writing your client. 

 

If ``my_command`` is loaded in a *client* context, ``forward()`` will be 

called: 

 

>>> api = create_api() 

>>> api.env.in_server = False # run() will dispatch to forward() 

>>> api.register(my_command) 

>>> api.finalize() 

>>> api.Command.my_command(version=u'2.47') # Call your command plugin 

{'result': 'forward(): in_server=False'} 

 

On the other hand, if ``my_command`` is loaded in a *server* context, 

``execute()`` will be called: 

 

>>> api = create_api() 

>>> api.env.in_server = True # run() will dispatch to execute() 

>>> api.register(my_command) 

>>> api.finalize() 

>>> api.Command.my_command(version=u'2.47') # Call your command plugin 

{'result': 'execute(): in_server=True'} 

 

Normally there should be no reason to override `frontend.Command.forward()`, 

but, as above, it can be done for demonstration purposes.  In contrast, there 

*is* a reason you might want to override `frontend.Command.run()`: if it only 

makes sense to execute your command locally, if it should never be forwarded 

to the server.  In this case, you should implement your *do-stuff* in the 

``run()`` method instead of in the ``execute()`` method. 

 

For example, the ``ipa`` command line script has a ``help`` command 

(`ipalib.cli.help`) that is specific to the command-line-interface and should 

never be forwarded to the server. 

 

 

--------------- 

Backend plugins 

--------------- 

 

There are two types of plugins: 

 

    1. *Frontend plugins* - These are loaded in both the *client* and *server* 

       contexts.  These need to be installed with any application built atop 

       the `ipalib` library.  The built-in frontend plugins can be found in 

       `ipalib.plugins`.  The ``my_command`` example above is a frontend 

       plugin. 

 

    2. *Backend plugins* - These are only loaded in a *server* context and 

       only need to be installed on the IPA server.  The built-in backend 

       plugins can be found in `ipaserver.plugins`. 

 

Backend plugins should provide a set of methods that standardize how IPA 

interacts with some external system or library.  For example, all interaction 

with LDAP is done through the ``ldap`` backend plugin defined in 

`ipaserver.plugins.b_ldap`.  As a good rule of thumb, anytime you need to 

import some package that is not part of the Python standard library, you 

should probably interact with that package via a corresponding backend 

plugin you implement. 

 

Backend plugins are much more free-form than command plugins.  Aside from a 

few reserved attribute names, you can define arbitrary public methods on your 

backend plugin. 

 

Here is a simple example: 

 

>>> from ipalib import Backend 

>>> class my_backend(Backend): 

...     """My example backend plugin.""" 

... 

...     def do_stuff(self): 

...         """Part of your API.""" 

...         return 'Stuff got done.' 

... 

>>> api = create_api() 

>>> api.register(my_backend) 

>>> api.finalize() 

>>> api.Backend.my_backend.do_stuff() 

'Stuff got done.' 

 

 

------------------------------- 

How your command should do work 

------------------------------- 

 

We now return to our ``my_command`` plugin example. 

 

Plugins are separated into frontend and backend plugins so that there are not 

unnecessary dependencies required by an application that only uses `ipalib` and 

its built-in frontend plugins (and then forwards over XML-RPC for execution). 

 

But how do we avoid introducing additional dependencies?  For example, the 

``user_add`` command needs to talk to LDAP to add the user, yet we want to 

somehow load the ``user_add`` plugin on client machines without requiring the 

``python-ldap`` package (Python bindings to openldap) to be installed.  To 

answer that, we consult our golden rule: 

 

  **The golden rule:** A command plugin should implement its ``execute()`` 

  method strictly via calls to methods on one or more backend plugins. 

 

So the module containing the ``user_add`` command does not itself import the 

Python LDAP bindings, only the module containing the ``ldap`` backend plugin 

does that, and the backend plugins are only installed on the server.  The 

``user_add.execute()`` method, which is only called when in a server context, 

is implemented as a series of calls to methods on the ``ldap`` backend plugin. 

 

When `plugable.Plugin.set_api()` is called, each plugin stores a reference to 

the `plugable.API` instance it has been loaded into.  So your plugin can 

access the ``my_backend`` plugin as ``self.api.Backend.my_backend``. 

 

Additionally, convenience attributes are set for each namespace, so your 

plugin can also access the ``my_backend`` plugin as simply 

``self.Backend.my_backend``. 

 

This next example will tie everything together.  First we create our backend 

plugin: 

 

>>> api = create_api() 

>>> api.env.in_server = True # We want to execute, not forward 

>>> class my_backend(Backend): 

...     """My example backend plugin.""" 

... 

...     def do_stuff(self): 

...         """my_command.execute() calls this.""" 

...         return 'my_backend.do_stuff() indeed did do stuff!' 

... 

>>> api.register(my_backend) 

 

Second, we have our frontend plugin, the command: 

 

>>> class my_command(Command): 

...     """My example command plugin.""" 

... 

...     def execute(self, **options): 

...         """Implemented against Backend.my_backend""" 

...         return dict(result=self.Backend.my_backend.do_stuff()) 

... 

>>> api.register(my_command) 

 

Lastly, we call ``api.finalize()`` and see what happens when we call 

``my_command()``: 

 

>>> api.finalize() 

>>> api.Command.my_command(version=u'2.47') 

{'result': 'my_backend.do_stuff() indeed did do stuff!'} 

 

When not in a server context, ``my_command.execute()`` never gets called, so 

it never tries to access the non-existent backend plugin at 

``self.Backend.my_backend.``  To emphasize this point, here is one last 

example: 

 

>>> api = create_api() 

>>> api.env.in_server = False # We want to forward, not execute 

>>> class my_command(Command): 

...     """My example command plugin.""" 

... 

...     def execute(self, **options): 

...         """Same as above.""" 

...         return dict(result=self.Backend.my_backend.do_stuff()) 

... 

...     def forward(self, **options): 

...         return dict(result='Just my_command.forward() getting called here.') 

... 

>>> api.register(my_command) 

>>> api.finalize() 

 

Notice that the ``my_backend`` plugin has certainly not be registered: 

 

>>> hasattr(api.Backend, 'my_backend') 

False 

 

And yet we can call ``my_command()``: 

 

>>> api.Command.my_command() 

{'result': 'Just my_command.forward() getting called here.'} 

 

 

---------------------------------------- 

Calling other commands from your command 

---------------------------------------- 

 

It can be useful to have your ``execute()`` method call other command plugins. 

Among other things, this allows for meta-commands that conveniently call 

several other commands in a single operation.  For example: 

 

>>> api = create_api() 

>>> api.env.in_server = True # We want to execute, not forward 

>>> class meta_command(Command): 

...     """My meta-command plugin.""" 

... 

...     def execute(self, **options): 

...         """Calls command_1(), command_2()""" 

...         msg = '%s; %s.' % ( 

...             self.Command.command_1()['result'], 

...             self.Command.command_2()['result'], 

...         ) 

...         return dict(result=msg) 

>>> class command_1(Command): 

...     def execute(self, **options): 

...         return dict(result='command_1.execute() called') 

... 

>>> class command_2(Command): 

...     def execute(self, **options): 

...         return dict(result='command_2.execute() called') 

... 

>>> api.register(meta_command) 

>>> api.register(command_1) 

>>> api.register(command_2) 

>>> api.finalize() 

>>> api.Command.meta_command(version=u'2.47') 

{'result': 'command_1.execute() called; command_2.execute() called.'} 

 

Because this is quite useful, we are going to revise our golden rule somewhat: 

 

  **The revised golden rule:** A command plugin should implement its 

  ``execute()`` method strictly via what it can access through ``self.api``, 

  most likely via the backend plugins in ``self.api.Backend`` (which can also 

  be conveniently accessed as ``self.Backend``). 

 

 

----------------------------------------------- 

Defining arguments and options for your command 

----------------------------------------------- 

 

You can define a command that will accept specific arguments and options. 

For example: 

 

>>> from ipalib import Str 

>>> class nudge(Command): 

...     """Takes one argument, one option""" 

... 

...     takes_args = ('programmer',) 

... 

...     takes_options = (Str('stuff', default=u'documentation')) 

... 

...     def execute(self, programmer, **kw): 

...         return dict( 

...             result='%s, go write more %s!' % (programmer, kw['stuff']) 

...         ) 

... 

>>> api = create_api() 

>>> api.env.in_server = True 

>>> api.register(nudge) 

>>> api.finalize() 

>>> api.Command.nudge(u'Jason', version=u'2.47') 

{'result': u'Jason, go write more documentation!'} 

>>> api.Command.nudge(u'Jason', stuff=u'unit tests', version=u'2.47') 

{'result': u'Jason, go write more unit tests!'} 

 

The ``args`` and ``options`` attributes are `plugable.NameSpace` instances 

containing a command's arguments and options, respectively, as you can see: 

 

>>> list(api.Command.nudge.args) # Iterates through argument names 

['programmer'] 

>>> api.Command.nudge.args.programmer 

Str('programmer') 

>>> list(api.Command.nudge.options) # Iterates through option names 

['stuff', 'version'] 

>>> api.Command.nudge.options.stuff 

Str('stuff', default=u'documentation') 

>>> api.Command.nudge.options.stuff.default 

u'documentation' 

 

The 'version' option is added to commands automatically. 

 

The arguments and options must not contain colliding names.  They are both 

merged together into the ``params`` attribute, another `plugable.NameSpace` 

instance, as you can see: 

 

>>> api.Command.nudge.params 

NameSpace(<3 members>, sort=False) 

>>> list(api.Command.nudge.params) # Iterates through the param names 

['programmer', 'stuff', 'version'] 

 

When calling a command, its positional arguments can also be provided as 

keyword arguments, and in any order.  For example: 

 

>>> api.Command.nudge(stuff=u'lines of code', programmer=u'Jason', version=u'2.47') 

{'result': u'Jason, go write more lines of code!'} 

 

When a command plugin is called, the values supplied for its parameters are 

put through a sophisticated processing pipeline that includes steps for 

normalization, type conversion, validation, and dynamically constructing 

the defaults for missing values.  The details wont be covered here; however, 

here is a quick teaser: 

 

>>> from ipalib import Int 

>>> class create_player(Command): 

...     takes_options = ( 

...         'first', 

...         'last', 

...         Str('nick', 

...             normalizer=lambda value: value.lower(), 

...             default_from=lambda first, last: first[0] + last, 

...         ), 

...         Int('points', default=0), 

...     ) 

... 

>>> cp = create_player() 

>>> cp.finalize() 

>>> cp.convert(points=u' 1000  ') 

{'points': 1000} 

>>> cp.normalize(nick=u'NickName') 

{'nick': u'nickname'} 

>>> cp.get_default(first=u'Jason', last=u'DeRose') 

{'nick': u'jderose', 'points': 0} 

 

For the full details on the parameter system, see the 

`frontend.parse_param_spec()` function, and the `frontend.Param` and 

`frontend.Command` classes. 

 

 

--------------------------------------- 

Allowed return values from your command 

--------------------------------------- 

 

The return values from your command can be rendered by different user 

interfaces (CLI, web-UI); furthermore, a call to your command can be 

transparently forwarded over the network (XML-RPC, JSON).  As such, the return 

values from your command must be usable by the least common denominator. 

 

Your command should return only simple data types and simple data structures, 

the kinds that can be represented in an XML-RPC request or in the JSON format. 

The return values from your command's ``execute()`` method can include only 

the following: 

 

    Simple scalar values: 

        These can be ``str``, ``unicode``, ``int``, and ``float`` instances, 

        plus the ``True``, ``False``, and ``None`` constants. 

 

    Simple compound values: 

        These can be ``dict``, ``list``, and ``tuple`` instances.  These 

        compound values must contain only the simple scalar values above or 

        other simple compound values.  These compound values can also be empty. 

        For our purposes here, the ``list`` and ``tuple`` types are equivalent 

        and can be used interchangeably. 

 

Also note that your ``execute()`` method should not contain any ``print`` 

statements or otherwise cause any output on ``sys.stdout``.  Your command can 

(and should) produce log messages by using ``self.log`` (see below). 

 

To learn more about XML-RPC (XML Remote Procedure Call), see: 

 

    http://docs.python.org/library/xmlrpclib.html 

 

    http://en.wikipedia.org/wiki/XML-RPC 

 

To learn more about JSON (Java Script Object Notation), see: 

 

    http://docs.python.org/library/json.html 

 

    http://www.json.org/ 

 

 

--------------------------------------- 

How your command should print to stdout 

--------------------------------------- 

 

As noted above, your command should not print anything while in its 

``execute()`` method.  So how does your command format its output when 

called from the ``ipa`` script? 

 

After the `cli.CLI.run_cmd()` method calls your command, it will call your 

command's ``output_for_cli()`` method (if you have implemented one). 

 

If you implement an ``output_for_cli()`` method, it must have the following 

signature: 

 

    :: 

 

        output_for_cli(textui, result, *args, **options) 

 

    textui 

        An object implementing methods for outputting to the console. 

        Currently the `ipalib.cli.textui` plugin is passed, which your method 

        can also access as ``self.Backend.textui``.  However, in case this 

        changes in the future, your method should use the instance passed to 

        it in this first argument. 

 

    result 

        This is the return value from calling your command plugin.  Depending 

        upon how your command is implemented, this is probably the return 

        value from your ``execute()`` method. 

 

    args 

        The arguments your command was called with.  If your command takes no 

        arguments, you can omit this.  You can also explicitly list your 

        arguments rather than using the generic ``*args`` form. 

 

    options 

        The options your command was called with.  If your command takes no 

        options, you can omit this.  If your command takes any options, you 

        must use the ``**options`` form as they will be provided strictly as 

        keyword arguments. 

 

For example, say we setup a command like this: 

 

>>> class show_items(Command): 

... 

...     takes_args = ('key?',) 

... 

...     takes_options = (Flag('reverse'),) 

... 

...     def execute(self, key, **options): 

...         items = dict( 

...             fruit=u'apple', 

...             pet=u'dog', 

...             city=u'Berlin', 

...         ) 

...         if key in items: 

...             return dict(result=items[key]) 

...         items = [ 

...             (k, items[k]) for k in sorted(items, reverse=options['reverse']) 

...         ] 

...         return dict(result=items) 

... 

...     def output_for_cli(self, textui, result, key, **options): 

...         result = result['result'] 

...         if key is not None: 

...             textui.print_plain('%s = %r' % (key, result)) 

...         else: 

...             textui.print_name(self.name) 

...             textui.print_keyval(result) 

...             format = '%d items' 

...             if options['reverse']: 

...                 format += ' (in reverse order)' 

...             textui.print_count(result, format) 

... 

>>> api = create_api() 

>>> api.bootstrap(in_server=True)  # We want to execute, not forward 

>>> api.register(show_items) 

>>> api.finalize() 

 

Normally when you invoke the ``ipa`` script, `cli.CLI.load_plugins()` will 

register the `cli.textui` backend plugin, but for the sake of our example, 

we will just create an instance here: 

 

>>> from ipalib import cli 

>>> textui = cli.textui()  # We'll pass this to output_for_cli() 

 

Now for what we are concerned with in this example, calling your command 

through the ``ipa`` script basically will do the following: 

 

>>> result = api.Command.show_items() 

>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=False) 

----------- 

show-items: 

----------- 

  city = u'Berlin' 

  fruit = u'apple' 

  pet = u'dog' 

------- 

3 items 

------- 

 

Similarly, calling it with ``reverse=True``  would result in the following: 

 

>>> result = api.Command.show_items(reverse=True) 

>>> api.Command.show_items.output_for_cli(textui, result, None, reverse=True) 

----------- 

show-items: 

----------- 

  pet = u'dog' 

  fruit = u'apple' 

  city = u'Berlin' 

-------------------------- 

3 items (in reverse order) 

-------------------------- 

 

Lastly, providing a ``key`` would result in the following: 

 

>>> result = api.Command.show_items(u'city') 

>>> api.Command.show_items.output_for_cli(textui, result, 'city', reverse=False) 

city = u'Berlin' 

 

See the `ipalib.cli.textui` plugin for a description of its methods. 

 

 

------------------------ 

Logging from your plugin 

------------------------ 

 

After `plugable.Plugin.set_api()` is called, your plugin will have a 

``self.log`` attribute.  Plugins should only log through this attribute. 

For example: 

 

>>> class paint_house(Command): 

... 

...     takes_args = 'color' 

... 

...     def execute(self, color, **options): 

...         """Uses self.log.error()""" 

...         if color not in ('red', 'blue', 'green'): 

...             self.log.error("I don't have %s paint!", color) # Log error 

...             return 

...         return 'I painted the house %s.' % color 

... 

 

Some basic knowledge of the Python ``logging`` module might be helpful. See: 

 

    http://docs.python.org/library/logging.html 

 

The important thing to remember is that your plugin should not configure 

logging itself, but should instead simply use the ``self.log`` logger. 

 

Also see the `plugable.API.bootstrap()` method for details on how the logging 

is configured. 

 

 

--------------------- 

Environment variables 

--------------------- 

 

Plugins access configuration variables and run-time information through 

``self.api.env`` (or for convenience, ``self.env`` is equivalent).  This 

attribute is a refences to the `ipalib.config.Env` instance created in 

`plugable.API.__init__()`. 

 

After `API.bootstrap()` has been called, the `Env` instance will be populated 

with all the environment information used by the built-in plugins. 

This will be called before any plugins are registered, so plugin authors can 

assume these variables will all exist by the time the module containing their 

plugin (or plugins) is imported. 

 

`Env._bootstrap()`, which is called by `API.bootstrap()`, will create several 

run-time variables that connot be overriden in configuration files or through 

command-line options.  Here is an overview of this run-time information: 

 

=============  =============================  ======================= 

Key            Example value                  Description 

=============  =============================  ======================= 

bin            '/usr/bin'                     Dir. containing script 

dot_ipa        '/home/jderose/.ipa'           User config directory 

home           os.environ['HOME']             User home dir. 

ipalib         '.../site-packages/ipalib'     Dir. of ipalib package 

mode           'unit_test'                    The mode ipalib is in 

script         sys.argv[0]                    Path of script 

site_packages  '.../python2.5/site-packages'  Dir. containing ipalib/ 

=============  =============================  ======================= 

 

If your plugin requires new environment variables *and* will be included in 

the freeIPA built-in plugins, you should add the defaults for your variables 

in `ipalib.constants.DEFAULT_CONFIG`.  Also, you should consider whether your 

new environment variables should have any auto-magic logic to determine their 

values if they haven't already been set by the time `config.Env._bootstrap()`, 

`config.Env._finalize_core()`, or `config.Env._finalize()` is called. 

 

On the other hand, if your plugin requires new environment variables and will 

be installed in a 3rd-party package, your plugin should set these variables 

in the module it is defined in. 

 

`config.Env` values work on a first-one-wins basis... after a value has been 

set, it can not be overridden with a new value.  As any variables can be set 

using the command-line ``-e`` global option or set in a configuration file, 

your module must check whether a variable has already been set before 

setting its default value.  For example: 

 

>>> if 'message_of_the_day' not in api.env: 

...     api.env.message_of_the_day = 'Hello, world!' 

... 

 

Your plugin can access any environment variables via ``self.env``. 

For example: 

 

>>> class motd(Command): 

...     """Print message of the day.""" 

... 

...     def execute(self, **options): 

...         return dict(result=self.env.message) 

... 

>>> api = create_api() 

>>> api.bootstrap(in_server=True, message='Hello, world!') 

>>> api.register(motd) 

>>> api.finalize() 

>>> api.Command.motd(version=u'2.47') 

{'result': u'Hello, world!'} 

 

Also see the `plugable.API.bootstrap_with_global_options()` method. 

 

 

--------------------------------------------- 

Indispensable ipa script commands and options 

--------------------------------------------- 

 

The ``console`` command will launch a custom interactive Python interpreter 

session.  The global environment will have an ``api`` variable, which is the 

standard `plugable.API` instance found at ``ipalib.api``.  All plugins will 

have been loaded (well, except the backend plugins if ``in_server`` is False) 

and ``api`` will be fully initialized.  To launch the console from within the 

top-level directory in the source tree, just run ``ipa console`` from a 

terminal, like this: 

 

    :: 

 

        $ ./ipa console 

 

By default, ``in_server`` is False.  If you want to start the console in a 

server context (so that all the backend plugins are loaded), you can use the 

``-e`` option to set the ``in_server`` environment variable, like this: 

 

    :: 

 

        $ ./ipa -e in_server=True console 

 

You can specify multiple environment variables by including the ``-e`` option 

multiple times, like this: 

 

    :: 

 

        $ ./ipa -e in_server=True -e mode=dummy console 

 

The space after the ``-e`` is optional.  This is equivalent to the above command: 

 

    :: 

 

        $ ./ipa -ein_server=True -emode=dummy console 

 

The ``env`` command will print out the full environment in key=value pairs, 

like this: 

 

    :: 

 

        $ ./ipa env 

 

If you use the ``--server`` option, it will forward the call to the server 

over XML-RPC and print out what the environment is on the server, like this: 

 

    :: 

 

        $ ./ipa env --server 

 

The ``plugins`` command will show details of all the plugin that are loaded, 

like this: 

 

    :: 

 

        $ ./ipa plugins 

 

 

----------------------------------- 

Learning more about freeIPA plugins 

----------------------------------- 

 

To learn more about writing freeIPA plugins, you should: 

 

    1. Look at some of the built-in plugins, like the frontend plugins in 

       `ipalib.plugins.f_user` and the backend plugins in 

       `ipaserver.plugins.b_ldap`. 

 

    2. Learn about the base classes for frontend plugins in `ipalib.frontend`. 

 

    3. Learn about the core plugin framework in `ipalib.plugable`. 

 

Furthermore, the freeIPA plugin architecture was inspired by the Bazaar plugin 

architecture.  Although the two are different enough that learning how to 

write plugins for Bazaar will not particularly help you write plugins for 

freeIPA, some might be interested in the documentation on writing plugins for 

Bazaar, available here: 

 

    http://bazaar-vcs.org/WritingPlugins 

 

If nothing else, we just want to give credit where credit is deserved! 

However, freeIPA does not use any *code* from Bazaar... it merely borrows a 

little inspiration. 

 

 

-------------------------- 

A note on docstring markup 

-------------------------- 

 

Lastly, a quick note on markup:  All the Python docstrings in freeIPA v2 

(including this tutorial) use the *reStructuredText* markup language.  For 

information on reStructuredText, see: 

 

    http://docutils.sourceforge.net/rst.html 

 

For information on using reStructuredText markup with epydoc, see: 

 

    http://epydoc.sourceforge.net/manual-othermarkup.html 

 

 

-------------------------------------------------- 

Next steps: get involved with freeIPA development! 

-------------------------------------------------- 

 

The freeIPA team is always interested in feedback and contribution from the 

community.  To get involved with freeIPA, see the *Contribute* page on 

freeIPA.org: 

 

    http://freeipa.org/page/Contribute 

 

''' 

 

import os 

import plugable 

from backend import Backend 

from frontend import Command, LocalOrRemote, Updater 

from frontend import Object, Method, Property 

from crud import Create, Retrieve, Update, Delete, Search 

from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam 

from parameters import BytesEnum, StrEnum, AccessTime, File 

from errors import SkipPluginModule 

from text import _, ngettext, GettextFactory, NGettextFactory 

 

version_info = (2, 0, 0, 'alpha', 0) 

if version_info[3] == 'final': 

    __version__ = '%d.%d.%d' % version_info[:3] 

else: 

    __version__ = '%d.%d.%d.%s.%d' % version_info 

 

 

def create_api(mode='dummy'): 

    """ 

    Return standard `plugable.API` instance. 

 

    This standard instance allows plugins that subclass from the following 

    base classes: 

 

        - `frontend.Command` 

 

        - `frontend.Object` 

 

        - `frontend.Method` 

 

        - `frontend.Property` 

 

        - `backend.Backend` 

    """ 

    api = plugable.API(Command, Object, Method, Property, Backend, Updater) 

    if mode is not None: 

        api.env.mode = mode 

    assert mode != 'production' 

    return api 

 

api = create_api(mode=None) 

 

if os.environ.get('IPA_UNIT_TEST_MODE', None) == 'cli_test': 

    from cli import cli_plugins 

    for klass in cli_plugins: 

        api.register(klass) 

    api.bootstrap(context='cli', in_server=False, in_tree=True) 

    api.finalize()