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
|
%global _empty_manifest_terminate_build 0
Name: python-pyfmg
Version: 0.8.5.4
Release: 1
Summary: Represents the base components of the Fortinet FortiManager JSON-RPC interface
License: Apache 2.0
URL: https://github.com/p4r4n0y1ng/pyfmg
Source0: https://mirrors.nju.edu.cn/pypi/web/packages/0d/e3/f0688531e9f42d91b426b2be163dcfd1b4f87df9cac886ef875c13b34fc0/pyfmg-0.8.5.4.tar.gz
BuildArch: noarch
Requires: python3-requests
%description
## Synopsis
Represents the base components of the Fortinet FortiManager JSON-RPC interface. Written and maintained by the Fortinet North America CSE Team. This code is based on the fmg_jsonapi.py code provided in the ftntlib package as provided on the Fortinet Developer Network (FNDN). That code has since been modified by JP Forcioli as well as several others within Fortinet. This has now been streamlined and modified to utilize the standard **\**kwargs** functionality as well as has been modified extensively to be more scalable and provide context management and other aspects.
## Code Example
Standard format for a FortiManager JSON-RPC is utilized.
**Of Importance** is that this package uses context behavior for the FortiManager instance, so the **with** keyword can be utilized. This ensures that the FortiManager instance is logged into upon instantiation and is logged out of once the scope of the **with** statement is completed. For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the user name admin and a password of <blank>, the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '') as fmg_instance:
```
The context manager does not HAVE to be utilized obviously. However, if it is not utilized, the *login* and *logout* functionality is not handled for the caller. It is expected that these methods will be called if the context manager is not utilized. An example would be:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '')
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Continuing, when a FortiManager instance is instantiated, the following attributes are configured (or can be configured by the user). The list provided lists the defaults.
```
- debug (default False),
- use_ssl (default True),
- verify_ssl (default False),
- timeout (default 300)
```
For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the username admin and a password of <blank>, that uses http instead of https, is in debug mode, and warns after the verification of the SSL certificate upon each request and has a timeout of 100 the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100) as fmg_instance:
```
Obviously these same parameters would be used in the standard call if the context manager is not utilized so:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100)
```
With the release of FMG 7.2.2 an API User can be created with an API Key (THANK ALL THINGS GOOD!)
The feature works very much like it does in FOS and is very helpful. However, it clearly calls for a different way
to login. We have modified pyFMG so the interface stays very close to the "standard" way you've done things.
Basically you will do the following if you're using an API Key. Let's assume that you want to put the API Key in a
variable and use that in your login. For a context manager setup you would do the following:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key) as fmg_instance:
```
pyFMG takes this information and creates a "session" key just as if you are logging in the old way. That session
information is randomly generated and then appended to a dash and the last 4 digits of your API Key. This is for
those people who need to track session information (particularly those of you who are threading/multiprocessing etc.)
A for instance of this is that, if you're in debug mode and watching your output you'll see something like
```
"session": "8862b4d9-256b-43a5-bc2a-71d330978a6b-mfmg"
```
Notice the "mfmg" there. Again, this will make it where you can track things if needed. If you're not tracking by
session, this really doesn't matter to you. However, it's there for those that need it.
If you don't use a context manager, then you do the same basic thing as before...except clearly this time you will
use your API Key and not the user, password combo. For instance:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key)
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Notice that login() is still called and still must be used, despite that there really is no initial login. This is
so the session can be created and maintained and ensures a consistent interface. The attributes of debug, use_ssl,
verify_ssl and timeout remain and can be called in the login area itself or set the same as before. Nothing has
changed in this area.
A solution has been provided to ensure workspace mode can be handled. When a FMG instance is created, either using the **with** statement as shown above or in a standard scenario (also shown above), the instance checks the FMG for status. At login a call is made to check for status and if *workspace-mode* is returned as anything other than a **0** then workspace capabilities are provided. Standard calls to *lock*, *commit*, and *unlock* are required and are passed through to the workspace manager object for ease of use. If a caller is using the context manager, the workspace manager will now ensure an errant exception does not leave an ADOM stranded in a locked state. The workspace manager functionality will **NOT** call an automatic *commit*, it will simply ensure the *unlock_adom* function is called on any locked ADOM and then will logout. This happens in *logout*, thus a caller could lock an ADOM (or multiple ADOMs), do his work, call *commit* on any ADOM he wants to commit, and then simply call *logout* and then the workspace manager will take care of the unlocks. A common example (using an explicit call to *unlock_adom*) to add an address object might be:
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.unlock_adom("root")
fmg_instance.logout()
```
The following would perform the same and would also unlock the *root* ADOM on the way out (notice no call to *unlock_adom* is required here):
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.logout()
```
While this module is meant to be utilized with another caller-written abstraction, there is no reason that this module could not be utilized by itself to make detailed, multi-parameter calls. To that end, a capability has been provided that enables keyword/value arguments to be passed into any of the *get*, *add*, *update* ,*delete* ,*set* ,*replace* ,*clone* ,*execute* , or *move* helper methods. Since there are many keywords in the FortiManager body that require a dash (and since the dash character is not allowed as a keyword argument handled by the **\**kwargs** pointer), a facility has been added such that a keyword with a double underscore **__** is automatically translated into a dash **-** when the keyword/value pair is put into the body of the call. An example follows (notice the double underscores in the keyword items, these will be translated to dashes when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
Another addition to this concept has been added which is when FortiManager requires an attribute with a space between two words. Since this is not allowed, a facility has been added such that a keyword with a triple underscore **___** is automatically translated into a blank space when the keyword/value pair is put into the body of the call. An example follows (notice the triple underscores in the keyword items, these will be translated to spaces when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), fake___attribute='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
These facilities are helpful, but a more obvious way to make these kind of calls with a little more clarity is shown below in the **Tests** section where a standard dictionary is utilized effectively. In that case, the double underscore translations are not needed and dashes will work perfectly fine (see below). The same holds true for spaces within an attribute when using the free-form method.
## Exceptions
The module provides the following exceptions for use:
1. FMGBaseException(Exception)
2. FMGValidSessionException(FMGBaseException)
3. FMGValueError(ValueError)
4. FMGResponseNotFormedCorrect(KeyError)
5. FMGConnectionError(ReqConnError)
6. FMGConnectTimeout(ReqConnTimeout)
7. FMGRequestNotFormedCorrect(FMGBaseException)
**FMGBaseException** is the Base exception for the module and can be used to catch all things outside of the ValueError and Keyerror issues.
a caller could then write the following and have the equivalent of a standard *except* call with no exception mentioned. This ensures scalability:
```
try:
Doing Something Here
except FMGBaseException:
Do something with Exception
```
**FMGValidSessionException** has been added and is raised if any call is attempted without a valid connection being made to a FMG. In the past, other than to check the \_\_str()\_\_ value of the object after the login return, the code would continue to try to make calls despite having no valid session. Any call attempted now on an invalid session will have this error thrown.
**FMGValueError** is a standard ValueError and is caught in special cases where a connection attempt is made or a call is made with an invalid value. An example of this would be a connection to a FMG instance with a *timeout* value of <= 0.
**FMGResponseNotFormedCorrect** will be raised when response received back from the FMG instance does not have a *result*, *status*, or *code* attribute. FMG responses without these attributes are ill-formed and will raise this error. The only exception to this is the response from a valid *login()* call. This exception is suppressed for this, and a valid response is crafted for login to ensure a stable, standard, and constant response back from the module.
**FMGConnectionError** and **FMGConnectTimeout** are raised when a *requests.exception.ConnectionError* or *requests.exceptions.ConnectTimeout* exception is caught. This ensures calling code does not need to import/depend on the requests module to handle requests connection exceptions. *FMGConnectionError* will most likely be thrown at *login()* and are likely due to an incorrect hostname, or IP Address of the FMG appliance.
**FMGRequestNotFormedCorrect** will be raised when a request for free form capability is issued and the request format is not correct. Specifically a *data* keyword is required to be passed in and the value must be a dictionary. See the ```free_form()``` method explanation below
Exceptions are allowed to propogate up to the caller and are only caught in certain cases where they will be needed in case verbose mode is asked for and the caller wants a print out of the exception. After the print is accomplished that same exception will be raised and propogated so it can be either caught and handled by the caller or used as a debug tool.
## Special Keywords
This section outlines special keywords that will be used within \*\*kwargs that will mean something significant to pyFMG. These keywords, when used by the caller will be checked and will provided special circumstances to the pyFMG calls as there are quite a few special reqiurements when dealing with the FortiManager.
The *data* keyword - utilizing arrays instead of JSON objects in the params section of the request object.
This case is required when an array of objects is needed vice a JSON object with possibly arrays or other objects inside it. An example of this would be a request that needs to look like the following:
```
{
"id": 1,
"method": "add",
"params": [
{
"data": [
"membername1",
"membername2"
],
"url": "pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member"
}
],
"session": "BLAH"
}
```
Notice that the params attribute is holding a data attribute that is an array of items vice the standard JSON object as normally required. To utilize this functionality, the caller will provide a keyword of *data* in the call with the array of information as its value. The call would look like:
```
fmg_instance.add("pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member", data=["membername1", "membername2"])
```
Any and all keywords past the data keyword will be disregarded.
## Responses
A standard, response mechanism is provided from this module so calling objects know what to expect back. Unless an exception is thrown, this module will return a 2 object tuple consisting of the code of the response back, followed by the information in the *"data"* attribute within the response. If there's no data attribute in the response, the text of the response is provided. Since login does not provide a constant response from a FMG appliance, one is provided by this module to ensure a caller knows what will be returned and in what format. An example response of a login, get call, and then logout process is below:
```
(0, {'status': {'message': 'OK', 'code': 0}, 'url': 'sys/login/user'})
(0, [{u'faz.quota': 0, u'foslic_ram': 0, u'foslic_type': 0, u'sn': u'FGVM020000098115', u'mr': 6, u'conf_status': 1, u'os_type': 0, u'node_flags': 0, u'os_ver': 5, ...(truncated)}])
(0, {u'status': {u'message': u'OK', u'code': 0}, u'url': u'sys/logout'})
```
Notice the the login response (the first response above) is NOT unicode. Other than that it matches exactly with other call responses.
## Special Functions
When an operation is sent to the FMG that in return kicks off a task on the sytem (i.e. device config installation, policy package push, etc...) the return value is as discussed where a tuple with the return code and the return json value is provided. In this case, the JSON value will have a task identifier attribute and can be used to track that task. This module provides a simple track tasking functionality called ```track_task()``` that takes in a *task_id* integer and then optional values for *sleep_time* (default is 5 seconds) between requests, *retrieval_fail_gate* (default is 10) and a *timeout* (default is 120). This provides a looped response for that task that with the defaults allows for the system to take approx a minute to respond - this value is a very long time, so we are certain that if the system does not respond by then something is wrong. The loop requests information from the system about the task every 5 seconds and give the system over 2 minutes to complete prior to giving a response that the task is taking too long. This function allows the capability of getting a task and then watching the values - as well as pivoting off of the rich data the FMG responds with to include number of lines that were completed, any errors or warnings, completion time and more. The system also adds in an attribute to the response data on the completion cycle named **total_task_time** which is the time it took for the task to complete its actions. A way to call and use this function is as follows:
```
code, task_obj = fmg_instance.execute("securityconsole/install/package", flags=["preview"], adom="root", pkg=pp_name)
if 'task' in task_obj:
taskid = task_obj.get('task')
fmg_instance.track_task(taskid)
```
An execution function outside of the standard *get*, *add*, *update*, *delete*, *set*, *replace*, *clone*, *execute*, or *move* has been added. This function is called ```free_form(method, **kwargs)```. The arguments are the string method that must be called such as *add* or *get*, etc... and a key word argument list. The kw argument must be a dictionary that has the key **data** or a *FMGRequestNotFormedCorrect* exception will be raised. This data keyword must have the exact value you want to send to the FMG. This function is used for when either the FMG Request object is slightly different than standard OR you are trying to call the FMG with multiple operations. For instance, you want to add 3 address objects with one call. In order to do something like this, the ```free_form()``` function is used and called as below where we are requesting all data from policy id's 1, 3, 4, 5, and 7 with one call:
```
multi_data = []
for pol_id in [1, 3, 4, 5, 7]:
multi_data.append({
"url": f"/pm/config/adom/root/pkg/default/firewall/policy/{pol_id}",
"fields": ["policyid", "name"],
})
if len(multi_data) > 0:
code, res = fmg_instance.free_form("get", data=multi_data)
```
## Logging
A logging functionality has been provided to enable logging to different handlers as required by the caller using the standard python logging facility. The capability to start logging is simply by calling the *getLog* function. This function returns the internal logging reference held by the FortiGate instance. To add or remove a handler use the associated *addHandler()* or *removeHandler()* functions providing a FileHandler or StreamHandler etc... object. The signature for the *getLog()* function is:
```
def getLog(self, loggername="fortinet", lvl=logging.INFO)
```
Once a logger is created by calling the *getLog* function, the logger will log the debug information to whatever handler was provided to the *addHandler()* function. If more than one handler is added, more than one log will occur. To stop logging simply use the *resetLog()* function and the Logging object will be set to None. An example of how to log all debug output to a file would be:
```
fmg.getLog(loggername="fmg")
fh = logging.FileHandler("/location/to/log/fil.log")
fh.setLevel(logging.INFO)
fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s: %(message)s ", "%m/%d/%Y %I:%M:%S %p"))
fgt.addHandler(fh)
```
An external module can utilize standard logging functionality to provide a subordinate type logging function using the same handlers as provided to the pyFGT module. For instance, to log to the same location as the pyFGT module logs Handler is set, you would simply have to do the following:
```
fmg_logger = logging.getLogger("fmg.drvr")
# somewhere in the module
fmg_logger.log(logging.INFO, "This is a log message)
```
The log output in this case would have the fgt.drvr moniker in the format header due to the use of the *%(name)s* format string shown above.
## Motivation
This package is being established to support Ansible requirements and proper mod_utils utilization, however, it can be utilized for contact with any Fortinet FortiManager appliance or VM asset.
## Installation
Installation of this package will be via the pip interface
## Tests
Utilizing the library is relatively simple.
Assuming you are within the with context and still using **fmg_instance** as before, to get all managed devices in the **root** adom, the following would be used:
```
fmg_instance.get(url to get devices for FortiManager version)
```
To **add** an address group the following would be used:
```
data = {
'allow-routing': 1,
'associated-interface': 'any',
'name': 'test_addr_object',
'subnet': ['10.1.1.0', '255.255.255.255'],
'type': 0,
}
fmg_instance.add(URL to add address group objects for FortiManager version, **data)
```
Notice how the **data** dictionary is created and then sent in as **\**data**. This is because there are dashes in the keys of the dictionary that is required and dashes are not allowed in a keyword argument setup. For instance, let's assume that **allow-routing** and **associated-interface** are not required for this call. In that case, the call could have been:
```
fmg_instance.add(URL to add address object for FortiManager version, name='test_addr_object', subnet=['10.1.1.0', '255.255.255.255'],type=0)
```
Notice that all you have to do is send in the data that needs to be sent to the FortiManager appliance in the **\**kwargs** field - this makes calls extremely simple - send in a URL and the keyword arguments and the rest is taken care of.
%package -n python3-pyfmg
Summary: Represents the base components of the Fortinet FortiManager JSON-RPC interface
Provides: python-pyfmg
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-pip
%description -n python3-pyfmg
## Synopsis
Represents the base components of the Fortinet FortiManager JSON-RPC interface. Written and maintained by the Fortinet North America CSE Team. This code is based on the fmg_jsonapi.py code provided in the ftntlib package as provided on the Fortinet Developer Network (FNDN). That code has since been modified by JP Forcioli as well as several others within Fortinet. This has now been streamlined and modified to utilize the standard **\**kwargs** functionality as well as has been modified extensively to be more scalable and provide context management and other aspects.
## Code Example
Standard format for a FortiManager JSON-RPC is utilized.
**Of Importance** is that this package uses context behavior for the FortiManager instance, so the **with** keyword can be utilized. This ensures that the FortiManager instance is logged into upon instantiation and is logged out of once the scope of the **with** statement is completed. For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the user name admin and a password of <blank>, the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '') as fmg_instance:
```
The context manager does not HAVE to be utilized obviously. However, if it is not utilized, the *login* and *logout* functionality is not handled for the caller. It is expected that these methods will be called if the context manager is not utilized. An example would be:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '')
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Continuing, when a FortiManager instance is instantiated, the following attributes are configured (or can be configured by the user). The list provided lists the defaults.
```
- debug (default False),
- use_ssl (default True),
- verify_ssl (default False),
- timeout (default 300)
```
For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the username admin and a password of <blank>, that uses http instead of https, is in debug mode, and warns after the verification of the SSL certificate upon each request and has a timeout of 100 the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100) as fmg_instance:
```
Obviously these same parameters would be used in the standard call if the context manager is not utilized so:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100)
```
With the release of FMG 7.2.2 an API User can be created with an API Key (THANK ALL THINGS GOOD!)
The feature works very much like it does in FOS and is very helpful. However, it clearly calls for a different way
to login. We have modified pyFMG so the interface stays very close to the "standard" way you've done things.
Basically you will do the following if you're using an API Key. Let's assume that you want to put the API Key in a
variable and use that in your login. For a context manager setup you would do the following:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key) as fmg_instance:
```
pyFMG takes this information and creates a "session" key just as if you are logging in the old way. That session
information is randomly generated and then appended to a dash and the last 4 digits of your API Key. This is for
those people who need to track session information (particularly those of you who are threading/multiprocessing etc.)
A for instance of this is that, if you're in debug mode and watching your output you'll see something like
```
"session": "8862b4d9-256b-43a5-bc2a-71d330978a6b-mfmg"
```
Notice the "mfmg" there. Again, this will make it where you can track things if needed. If you're not tracking by
session, this really doesn't matter to you. However, it's there for those that need it.
If you don't use a context manager, then you do the same basic thing as before...except clearly this time you will
use your API Key and not the user, password combo. For instance:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key)
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Notice that login() is still called and still must be used, despite that there really is no initial login. This is
so the session can be created and maintained and ensures a consistent interface. The attributes of debug, use_ssl,
verify_ssl and timeout remain and can be called in the login area itself or set the same as before. Nothing has
changed in this area.
A solution has been provided to ensure workspace mode can be handled. When a FMG instance is created, either using the **with** statement as shown above or in a standard scenario (also shown above), the instance checks the FMG for status. At login a call is made to check for status and if *workspace-mode* is returned as anything other than a **0** then workspace capabilities are provided. Standard calls to *lock*, *commit*, and *unlock* are required and are passed through to the workspace manager object for ease of use. If a caller is using the context manager, the workspace manager will now ensure an errant exception does not leave an ADOM stranded in a locked state. The workspace manager functionality will **NOT** call an automatic *commit*, it will simply ensure the *unlock_adom* function is called on any locked ADOM and then will logout. This happens in *logout*, thus a caller could lock an ADOM (or multiple ADOMs), do his work, call *commit* on any ADOM he wants to commit, and then simply call *logout* and then the workspace manager will take care of the unlocks. A common example (using an explicit call to *unlock_adom*) to add an address object might be:
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.unlock_adom("root")
fmg_instance.logout()
```
The following would perform the same and would also unlock the *root* ADOM on the way out (notice no call to *unlock_adom* is required here):
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.logout()
```
While this module is meant to be utilized with another caller-written abstraction, there is no reason that this module could not be utilized by itself to make detailed, multi-parameter calls. To that end, a capability has been provided that enables keyword/value arguments to be passed into any of the *get*, *add*, *update* ,*delete* ,*set* ,*replace* ,*clone* ,*execute* , or *move* helper methods. Since there are many keywords in the FortiManager body that require a dash (and since the dash character is not allowed as a keyword argument handled by the **\**kwargs** pointer), a facility has been added such that a keyword with a double underscore **__** is automatically translated into a dash **-** when the keyword/value pair is put into the body of the call. An example follows (notice the double underscores in the keyword items, these will be translated to dashes when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
Another addition to this concept has been added which is when FortiManager requires an attribute with a space between two words. Since this is not allowed, a facility has been added such that a keyword with a triple underscore **___** is automatically translated into a blank space when the keyword/value pair is put into the body of the call. An example follows (notice the triple underscores in the keyword items, these will be translated to spaces when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), fake___attribute='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
These facilities are helpful, but a more obvious way to make these kind of calls with a little more clarity is shown below in the **Tests** section where a standard dictionary is utilized effectively. In that case, the double underscore translations are not needed and dashes will work perfectly fine (see below). The same holds true for spaces within an attribute when using the free-form method.
## Exceptions
The module provides the following exceptions for use:
1. FMGBaseException(Exception)
2. FMGValidSessionException(FMGBaseException)
3. FMGValueError(ValueError)
4. FMGResponseNotFormedCorrect(KeyError)
5. FMGConnectionError(ReqConnError)
6. FMGConnectTimeout(ReqConnTimeout)
7. FMGRequestNotFormedCorrect(FMGBaseException)
**FMGBaseException** is the Base exception for the module and can be used to catch all things outside of the ValueError and Keyerror issues.
a caller could then write the following and have the equivalent of a standard *except* call with no exception mentioned. This ensures scalability:
```
try:
Doing Something Here
except FMGBaseException:
Do something with Exception
```
**FMGValidSessionException** has been added and is raised if any call is attempted without a valid connection being made to a FMG. In the past, other than to check the \_\_str()\_\_ value of the object after the login return, the code would continue to try to make calls despite having no valid session. Any call attempted now on an invalid session will have this error thrown.
**FMGValueError** is a standard ValueError and is caught in special cases where a connection attempt is made or a call is made with an invalid value. An example of this would be a connection to a FMG instance with a *timeout* value of <= 0.
**FMGResponseNotFormedCorrect** will be raised when response received back from the FMG instance does not have a *result*, *status*, or *code* attribute. FMG responses without these attributes are ill-formed and will raise this error. The only exception to this is the response from a valid *login()* call. This exception is suppressed for this, and a valid response is crafted for login to ensure a stable, standard, and constant response back from the module.
**FMGConnectionError** and **FMGConnectTimeout** are raised when a *requests.exception.ConnectionError* or *requests.exceptions.ConnectTimeout* exception is caught. This ensures calling code does not need to import/depend on the requests module to handle requests connection exceptions. *FMGConnectionError* will most likely be thrown at *login()* and are likely due to an incorrect hostname, or IP Address of the FMG appliance.
**FMGRequestNotFormedCorrect** will be raised when a request for free form capability is issued and the request format is not correct. Specifically a *data* keyword is required to be passed in and the value must be a dictionary. See the ```free_form()``` method explanation below
Exceptions are allowed to propogate up to the caller and are only caught in certain cases where they will be needed in case verbose mode is asked for and the caller wants a print out of the exception. After the print is accomplished that same exception will be raised and propogated so it can be either caught and handled by the caller or used as a debug tool.
## Special Keywords
This section outlines special keywords that will be used within \*\*kwargs that will mean something significant to pyFMG. These keywords, when used by the caller will be checked and will provided special circumstances to the pyFMG calls as there are quite a few special reqiurements when dealing with the FortiManager.
The *data* keyword - utilizing arrays instead of JSON objects in the params section of the request object.
This case is required when an array of objects is needed vice a JSON object with possibly arrays or other objects inside it. An example of this would be a request that needs to look like the following:
```
{
"id": 1,
"method": "add",
"params": [
{
"data": [
"membername1",
"membername2"
],
"url": "pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member"
}
],
"session": "BLAH"
}
```
Notice that the params attribute is holding a data attribute that is an array of items vice the standard JSON object as normally required. To utilize this functionality, the caller will provide a keyword of *data* in the call with the array of information as its value. The call would look like:
```
fmg_instance.add("pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member", data=["membername1", "membername2"])
```
Any and all keywords past the data keyword will be disregarded.
## Responses
A standard, response mechanism is provided from this module so calling objects know what to expect back. Unless an exception is thrown, this module will return a 2 object tuple consisting of the code of the response back, followed by the information in the *"data"* attribute within the response. If there's no data attribute in the response, the text of the response is provided. Since login does not provide a constant response from a FMG appliance, one is provided by this module to ensure a caller knows what will be returned and in what format. An example response of a login, get call, and then logout process is below:
```
(0, {'status': {'message': 'OK', 'code': 0}, 'url': 'sys/login/user'})
(0, [{u'faz.quota': 0, u'foslic_ram': 0, u'foslic_type': 0, u'sn': u'FGVM020000098115', u'mr': 6, u'conf_status': 1, u'os_type': 0, u'node_flags': 0, u'os_ver': 5, ...(truncated)}])
(0, {u'status': {u'message': u'OK', u'code': 0}, u'url': u'sys/logout'})
```
Notice the the login response (the first response above) is NOT unicode. Other than that it matches exactly with other call responses.
## Special Functions
When an operation is sent to the FMG that in return kicks off a task on the sytem (i.e. device config installation, policy package push, etc...) the return value is as discussed where a tuple with the return code and the return json value is provided. In this case, the JSON value will have a task identifier attribute and can be used to track that task. This module provides a simple track tasking functionality called ```track_task()``` that takes in a *task_id* integer and then optional values for *sleep_time* (default is 5 seconds) between requests, *retrieval_fail_gate* (default is 10) and a *timeout* (default is 120). This provides a looped response for that task that with the defaults allows for the system to take approx a minute to respond - this value is a very long time, so we are certain that if the system does not respond by then something is wrong. The loop requests information from the system about the task every 5 seconds and give the system over 2 minutes to complete prior to giving a response that the task is taking too long. This function allows the capability of getting a task and then watching the values - as well as pivoting off of the rich data the FMG responds with to include number of lines that were completed, any errors or warnings, completion time and more. The system also adds in an attribute to the response data on the completion cycle named **total_task_time** which is the time it took for the task to complete its actions. A way to call and use this function is as follows:
```
code, task_obj = fmg_instance.execute("securityconsole/install/package", flags=["preview"], adom="root", pkg=pp_name)
if 'task' in task_obj:
taskid = task_obj.get('task')
fmg_instance.track_task(taskid)
```
An execution function outside of the standard *get*, *add*, *update*, *delete*, *set*, *replace*, *clone*, *execute*, or *move* has been added. This function is called ```free_form(method, **kwargs)```. The arguments are the string method that must be called such as *add* or *get*, etc... and a key word argument list. The kw argument must be a dictionary that has the key **data** or a *FMGRequestNotFormedCorrect* exception will be raised. This data keyword must have the exact value you want to send to the FMG. This function is used for when either the FMG Request object is slightly different than standard OR you are trying to call the FMG with multiple operations. For instance, you want to add 3 address objects with one call. In order to do something like this, the ```free_form()``` function is used and called as below where we are requesting all data from policy id's 1, 3, 4, 5, and 7 with one call:
```
multi_data = []
for pol_id in [1, 3, 4, 5, 7]:
multi_data.append({
"url": f"/pm/config/adom/root/pkg/default/firewall/policy/{pol_id}",
"fields": ["policyid", "name"],
})
if len(multi_data) > 0:
code, res = fmg_instance.free_form("get", data=multi_data)
```
## Logging
A logging functionality has been provided to enable logging to different handlers as required by the caller using the standard python logging facility. The capability to start logging is simply by calling the *getLog* function. This function returns the internal logging reference held by the FortiGate instance. To add or remove a handler use the associated *addHandler()* or *removeHandler()* functions providing a FileHandler or StreamHandler etc... object. The signature for the *getLog()* function is:
```
def getLog(self, loggername="fortinet", lvl=logging.INFO)
```
Once a logger is created by calling the *getLog* function, the logger will log the debug information to whatever handler was provided to the *addHandler()* function. If more than one handler is added, more than one log will occur. To stop logging simply use the *resetLog()* function and the Logging object will be set to None. An example of how to log all debug output to a file would be:
```
fmg.getLog(loggername="fmg")
fh = logging.FileHandler("/location/to/log/fil.log")
fh.setLevel(logging.INFO)
fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s: %(message)s ", "%m/%d/%Y %I:%M:%S %p"))
fgt.addHandler(fh)
```
An external module can utilize standard logging functionality to provide a subordinate type logging function using the same handlers as provided to the pyFGT module. For instance, to log to the same location as the pyFGT module logs Handler is set, you would simply have to do the following:
```
fmg_logger = logging.getLogger("fmg.drvr")
# somewhere in the module
fmg_logger.log(logging.INFO, "This is a log message)
```
The log output in this case would have the fgt.drvr moniker in the format header due to the use of the *%(name)s* format string shown above.
## Motivation
This package is being established to support Ansible requirements and proper mod_utils utilization, however, it can be utilized for contact with any Fortinet FortiManager appliance or VM asset.
## Installation
Installation of this package will be via the pip interface
## Tests
Utilizing the library is relatively simple.
Assuming you are within the with context and still using **fmg_instance** as before, to get all managed devices in the **root** adom, the following would be used:
```
fmg_instance.get(url to get devices for FortiManager version)
```
To **add** an address group the following would be used:
```
data = {
'allow-routing': 1,
'associated-interface': 'any',
'name': 'test_addr_object',
'subnet': ['10.1.1.0', '255.255.255.255'],
'type': 0,
}
fmg_instance.add(URL to add address group objects for FortiManager version, **data)
```
Notice how the **data** dictionary is created and then sent in as **\**data**. This is because there are dashes in the keys of the dictionary that is required and dashes are not allowed in a keyword argument setup. For instance, let's assume that **allow-routing** and **associated-interface** are not required for this call. In that case, the call could have been:
```
fmg_instance.add(URL to add address object for FortiManager version, name='test_addr_object', subnet=['10.1.1.0', '255.255.255.255'],type=0)
```
Notice that all you have to do is send in the data that needs to be sent to the FortiManager appliance in the **\**kwargs** field - this makes calls extremely simple - send in a URL and the keyword arguments and the rest is taken care of.
%package help
Summary: Development documents and examples for pyfmg
Provides: python3-pyfmg-doc
%description help
## Synopsis
Represents the base components of the Fortinet FortiManager JSON-RPC interface. Written and maintained by the Fortinet North America CSE Team. This code is based on the fmg_jsonapi.py code provided in the ftntlib package as provided on the Fortinet Developer Network (FNDN). That code has since been modified by JP Forcioli as well as several others within Fortinet. This has now been streamlined and modified to utilize the standard **\**kwargs** functionality as well as has been modified extensively to be more scalable and provide context management and other aspects.
## Code Example
Standard format for a FortiManager JSON-RPC is utilized.
**Of Importance** is that this package uses context behavior for the FortiManager instance, so the **with** keyword can be utilized. This ensures that the FortiManager instance is logged into upon instantiation and is logged out of once the scope of the **with** statement is completed. For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the user name admin and a password of <blank>, the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '') as fmg_instance:
```
The context manager does not HAVE to be utilized obviously. However, if it is not utilized, the *login* and *logout* functionality is not handled for the caller. It is expected that these methods will be called if the context manager is not utilized. An example would be:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '')
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Continuing, when a FortiManager instance is instantiated, the following attributes are configured (or can be configured by the user). The list provided lists the defaults.
```
- debug (default False),
- use_ssl (default True),
- verify_ssl (default False),
- timeout (default 300)
```
For instance, to instantiate a FortiManager instance with the IP address of 10.1.1.1, with the username admin and a password of <blank>, that uses http instead of https, is in debug mode, and warns after the verification of the SSL certificate upon each request and has a timeout of 100 the user would simply type:
```
with FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100) as fmg_instance:
```
Obviously these same parameters would be used in the standard call if the context manager is not utilized so:
```
fmg_instance = FortiManager('10.1.1.1', 'admin', '', debug=True, use_ssl=False, debug=True, disable_request_warnings=False, timeout=100)
```
With the release of FMG 7.2.2 an API User can be created with an API Key (THANK ALL THINGS GOOD!)
The feature works very much like it does in FOS and is very helpful. However, it clearly calls for a different way
to login. We have modified pyFMG so the interface stays very close to the "standard" way you've done things.
Basically you will do the following if you're using an API Key. Let's assume that you want to put the API Key in a
variable and use that in your login. For a context manager setup you would do the following:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key) as fmg_instance:
```
pyFMG takes this information and creates a "session" key just as if you are logging in the old way. That session
information is randomly generated and then appended to a dash and the last 4 digits of your API Key. This is for
those people who need to track session information (particularly those of you who are threading/multiprocessing etc.)
A for instance of this is that, if you're in debug mode and watching your output you'll see something like
```
"session": "8862b4d9-256b-43a5-bc2a-71d330978a6b-mfmg"
```
Notice the "mfmg" there. Again, this will make it where you can track things if needed. If you're not tracking by
session, this really doesn't matter to you. However, it's there for those that need it.
If you don't use a context manager, then you do the same basic thing as before...except clearly this time you will
use your API Key and not the user, password combo. For instance:
```
api_key = "reallylongfakeapikeyireceivedfromfmg"
fmg_instance = FortiManager('10.1.1.1', apikey=api_key)
fmg_instance.login()
*something of importance accomplished here*
fmg_instance.logout()
```
Notice that login() is still called and still must be used, despite that there really is no initial login. This is
so the session can be created and maintained and ensures a consistent interface. The attributes of debug, use_ssl,
verify_ssl and timeout remain and can be called in the login area itself or set the same as before. Nothing has
changed in this area.
A solution has been provided to ensure workspace mode can be handled. When a FMG instance is created, either using the **with** statement as shown above or in a standard scenario (also shown above), the instance checks the FMG for status. At login a call is made to check for status and if *workspace-mode* is returned as anything other than a **0** then workspace capabilities are provided. Standard calls to *lock*, *commit*, and *unlock* are required and are passed through to the workspace manager object for ease of use. If a caller is using the context manager, the workspace manager will now ensure an errant exception does not leave an ADOM stranded in a locked state. The workspace manager functionality will **NOT** call an automatic *commit*, it will simply ensure the *unlock_adom* function is called on any locked ADOM and then will logout. This happens in *logout*, thus a caller could lock an ADOM (or multiple ADOMs), do his work, call *commit* on any ADOM he wants to commit, and then simply call *logout* and then the workspace manager will take care of the unlocks. A common example (using an explicit call to *unlock_adom*) to add an address object might be:
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.unlock_adom("root")
fmg_instance.logout()
```
The following would perform the same and would also unlock the *root* ADOM on the way out (notice no call to *unlock_adom* is required here):
```
fmg_instance.lock_adom("root")
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
fmg_instance.commit_changes("root")
fmg_instance.logout()
```
While this module is meant to be utilized with another caller-written abstraction, there is no reason that this module could not be utilized by itself to make detailed, multi-parameter calls. To that end, a capability has been provided that enables keyword/value arguments to be passed into any of the *get*, *add*, *update* ,*delete* ,*set* ,*replace* ,*clone* ,*execute* , or *move* helper methods. Since there are many keywords in the FortiManager body that require a dash (and since the dash character is not allowed as a keyword argument handled by the **\**kwargs** pointer), a facility has been added such that a keyword with a double underscore **__** is automatically translated into a dash **-** when the keyword/value pair is put into the body of the call. An example follows (notice the double underscores in the keyword items, these will be translated to dashes when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), allow__routing=0, associated__interface='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
Another addition to this concept has been added which is when FortiManager requires an attribute with a space between two words. Since this is not allowed, a facility has been added such that a keyword with a triple underscore **___** is automatically translated into a blank space when the keyword/value pair is put into the body of the call. An example follows (notice the triple underscores in the keyword items, these will be translated to spaces when the call is made):
```
fmg_instance.add('pm/config/adom/{adom}/obj/firewall/address'.format(adom="root"), fake___attribute='any', name='add_obj_name', subnet=["192.168.1.0", "255.255.255.0"], type=0, comment='API address obj addition')
```
These facilities are helpful, but a more obvious way to make these kind of calls with a little more clarity is shown below in the **Tests** section where a standard dictionary is utilized effectively. In that case, the double underscore translations are not needed and dashes will work perfectly fine (see below). The same holds true for spaces within an attribute when using the free-form method.
## Exceptions
The module provides the following exceptions for use:
1. FMGBaseException(Exception)
2. FMGValidSessionException(FMGBaseException)
3. FMGValueError(ValueError)
4. FMGResponseNotFormedCorrect(KeyError)
5. FMGConnectionError(ReqConnError)
6. FMGConnectTimeout(ReqConnTimeout)
7. FMGRequestNotFormedCorrect(FMGBaseException)
**FMGBaseException** is the Base exception for the module and can be used to catch all things outside of the ValueError and Keyerror issues.
a caller could then write the following and have the equivalent of a standard *except* call with no exception mentioned. This ensures scalability:
```
try:
Doing Something Here
except FMGBaseException:
Do something with Exception
```
**FMGValidSessionException** has been added and is raised if any call is attempted without a valid connection being made to a FMG. In the past, other than to check the \_\_str()\_\_ value of the object after the login return, the code would continue to try to make calls despite having no valid session. Any call attempted now on an invalid session will have this error thrown.
**FMGValueError** is a standard ValueError and is caught in special cases where a connection attempt is made or a call is made with an invalid value. An example of this would be a connection to a FMG instance with a *timeout* value of <= 0.
**FMGResponseNotFormedCorrect** will be raised when response received back from the FMG instance does not have a *result*, *status*, or *code* attribute. FMG responses without these attributes are ill-formed and will raise this error. The only exception to this is the response from a valid *login()* call. This exception is suppressed for this, and a valid response is crafted for login to ensure a stable, standard, and constant response back from the module.
**FMGConnectionError** and **FMGConnectTimeout** are raised when a *requests.exception.ConnectionError* or *requests.exceptions.ConnectTimeout* exception is caught. This ensures calling code does not need to import/depend on the requests module to handle requests connection exceptions. *FMGConnectionError* will most likely be thrown at *login()* and are likely due to an incorrect hostname, or IP Address of the FMG appliance.
**FMGRequestNotFormedCorrect** will be raised when a request for free form capability is issued and the request format is not correct. Specifically a *data* keyword is required to be passed in and the value must be a dictionary. See the ```free_form()``` method explanation below
Exceptions are allowed to propogate up to the caller and are only caught in certain cases where they will be needed in case verbose mode is asked for and the caller wants a print out of the exception. After the print is accomplished that same exception will be raised and propogated so it can be either caught and handled by the caller or used as a debug tool.
## Special Keywords
This section outlines special keywords that will be used within \*\*kwargs that will mean something significant to pyFMG. These keywords, when used by the caller will be checked and will provided special circumstances to the pyFMG calls as there are quite a few special reqiurements when dealing with the FortiManager.
The *data* keyword - utilizing arrays instead of JSON objects in the params section of the request object.
This case is required when an array of objects is needed vice a JSON object with possibly arrays or other objects inside it. An example of this would be a request that needs to look like the following:
```
{
"id": 1,
"method": "add",
"params": [
{
"data": [
"membername1",
"membername2"
],
"url": "pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member"
}
],
"session": "BLAH"
}
```
Notice that the params attribute is holding a data attribute that is an array of items vice the standard JSON object as normally required. To utilize this functionality, the caller will provide a keyword of *data* in the call with the array of information as its value. The call would look like:
```
fmg_instance.add("pm/config/adom/root/obj/firewall/addrgrp/test_addr_group/member", data=["membername1", "membername2"])
```
Any and all keywords past the data keyword will be disregarded.
## Responses
A standard, response mechanism is provided from this module so calling objects know what to expect back. Unless an exception is thrown, this module will return a 2 object tuple consisting of the code of the response back, followed by the information in the *"data"* attribute within the response. If there's no data attribute in the response, the text of the response is provided. Since login does not provide a constant response from a FMG appliance, one is provided by this module to ensure a caller knows what will be returned and in what format. An example response of a login, get call, and then logout process is below:
```
(0, {'status': {'message': 'OK', 'code': 0}, 'url': 'sys/login/user'})
(0, [{u'faz.quota': 0, u'foslic_ram': 0, u'foslic_type': 0, u'sn': u'FGVM020000098115', u'mr': 6, u'conf_status': 1, u'os_type': 0, u'node_flags': 0, u'os_ver': 5, ...(truncated)}])
(0, {u'status': {u'message': u'OK', u'code': 0}, u'url': u'sys/logout'})
```
Notice the the login response (the first response above) is NOT unicode. Other than that it matches exactly with other call responses.
## Special Functions
When an operation is sent to the FMG that in return kicks off a task on the sytem (i.e. device config installation, policy package push, etc...) the return value is as discussed where a tuple with the return code and the return json value is provided. In this case, the JSON value will have a task identifier attribute and can be used to track that task. This module provides a simple track tasking functionality called ```track_task()``` that takes in a *task_id* integer and then optional values for *sleep_time* (default is 5 seconds) between requests, *retrieval_fail_gate* (default is 10) and a *timeout* (default is 120). This provides a looped response for that task that with the defaults allows for the system to take approx a minute to respond - this value is a very long time, so we are certain that if the system does not respond by then something is wrong. The loop requests information from the system about the task every 5 seconds and give the system over 2 minutes to complete prior to giving a response that the task is taking too long. This function allows the capability of getting a task and then watching the values - as well as pivoting off of the rich data the FMG responds with to include number of lines that were completed, any errors or warnings, completion time and more. The system also adds in an attribute to the response data on the completion cycle named **total_task_time** which is the time it took for the task to complete its actions. A way to call and use this function is as follows:
```
code, task_obj = fmg_instance.execute("securityconsole/install/package", flags=["preview"], adom="root", pkg=pp_name)
if 'task' in task_obj:
taskid = task_obj.get('task')
fmg_instance.track_task(taskid)
```
An execution function outside of the standard *get*, *add*, *update*, *delete*, *set*, *replace*, *clone*, *execute*, or *move* has been added. This function is called ```free_form(method, **kwargs)```. The arguments are the string method that must be called such as *add* or *get*, etc... and a key word argument list. The kw argument must be a dictionary that has the key **data** or a *FMGRequestNotFormedCorrect* exception will be raised. This data keyword must have the exact value you want to send to the FMG. This function is used for when either the FMG Request object is slightly different than standard OR you are trying to call the FMG with multiple operations. For instance, you want to add 3 address objects with one call. In order to do something like this, the ```free_form()``` function is used and called as below where we are requesting all data from policy id's 1, 3, 4, 5, and 7 with one call:
```
multi_data = []
for pol_id in [1, 3, 4, 5, 7]:
multi_data.append({
"url": f"/pm/config/adom/root/pkg/default/firewall/policy/{pol_id}",
"fields": ["policyid", "name"],
})
if len(multi_data) > 0:
code, res = fmg_instance.free_form("get", data=multi_data)
```
## Logging
A logging functionality has been provided to enable logging to different handlers as required by the caller using the standard python logging facility. The capability to start logging is simply by calling the *getLog* function. This function returns the internal logging reference held by the FortiGate instance. To add or remove a handler use the associated *addHandler()* or *removeHandler()* functions providing a FileHandler or StreamHandler etc... object. The signature for the *getLog()* function is:
```
def getLog(self, loggername="fortinet", lvl=logging.INFO)
```
Once a logger is created by calling the *getLog* function, the logger will log the debug information to whatever handler was provided to the *addHandler()* function. If more than one handler is added, more than one log will occur. To stop logging simply use the *resetLog()* function and the Logging object will be set to None. An example of how to log all debug output to a file would be:
```
fmg.getLog(loggername="fmg")
fh = logging.FileHandler("/location/to/log/fil.log")
fh.setLevel(logging.INFO)
fh.setFormatter(logging.Formatter("%(asctime)s - %(name)s: %(message)s ", "%m/%d/%Y %I:%M:%S %p"))
fgt.addHandler(fh)
```
An external module can utilize standard logging functionality to provide a subordinate type logging function using the same handlers as provided to the pyFGT module. For instance, to log to the same location as the pyFGT module logs Handler is set, you would simply have to do the following:
```
fmg_logger = logging.getLogger("fmg.drvr")
# somewhere in the module
fmg_logger.log(logging.INFO, "This is a log message)
```
The log output in this case would have the fgt.drvr moniker in the format header due to the use of the *%(name)s* format string shown above.
## Motivation
This package is being established to support Ansible requirements and proper mod_utils utilization, however, it can be utilized for contact with any Fortinet FortiManager appliance or VM asset.
## Installation
Installation of this package will be via the pip interface
## Tests
Utilizing the library is relatively simple.
Assuming you are within the with context and still using **fmg_instance** as before, to get all managed devices in the **root** adom, the following would be used:
```
fmg_instance.get(url to get devices for FortiManager version)
```
To **add** an address group the following would be used:
```
data = {
'allow-routing': 1,
'associated-interface': 'any',
'name': 'test_addr_object',
'subnet': ['10.1.1.0', '255.255.255.255'],
'type': 0,
}
fmg_instance.add(URL to add address group objects for FortiManager version, **data)
```
Notice how the **data** dictionary is created and then sent in as **\**data**. This is because there are dashes in the keys of the dictionary that is required and dashes are not allowed in a keyword argument setup. For instance, let's assume that **allow-routing** and **associated-interface** are not required for this call. In that case, the call could have been:
```
fmg_instance.add(URL to add address object for FortiManager version, name='test_addr_object', subnet=['10.1.1.0', '255.255.255.255'],type=0)
```
Notice that all you have to do is send in the data that needs to be sent to the FortiManager appliance in the **\**kwargs** field - this makes calls extremely simple - send in a URL and the keyword arguments and the rest is taken care of.
%prep
%autosetup -n pyfmg-0.8.5.4
%build
%py3_build
%install
%py3_install
install -d -m755 %{buildroot}/%{_pkgdocdir}
if [ -d doc ]; then cp -arf doc %{buildroot}/%{_pkgdocdir}; fi
if [ -d docs ]; then cp -arf docs %{buildroot}/%{_pkgdocdir}; fi
if [ -d example ]; then cp -arf example %{buildroot}/%{_pkgdocdir}; fi
if [ -d examples ]; then cp -arf examples %{buildroot}/%{_pkgdocdir}; fi
pushd %{buildroot}
if [ -d usr/lib ]; then
find usr/lib -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/lib64 ]; then
find usr/lib64 -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/bin ]; then
find usr/bin -type f -printf "/%h/%f\n" >> filelist.lst
fi
if [ -d usr/sbin ]; then
find usr/sbin -type f -printf "/%h/%f\n" >> filelist.lst
fi
touch doclist.lst
if [ -d usr/share/man ]; then
find usr/share/man -type f -printf "/%h/%f.gz\n" >> doclist.lst
fi
popd
mv %{buildroot}/filelist.lst .
mv %{buildroot}/doclist.lst .
%files -n python3-pyfmg -f filelist.lst
%dir %{python3_sitelib}/*
%files help -f doclist.lst
%{_docdir}/*
%changelog
* Fri May 05 2023 Python_Bot <Python_Bot@openeuler.org> - 0.8.5.4-1
- Package Spec generated
|