gns3-server 3.0.1__py3-none-any.whl → 3.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of gns3-server might be problematic. Click here for more details.
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/METADATA +17 -16
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/RECORD +24 -24
- gns3server/api/routes/controller/images.py +58 -21
- gns3server/api/routes/controller/templates.py +23 -2
- gns3server/compute/docker/__init__.py +1 -1
- gns3server/controller/__init__.py +27 -18
- gns3server/controller/appliance_manager.py +2 -2
- gns3server/controller/compute.py +7 -2
- gns3server/crash_report.py +1 -1
- gns3server/db/repositories/images.py +22 -3
- gns3server/db/repositories/templates.py +11 -0
- gns3server/db/tasks.py +120 -79
- gns3server/server.py +3 -3
- gns3server/services/authentication.py +9 -6
- gns3server/static/web-ui/index.html +1 -1
- gns3server/static/web-ui/main.62c99707e4709a56.js +1 -0
- gns3server/utils/asyncio/__init__.py +4 -12
- gns3server/utils/asyncio/pool.py +1 -4
- gns3server/utils/images.py +17 -4
- gns3server/version.py +2 -2
- gns3server/static/web-ui/main.e55eeff5c0ba1cf4.js +0 -1
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/LICENSE +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/WHEEL +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/entry_points.txt +0 -0
- {gns3_server-3.0.1.dist-info → gns3_server-3.0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: gns3-server
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.2
|
|
4
4
|
Summary: GNS3 graphical interface for the GNS3 server.
|
|
5
5
|
Author-email: Jeremy Grossmann <developers@gns3.com>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
@@ -690,35 +690,36 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
|
690
690
|
Classifier: Natural Language :: English
|
|
691
691
|
Classifier: Operating System :: POSIX :: Linux
|
|
692
692
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
693
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
694
693
|
Classifier: Programming Language :: Python :: 3.9
|
|
695
694
|
Classifier: Programming Language :: Python :: 3.10
|
|
696
695
|
Classifier: Programming Language :: Python :: 3.11
|
|
697
696
|
Classifier: Programming Language :: Python :: 3.12
|
|
697
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
698
698
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
699
|
-
Requires-Python: >=3.
|
|
699
|
+
Requires-Python: >=3.9
|
|
700
700
|
Description-Content-Type: text/markdown
|
|
701
701
|
License-File: LICENSE
|
|
702
|
-
Requires-Dist: uvicorn==0.
|
|
703
|
-
Requires-Dist: pydantic==2.
|
|
704
|
-
Requires-Dist: fastapi==0.115.
|
|
705
|
-
Requires-Dist: python-multipart==0.0.
|
|
706
|
-
Requires-Dist: websockets==
|
|
702
|
+
Requires-Dist: uvicorn==0.33.0
|
|
703
|
+
Requires-Dist: pydantic==2.10.4
|
|
704
|
+
Requires-Dist: fastapi==0.115.6
|
|
705
|
+
Requires-Dist: python-multipart==0.0.20
|
|
706
|
+
Requires-Dist: websockets==14.1
|
|
707
707
|
Requires-Dist: aiohttp<3.11,>=3.10.10
|
|
708
|
-
Requires-Dist: async-timeout==
|
|
708
|
+
Requires-Dist: async-timeout==5.0.1; python_version < "3.11"
|
|
709
709
|
Requires-Dist: aiofiles<25.0,>=24.1.0
|
|
710
|
-
Requires-Dist: Jinja2<3.2,>=3.1.
|
|
711
|
-
Requires-Dist: sentry-sdk<2.
|
|
712
|
-
Requires-Dist: psutil>=6.1.
|
|
710
|
+
Requires-Dist: Jinja2<3.2,>=3.1.5
|
|
711
|
+
Requires-Dist: sentry-sdk<2.20,>=2.19.2
|
|
712
|
+
Requires-Dist: psutil>=6.1.1
|
|
713
713
|
Requires-Dist: distro>=1.9.0
|
|
714
714
|
Requires-Dist: py-cpuinfo<10.0,>=9.0.0
|
|
715
|
+
Requires-Dist: greenlet==3.1.1
|
|
715
716
|
Requires-Dist: sqlalchemy==2.0.36
|
|
716
717
|
Requires-Dist: aiosqlite==0.20.0
|
|
717
|
-
Requires-Dist: alembic==1.
|
|
718
|
-
Requires-Dist: bcrypt==4.2.
|
|
719
|
-
Requires-Dist:
|
|
718
|
+
Requires-Dist: alembic==1.14.0
|
|
719
|
+
Requires-Dist: bcrypt==4.2.1
|
|
720
|
+
Requires-Dist: joserfc==1.0.1
|
|
720
721
|
Requires-Dist: email-validator==2.2.0
|
|
721
|
-
Requires-Dist:
|
|
722
|
+
Requires-Dist: watchdog==6.0.0
|
|
722
723
|
Requires-Dist: zstandard==0.23.0
|
|
723
724
|
Requires-Dist: platformdirs<3,>=2.4.0
|
|
724
725
|
Requires-Dist: importlib-resources>=1.3; python_version <= "3.9"
|
|
@@ -2,11 +2,11 @@ gns3server/__init__.py,sha256=7t93V0ASczMyzTR7up2g0FBQ6Rm1eC8wxyysMKUeAKE,716
|
|
|
2
2
|
gns3server/__main__.py,sha256=5CO8WDwpzZ2BRo3Ct95MhMGxyXQ3QsHBcWt3jPlcLb0,736
|
|
3
3
|
gns3server/alembic.ini,sha256=XWHHdJ3QOHO3NJh5LK6bMHT6kv7GzAr26X1hs43DH4o,3086
|
|
4
4
|
gns3server/config.py,sha256=3UmjwcthW-gMKbLEPzygzrEVJYza-H1IWRjJZ_L1-tg,9244
|
|
5
|
-
gns3server/crash_report.py,sha256=
|
|
5
|
+
gns3server/crash_report.py,sha256=WLpcAtoTZ_MFDuPpJo6L6MW0OqGjMTrIilSc7oditOE,6021
|
|
6
6
|
gns3server/logger.py,sha256=zjHjyyqK74YMVPdjjznj03xWO42Kx3RDeVnZflT-0eI,5309
|
|
7
7
|
gns3server/main.py,sha256=w16TCAYfTmdKsrCjm7rt4hlwsaMfLiWxw8598K4B3zA,2286
|
|
8
|
-
gns3server/server.py,sha256=
|
|
9
|
-
gns3server/version.py,sha256=
|
|
8
|
+
gns3server/server.py,sha256=ZoqI8I1UI_gQgescnEiT0XFijJVo86vM3aowGLUOXHw,13362
|
|
9
|
+
gns3server/version.py,sha256=QxUxoSMx508tXlMSHuEUDgxjrmPrqRyrX3VxGhKmlbE,1462
|
|
10
10
|
gns3server/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
gns3server/api/server.py,sha256=LQAKxtBaVxylgVTqRpVhdwACdkaibDryOSUT2us5PUk,7972
|
|
12
12
|
gns3server/api/routes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -40,7 +40,7 @@ gns3server/api/routes/controller/controller.py,sha256=6S8pMz5aj47RSrFeGP8CTuGYs8
|
|
|
40
40
|
gns3server/api/routes/controller/drawings.py,sha256=E76kffF_AqXtdY684snU507uAalLNVv-0l2eXe0b5WM,4076
|
|
41
41
|
gns3server/api/routes/controller/gns3vm.py,sha256=7uwJYx7PSkol6_trM8uN4g_XL7SNds8_r0EMC8UyNHM,2035
|
|
42
42
|
gns3server/api/routes/controller/groups.py,sha256=Haqy3lzYRS1-FcS15yXEeAPz6k7-sD1_RaPxPVZc85A,6962
|
|
43
|
-
gns3server/api/routes/controller/images.py,sha256=
|
|
43
|
+
gns3server/api/routes/controller/images.py,sha256=MJiMxvmG6P3BNd71tQ5EYbpd7xotm2VJRBYujY1hYV8,10334
|
|
44
44
|
gns3server/api/routes/controller/links.py,sha256=LgqmdVUUwtpipDhDzRdzUyKpsr1Or-t-E05SXAzGNkA,8464
|
|
45
45
|
gns3server/api/routes/controller/nodes.py,sha256=WeWmKVDU7aG9GeW4ZbvD79P12Ov0jTIZ5lw4fJjWq8s,18855
|
|
46
46
|
gns3server/api/routes/controller/pools.py,sha256=FCSqEmRxRvjMKM_iGwERxUrukJEGczV_XRpa8mSZmFw,7806
|
|
@@ -49,7 +49,7 @@ gns3server/api/routes/controller/projects.py,sha256=1I5QL3rB35POkfBoObPH0xbUz-Nk
|
|
|
49
49
|
gns3server/api/routes/controller/roles.py,sha256=IQ-9BcUDOWqFpxSU7CEZWuEZ6H0VaWv4AfHxT0NZ8ME,6089
|
|
50
50
|
gns3server/api/routes/controller/snapshots.py,sha256=GMBL2nQJp8VJRpo7LpzHSBhPn1SJbvjs0HugDFqCOYE,3559
|
|
51
51
|
gns3server/api/routes/controller/symbols.py,sha256=D_fD-j421LJjiO0Rtgo82psxm4WLgN1psVqTdc4psSw,3825
|
|
52
|
-
gns3server/api/routes/controller/templates.py,sha256=
|
|
52
|
+
gns3server/api/routes/controller/templates.py,sha256=ZeD3eT2x__FvCqRLc85AukOkpHnBCwg1msYL826uqBI,7296
|
|
53
53
|
gns3server/api/routes/controller/users.py,sha256=cwf11_V1gZ-DThKILGlPZq-g6C4PuAJPU8HyEu5FdVU,8056
|
|
54
54
|
gns3server/api/routes/controller/dependencies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
55
|
gns3server/api/routes/controller/dependencies/authentication.py,sha256=-XeUWr21hYkQovqUhcctinPuLqk-q6803evBSTMHtNM,3979
|
|
@@ -284,7 +284,7 @@ gns3server/compute/builtin/nodes/cloud.py,sha256=r3tq0b-7uvM9xHRVtNx65Dv3D69_TDO
|
|
|
284
284
|
gns3server/compute/builtin/nodes/ethernet_hub.py,sha256=_aPL_bKpvYY_REHRcUlkSLMobN4ZhhGGCJLgQNHUvtc,2708
|
|
285
285
|
gns3server/compute/builtin/nodes/ethernet_switch.py,sha256=Xc59N3kM9nuzLjHlgllRI3pzXtDQdc2SsCucdZxMebs,2733
|
|
286
286
|
gns3server/compute/builtin/nodes/nat.py,sha256=rs1M_VZ42zhtb0RmukD7zHEEpR80WAACZykY9ETN0MQ,3804
|
|
287
|
-
gns3server/compute/docker/__init__.py,sha256=
|
|
287
|
+
gns3server/compute/docker/__init__.py,sha256=DqR1b_qZQnS9Ej4kN6_w3fmYzNdf3N1Hw_UudcmIwOs,12476
|
|
288
288
|
gns3server/compute/docker/docker_error.py,sha256=Rj5QDgOFt8ENZy23nDI8J-gmtt8-lDDtlNdmK70d_tg,904
|
|
289
289
|
gns3server/compute/docker/docker_vm.py,sha256=4IQdr-5rL5ylnhPNPaCzU65EZLRSH1yKTWzwuu3cuco,49601
|
|
290
290
|
gns3server/compute/docker/resources/init.sh,sha256=7BxZQm5TXOmDk-TVc0zJmXOX5Jx3azOOnpbPJohF_x0,2868
|
|
@@ -388,11 +388,11 @@ gns3server/configs/ios_etherswitch_startup-config.txt,sha256=zf1QafnlBe729AsDcqL
|
|
|
388
388
|
gns3server/configs/iou_l2_base_startup-config.txt,sha256=LYtQ1pOsmLzEg6DjObmB7PvOhMP93DZkQDFvXh1b2IU,1726
|
|
389
389
|
gns3server/configs/iou_l3_base_startup-config.txt,sha256=zF3Ovq-jKZY0UpiAe8VqhlTzcA7ms1JHWY67vj5gmJQ,1495
|
|
390
390
|
gns3server/configs/vpcs_base_config.txt,sha256=HsThZLGuqANwIjYrCUklQlvHQq6owMdzlXDDE2aWrBQ,199
|
|
391
|
-
gns3server/controller/__init__.py,sha256=
|
|
391
|
+
gns3server/controller/__init__.py,sha256=VRKkV0SqZOs99A4LbZV7Is_705_K62PVj7vcg0rsEes,25423
|
|
392
392
|
gns3server/controller/appliance.py,sha256=xxHdtgBHFN85hxeg3kS6SLyFYDJp3llTSr0LXupByr0,2128
|
|
393
|
-
gns3server/controller/appliance_manager.py,sha256=
|
|
393
|
+
gns3server/controller/appliance_manager.py,sha256=cOCHFdxFhpjnoW1de2LdcE0K_ptVw6DKPxedbXALt3A,20996
|
|
394
394
|
gns3server/controller/appliance_to_template.py,sha256=nM4PIbPxXqYUq7B1oyLhyGz3ZC2QAdk7qClMVTdlJQo,5224
|
|
395
|
-
gns3server/controller/compute.py,sha256=
|
|
395
|
+
gns3server/controller/compute.py,sha256=OamRdq5cZDXSx5QpDIHeq7RWvTWtXtSvcJUua2S9pzE,25581
|
|
396
396
|
gns3server/controller/controller_error.py,sha256=pYW8eY3VZeBl6nSUOUQh-47pbeKfRtSwbkLiWpleHoo,2071
|
|
397
397
|
gns3server/controller/drawing.py,sha256=yMN46Ev0dV-wBA4P4g0xzq-XU1qHVcKGmwriO_0GvsU,7179
|
|
398
398
|
gns3server/controller/export_project.py,sha256=uN5NeMCy1ajMhrEWOli6lHsJNQCI6eRatMss4_kX8vE,12917
|
|
@@ -426,7 +426,7 @@ gns3server/controller/ports/serial_port.py,sha256=FdyfxZN1VGn6_DqEE4p8OBkO-IUaFM
|
|
|
426
426
|
gns3server/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
427
427
|
gns3server/core/tasks.py,sha256=Zo5pCs4LKBUb1I9mgWgQ_h0Bj-_n33-zeCNFZ5xQQl0,3329
|
|
428
428
|
gns3server/db/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
429
|
-
gns3server/db/tasks.py,sha256=
|
|
429
|
+
gns3server/db/tasks.py,sha256=aK2USujpgxIP4nlEH8MgGp3CPpZc6B_3TudYJwD-t7Q,10145
|
|
430
430
|
gns3server/db/models/__init__.py,sha256=X3jpiZd7WqERqYmrtalcAvWatfnSL5NaKrgvZPHRs8k,1192
|
|
431
431
|
gns3server/db/models/acl.py,sha256=jpcCWTEMAEHDQG5mdHhAYkX80oiPc5pQ9LQ9UoJluFI,1776
|
|
432
432
|
gns3server/db/models/base.py,sha256=E73cXMoRr85tOaDEPcMCtfBMxdd1H_vIPU0wKiCGC2w,3419
|
|
@@ -440,10 +440,10 @@ gns3server/db/models/users.py,sha256=78037IGFag5Fp7NlIWywycYF5otMWGmsMSROrPvmZK0
|
|
|
440
440
|
gns3server/db/repositories/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
441
441
|
gns3server/db/repositories/base.py,sha256=a6HZJ4mu9GVpdUCZseA65xy8nzrkN_M00WLFKYVmCzM,874
|
|
442
442
|
gns3server/db/repositories/computes.py,sha256=Km00JeclL6xDZ_863keGTiU38lZ-ufQhKmiU8hvWEj8,3445
|
|
443
|
-
gns3server/db/repositories/images.py,sha256=
|
|
443
|
+
gns3server/db/repositories/images.py,sha256=UNNxXTxo04Ih-hwbJMmSwKom5XC88qcwNL3meamq9wI,5547
|
|
444
444
|
gns3server/db/repositories/pools.py,sha256=2Iqwg1DPuEr1e47trjBtSHLPwuRP7PtxVpMUA6VyY3M,7569
|
|
445
445
|
gns3server/db/repositories/rbac.py,sha256=vkWFp2OiehaETpaff_-_BHFQJZxX1wiukqmdj0rAbQk,15052
|
|
446
|
-
gns3server/db/repositories/templates.py,sha256=
|
|
446
|
+
gns3server/db/repositories/templates.py,sha256=b6v7mxPWVhPTrF6J-fzwi86pdMDJIlotAz_gHSGRajI,6875
|
|
447
447
|
gns3server/db/repositories/users.py,sha256=e1NCZFR8f00MvwovBwXbn0oEo8lAo1okEC01DSuJY2E,9914
|
|
448
448
|
gns3server/db_migrations/README,sha256=JNt8RkbFUWF8Ba9uw0bCvMvNW7PONXJvDhREDLOy61o,171
|
|
449
449
|
gns3server/db_migrations/env.py,sha256=mieif6012ww6wn9JPReZy1WvZHVsTyojbesYpqSE8qw,2782
|
|
@@ -524,7 +524,7 @@ gns3server/schemas/controller/templates/virtualbox_templates.py,sha256=8DZEUBe2l
|
|
|
524
524
|
gns3server/schemas/controller/templates/vmware_templates.py,sha256=_W7ObQArm1aMd6ANGOzhV8A8C4IEuIGvOAvlVu5x3ag,2843
|
|
525
525
|
gns3server/schemas/controller/templates/vpcs_templates.py,sha256=cAPdGve1OJCPaSdYLZfxFwidkTL4pzaJxfdtVKI8bFE,1400
|
|
526
526
|
gns3server/services/__init__.py,sha256=Qdb0EuWkPSLx9HeWcV8lJ1qPez288e7vgpCNLNGbvxw,753
|
|
527
|
-
gns3server/services/authentication.py,sha256
|
|
527
|
+
gns3server/services/authentication.py,sha256=-8nABBBN0co-xDCBjeu9ZMVrlQNq-7QmX4JjAuKHZ_A,3630
|
|
528
528
|
gns3server/services/computes.py,sha256=ZcHb7HXxdVjtV0N0ggxOkvDL7mFpemisXLwXMrNNmpQ,3411
|
|
529
529
|
gns3server/services/templates.py,sha256=w_8grcTPVMXFWGqF40aBQ2OeXK5WDd-yQ4OD6q9jKIg,14211
|
|
530
530
|
gns3server/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -560,8 +560,8 @@ gns3server/static/web-ui/NotoSans-Regular.bb384defbe36eaec.svg,sha256=uECuQtpAPz
|
|
|
560
560
|
gns3server/static/web-ui/NotoSans-Regular.e6139cb1663a1c57.ttf,sha256=NPyxxROGVf4PlLBl3aiZC8bvHIpeR0bbt37H7hWtNGI,512588
|
|
561
561
|
gns3server/static/web-ui/ReleaseNotes.txt,sha256=uag7ABM-yXleLBxhtetbpVOCssZPXcd_lLMyUAdQsn4,5527
|
|
562
562
|
gns3server/static/web-ui/favicon.ico,sha256=ucy7cQDhOulawYo6ntAIV_MhtjtJjx-3q6tQb8HEDpk,5430
|
|
563
|
-
gns3server/static/web-ui/index.html,sha256=
|
|
564
|
-
gns3server/static/web-ui/main.
|
|
563
|
+
gns3server/static/web-ui/index.html,sha256=mear17OnyP8egGZXvsUmuuClEa01X_YIFZZbb5OBPDc,11546
|
|
564
|
+
gns3server/static/web-ui/main.62c99707e4709a56.js,sha256=m0OxkO_z5Zosa-n7qxnT3OvXjtgMYCL348nJ4iZrpws,4319606
|
|
565
565
|
gns3server/static/web-ui/polyfills.319c79dd175e50d0.js,sha256=sQPX0SyLyUHk_0gvODKsdL3-lQsQ_whJeMgxx6tO04A,38362
|
|
566
566
|
gns3server/static/web-ui/roboto-latin-100.539f0a96b40596f7.woff2,sha256=EoI9WFYFI4EhVUr_i7BgojXcNvN-_Z-x5-bqGpYivDU,15808
|
|
567
567
|
gns3server/static/web-ui/roboto-latin-100.5ba994dac3e79ea8.woff,sha256=xOrU3p96_yN9BrUw6thBPRNXQn9qkllENCu04rHc5tA,20368
|
|
@@ -1225,27 +1225,27 @@ gns3server/utils/file_watcher.py,sha256=CF-5dB-RVSj8xuVxJ9siSpf3QOnXpw3n2IkjCzRg
|
|
|
1225
1225
|
gns3server/utils/get_resource.py,sha256=WWW4mejsye6Tpw5UmIT6drfcUDqs-YhLxSlQaNboRDg,1534
|
|
1226
1226
|
gns3server/utils/hostname.py,sha256=XUswUi47n-LfUbMiZ5AOs_Crl6lln606nhttEaJanpA,3342
|
|
1227
1227
|
gns3server/utils/http_client.py,sha256=MD-yZA2VjQonqOlPiU4SXo9AjQWDYFFP0ZeZzz680EI,2318
|
|
1228
|
-
gns3server/utils/images.py,sha256=
|
|
1228
|
+
gns3server/utils/images.py,sha256=v7kPc3iDN6E84naQcS9OUHpZLQ2xfY5syeqY0p_bZ4Y,14840
|
|
1229
1229
|
gns3server/utils/interfaces.py,sha256=DwsZXwPxmc5E-QwquzMqAfQRLrrt-GyYUjUDxFKqEZo,8981
|
|
1230
1230
|
gns3server/utils/notification_queue.py,sha256=EO2vHr_Lkxiq9NP-6u6eDsVvFNKbe-QSZqHADU-Ynss,2688
|
|
1231
1231
|
gns3server/utils/path.py,sha256=m4kj9NoFMLQfMAEG1HJ97x-zAdw0bxq-14tCwaIOSKc,2348
|
|
1232
1232
|
gns3server/utils/picture.py,sha256=xC6KASep9RHUNzcb6scL5POPNZ04uROXJmTub3INpto,6160
|
|
1233
1233
|
gns3server/utils/qt.py,sha256=rNABILwh9iqK6XCqBOjjk60ELPPaI7DFQWfKrORk2Kk,1461
|
|
1234
1234
|
gns3server/utils/vmnet.py,sha256=_7foMqHldwH2kXMtgkpEqOKMJAU7reGI42v1Svnj3gw,9981
|
|
1235
|
-
gns3server/utils/asyncio/__init__.py,sha256=
|
|
1235
|
+
gns3server/utils/asyncio/__init__.py,sha256=43PKts4ppmCaFgfepYsI3BfhuqV77gnIudzdWk0umaw,4522
|
|
1236
1236
|
gns3server/utils/asyncio/aiozipstream.py,sha256=cZF-ASUuqLtXN1vJaDW2PfvSE9O4BjV55yGapelo1Ko,18984
|
|
1237
1237
|
gns3server/utils/asyncio/embed_shell.py,sha256=J9a3ibTi9AlRJ4YvFAhPmjz68lla0pNgzN31JMMkJXY,11490
|
|
1238
1238
|
gns3server/utils/asyncio/input_stream.py,sha256=hUnspj9z0X-mvTLi5EMTXpfQLAIIUvEfgjh-7zMug8Y,14560
|
|
1239
|
-
gns3server/utils/asyncio/pool.py,sha256=
|
|
1239
|
+
gns3server/utils/asyncio/pool.py,sha256=EV8ZMPmLe6vIsGjN9QrcxP1XZxjucxlIrWXtJkdd4Iw,2014
|
|
1240
1240
|
gns3server/utils/asyncio/raw_command_server.py,sha256=Thr023djzxIriw5Au3Y5BMAeQ6R7KNttOy-1Gs_gPvI,4671
|
|
1241
1241
|
gns3server/utils/asyncio/serial.py,sha256=VNhqaRkN9Mk3Z30CD6wMNfdTNqlY5YUbMyrAwuBsvXM,2333
|
|
1242
1242
|
gns3server/utils/asyncio/telnet_server.py,sha256=dteUIryUX25iRr5-dMkdBYSTNZQZfLKnw00Y4C87ewc,16752
|
|
1243
1243
|
gns3server/utils/zipfile_zstd/__init__.py,sha256=QXfnwyxx_CDJdAgOzqqBAD_7WDudc9VN03U0Gfccr5I,163
|
|
1244
1244
|
gns3server/utils/zipfile_zstd/_patcher.py,sha256=RCMj1y8OrIJheEGnuERDH0l2hvLaFl5wa_IZNiTpBbU,380
|
|
1245
1245
|
gns3server/utils/zipfile_zstd/_zipfile.py,sha256=KvAeNmWZHu2LHuUOZ8_Tnu_dRi3NOIWM6I3hg_c7NsQ,2189
|
|
1246
|
-
gns3_server-3.0.
|
|
1247
|
-
gns3_server-3.0.
|
|
1248
|
-
gns3_server-3.0.
|
|
1249
|
-
gns3_server-3.0.
|
|
1250
|
-
gns3_server-3.0.
|
|
1251
|
-
gns3_server-3.0.
|
|
1246
|
+
gns3_server-3.0.2.dist-info/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
1247
|
+
gns3_server-3.0.2.dist-info/METADATA,sha256=F5fxjfxloNsp52Ga_hxOtfUOYyQkBEV48WNwXadIMPk,48323
|
|
1248
|
+
gns3_server-3.0.2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
1249
|
+
gns3_server-3.0.2.dist-info/entry_points.txt,sha256=kgHc6rA0RfDOTAkTDSwgWXKHWG4r5mQ-SsmpCHNgXPI,92
|
|
1250
|
+
gns3_server-3.0.2.dist-info/top_level.txt,sha256=GvKJnXIKLBmewtHjQXkvmtz79ZW51IsfCoPMbHoWehQ,11
|
|
1251
|
+
gns3_server-3.0.2.dist-info/RECORD,,
|
|
@@ -27,11 +27,11 @@ from fastapi.encoders import jsonable_encoder
|
|
|
27
27
|
from starlette.requests import ClientDisconnect
|
|
28
28
|
from sqlalchemy.orm.exc import MultipleResultsFound
|
|
29
29
|
from typing import List, Optional
|
|
30
|
-
from gns3server import schemas
|
|
31
30
|
|
|
31
|
+
from gns3server import schemas
|
|
32
32
|
from gns3server.config import Config
|
|
33
33
|
from gns3server.compute.qemu import Qemu
|
|
34
|
-
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory
|
|
34
|
+
from gns3server.utils.images import InvalidImageError, write_image, read_image_info, default_images_directory, get_builtin_disks
|
|
35
35
|
from gns3server.db.repositories.images import ImagesRepository
|
|
36
36
|
from gns3server.db.repositories.templates import TemplatesRepository
|
|
37
37
|
from gns3server.db.repositories.rbac import RbacRepository
|
|
@@ -51,7 +51,6 @@ log = logging.getLogger(__name__)
|
|
|
51
51
|
|
|
52
52
|
router = APIRouter()
|
|
53
53
|
|
|
54
|
-
|
|
55
54
|
@router.post(
|
|
56
55
|
"/qemu/{image_path:path}",
|
|
57
56
|
response_model=schemas.Image,
|
|
@@ -175,6 +174,61 @@ async def upload_image(
|
|
|
175
174
|
return image
|
|
176
175
|
|
|
177
176
|
|
|
177
|
+
@router.delete(
|
|
178
|
+
"/prune",
|
|
179
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
180
|
+
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
|
181
|
+
)
|
|
182
|
+
async def prune_images(
|
|
183
|
+
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
|
184
|
+
) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Prune images not attached to any template.
|
|
187
|
+
|
|
188
|
+
Required privilege: Image.Allocate
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
skip_images = get_builtin_disks()
|
|
192
|
+
await images_repo.prune_images(skip_images)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@router.post(
|
|
196
|
+
"/install",
|
|
197
|
+
status_code=status.HTTP_204_NO_CONTENT,
|
|
198
|
+
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
|
199
|
+
)
|
|
200
|
+
async def install_images(
|
|
201
|
+
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
|
202
|
+
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
|
|
203
|
+
) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Attempt to automatically create templates based on image checksums.
|
|
206
|
+
|
|
207
|
+
Required privilege: Image.Allocate
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
skip_images = get_builtin_disks()
|
|
211
|
+
images = await images_repo.get_images()
|
|
212
|
+
for image in images:
|
|
213
|
+
if skip_images and image.filename in skip_images:
|
|
214
|
+
log.debug(f"Skipping image '{image.path}' for image installation")
|
|
215
|
+
continue
|
|
216
|
+
templates = await images_repo.get_image_templates(image.image_id)
|
|
217
|
+
if templates:
|
|
218
|
+
# the image is already used by a template
|
|
219
|
+
log.warning(f"Image '{image.path}' is used by one or more templates")
|
|
220
|
+
continue
|
|
221
|
+
await Controller.instance().appliance_manager.install_appliances_from_image(
|
|
222
|
+
image.path,
|
|
223
|
+
image.checksum,
|
|
224
|
+
images_repo,
|
|
225
|
+
templates_repo,
|
|
226
|
+
None,
|
|
227
|
+
None,
|
|
228
|
+
os.path.dirname(image.path)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
178
232
|
@router.get(
|
|
179
233
|
"/{image_path:path}",
|
|
180
234
|
response_model=schemas.Image,
|
|
@@ -218,7 +272,7 @@ async def delete_image(
|
|
|
218
272
|
image = await images_repo.get_image(image_path)
|
|
219
273
|
except MultipleResultsFound:
|
|
220
274
|
raise ControllerBadRequestError(f"Image '{image_path}' matches multiple images. "
|
|
221
|
-
f"Please include the
|
|
275
|
+
f"Please include the absolute path of the image")
|
|
222
276
|
|
|
223
277
|
if not image:
|
|
224
278
|
raise ControllerNotFoundError(f"Image '{image_path}' not found")
|
|
@@ -236,20 +290,3 @@ async def delete_image(
|
|
|
236
290
|
success = await images_repo.delete_image(image_path)
|
|
237
291
|
if not success:
|
|
238
292
|
raise ControllerError(f"Image '{image_path}' could not be deleted")
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@router.post(
|
|
242
|
-
"/prune",
|
|
243
|
-
status_code=status.HTTP_204_NO_CONTENT,
|
|
244
|
-
dependencies=[Depends(has_privilege("Image.Allocate"))]
|
|
245
|
-
)
|
|
246
|
-
async def prune_images(
|
|
247
|
-
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
|
|
248
|
-
) -> None:
|
|
249
|
-
"""
|
|
250
|
-
Prune images not attached to any template.
|
|
251
|
-
|
|
252
|
-
Required privilege: Image.Allocate
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
await images_repo.prune_images()
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
API routes for templates.
|
|
19
19
|
"""
|
|
20
20
|
|
|
21
|
+
import os
|
|
21
22
|
import hashlib
|
|
22
23
|
import json
|
|
23
24
|
|
|
@@ -34,6 +35,8 @@ from gns3server.db.repositories.templates import TemplatesRepository
|
|
|
34
35
|
from gns3server.services.templates import TemplatesService
|
|
35
36
|
from gns3server.db.repositories.rbac import RbacRepository
|
|
36
37
|
from gns3server.db.repositories.images import ImagesRepository
|
|
38
|
+
from gns3server.controller.controller_error import ControllerError
|
|
39
|
+
from gns3server.utils.images import get_builtin_disks
|
|
37
40
|
|
|
38
41
|
from .dependencies.authentication import get_current_active_user
|
|
39
42
|
from .dependencies.rbac import has_privilege
|
|
@@ -132,10 +135,28 @@ async def delete_template(
|
|
|
132
135
|
Required privilege: Template.Allocate
|
|
133
136
|
"""
|
|
134
137
|
|
|
138
|
+
images = await templates_repo.get_template_images(template_id)
|
|
135
139
|
await TemplatesService(templates_repo).delete_template(template_id)
|
|
136
140
|
await rbac_repo.delete_all_ace_starting_with_path(f"/templates/{template_id}")
|
|
137
|
-
if prune_images:
|
|
138
|
-
|
|
141
|
+
if prune_images and images:
|
|
142
|
+
skip_images = get_builtin_disks()
|
|
143
|
+
for image in images:
|
|
144
|
+
if image.filename in skip_images:
|
|
145
|
+
continue
|
|
146
|
+
templates = await images_repo.get_image_templates(image.image_id)
|
|
147
|
+
if templates:
|
|
148
|
+
template_names = ", ".join([template.name for template in templates])
|
|
149
|
+
raise ControllerError(f"Image '{image.path}' is used by one or more templates: {template_names}")
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
os.remove(image.path)
|
|
153
|
+
except OSError:
|
|
154
|
+
log.warning(f"Could not delete image file {image.path}")
|
|
155
|
+
|
|
156
|
+
print(f"Deleting image '{image.path}'")
|
|
157
|
+
success = await images_repo.delete_image(image.path)
|
|
158
|
+
if not success:
|
|
159
|
+
raise ControllerError(f"Image '{image.path}' could not removed from the database")
|
|
139
160
|
|
|
140
161
|
|
|
141
162
|
@router.get(
|
|
@@ -115,7 +115,7 @@ class Docker(BaseManager):
|
|
|
115
115
|
dst_path = self.resources_path()
|
|
116
116
|
log.info(f"Installing Docker resources in '{dst_path}'")
|
|
117
117
|
from gns3server.controller import Controller
|
|
118
|
-
Controller.instance().install_resource_files(dst_path, "compute/docker/resources")
|
|
118
|
+
await Controller.instance().install_resource_files(dst_path, "compute/docker/resources")
|
|
119
119
|
await self.install_busybox(dst_path)
|
|
120
120
|
except OSError as e:
|
|
121
121
|
raise DockerError(f"Could not install Docker resources to {dst_path}: {e}")
|
|
@@ -28,10 +28,10 @@ try:
|
|
|
28
28
|
except ImportError:
|
|
29
29
|
from importlib import resources as importlib_resources
|
|
30
30
|
|
|
31
|
-
|
|
32
31
|
from ..config import Config
|
|
33
32
|
from ..utils import parse_version, md5sum
|
|
34
33
|
from ..utils.images import default_images_directory
|
|
34
|
+
from ..utils.asyncio import wait_run_in_executor
|
|
35
35
|
|
|
36
36
|
from .project import Project
|
|
37
37
|
from .appliance import Appliance
|
|
@@ -43,6 +43,7 @@ from .topology import load_topology
|
|
|
43
43
|
from .gns3vm import GNS3VM
|
|
44
44
|
from .gns3vm.gns3_vm_error import GNS3VMError
|
|
45
45
|
from .controller_error import ControllerError, ControllerNotFoundError
|
|
46
|
+
from ..db.tasks import update_disk_checksums
|
|
46
47
|
from ..version import __version__
|
|
47
48
|
|
|
48
49
|
import logging
|
|
@@ -72,8 +73,11 @@ class Controller:
|
|
|
72
73
|
async def start(self, computes=None):
|
|
73
74
|
|
|
74
75
|
log.info("Controller is starting")
|
|
75
|
-
self._install_base_configs()
|
|
76
|
-
self._install_builtin_disks()
|
|
76
|
+
await self._install_base_configs()
|
|
77
|
+
installed_disks = await self._install_builtin_disks()
|
|
78
|
+
if installed_disks:
|
|
79
|
+
await update_disk_checksums(installed_disks)
|
|
80
|
+
|
|
77
81
|
server_config = Config.instance().settings.Server
|
|
78
82
|
Config.instance().listen_for_config_changes(self._update_config)
|
|
79
83
|
name = server_config.name
|
|
@@ -86,7 +90,7 @@ class Controller:
|
|
|
86
90
|
if host == "0.0.0.0":
|
|
87
91
|
host = "127.0.0.1"
|
|
88
92
|
|
|
89
|
-
self._load_controller_vars()
|
|
93
|
+
await self._load_controller_vars()
|
|
90
94
|
|
|
91
95
|
if server_config.enable_ssl:
|
|
92
96
|
self._ssl_context = self._create_ssl_context(server_config)
|
|
@@ -190,7 +194,7 @@ class Controller:
|
|
|
190
194
|
async def reload(self):
|
|
191
195
|
|
|
192
196
|
log.info("Controller is reloading")
|
|
193
|
-
self._load_controller_vars()
|
|
197
|
+
await self._load_controller_vars()
|
|
194
198
|
|
|
195
199
|
# remove all projects deleted from disk.
|
|
196
200
|
for project in self._projects.copy().values():
|
|
@@ -234,7 +238,7 @@ class Controller:
|
|
|
234
238
|
except OSError as e:
|
|
235
239
|
log.error(f"Cannot write controller vars file '{self._vars_file}': {e}")
|
|
236
240
|
|
|
237
|
-
def _load_controller_vars(self):
|
|
241
|
+
async def _load_controller_vars(self):
|
|
238
242
|
"""
|
|
239
243
|
Reload the controller vars from disk
|
|
240
244
|
"""
|
|
@@ -274,9 +278,9 @@ class Controller:
|
|
|
274
278
|
builtin_appliances_path = self._appliance_manager.builtin_appliances_path()
|
|
275
279
|
if not previous_version or \
|
|
276
280
|
parse_version(__version__.split("+")[0]) > parse_version(previous_version.split("+")[0]):
|
|
277
|
-
self._appliance_manager.install_builtin_appliances()
|
|
281
|
+
await self._appliance_manager.install_builtin_appliances()
|
|
278
282
|
elif not os.listdir(builtin_appliances_path):
|
|
279
|
-
self._appliance_manager.install_builtin_appliances()
|
|
283
|
+
await self._appliance_manager.install_builtin_appliances()
|
|
280
284
|
else:
|
|
281
285
|
log.info(f"Built-in appliances are installed in '{builtin_appliances_path}'")
|
|
282
286
|
|
|
@@ -307,18 +311,21 @@ class Controller:
|
|
|
307
311
|
|
|
308
312
|
|
|
309
313
|
@staticmethod
|
|
310
|
-
def install_resource_files(dst_path, resource_name, upgrade_resources=True):
|
|
314
|
+
async def install_resource_files(dst_path, resource_name, upgrade_resources=True):
|
|
311
315
|
"""
|
|
312
316
|
Install files from resources to user's file system
|
|
313
317
|
"""
|
|
314
318
|
|
|
315
|
-
|
|
319
|
+
installed_resources = []
|
|
320
|
+
async def should_copy(src, dst, upgrade_resources):
|
|
316
321
|
if not os.path.exists(dst):
|
|
317
322
|
return True
|
|
318
323
|
if upgrade_resources is False:
|
|
319
324
|
return False
|
|
320
325
|
# copy the resource if it is different
|
|
321
|
-
|
|
326
|
+
src_md5 = await wait_run_in_executor(md5sum, src)
|
|
327
|
+
dst_md5 = await wait_run_in_executor(md5sum, dst)
|
|
328
|
+
return src_md5 != dst_md5
|
|
322
329
|
|
|
323
330
|
if hasattr(sys, "frozen") and sys.platform.startswith("win"):
|
|
324
331
|
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), resource_name))
|
|
@@ -328,14 +335,16 @@ class Controller:
|
|
|
328
335
|
else:
|
|
329
336
|
for entry in importlib_resources.files('gns3server').joinpath(resource_name).iterdir():
|
|
330
337
|
full_path = os.path.join(dst_path, entry.name)
|
|
331
|
-
if entry.is_file() and should_copy(str(entry), full_path, upgrade_resources):
|
|
338
|
+
if entry.is_file() and await should_copy(str(entry), full_path, upgrade_resources):
|
|
332
339
|
log.debug(f'Installing {resource_name} resource file "{entry.name}" to "{full_path}"')
|
|
333
|
-
shutil.copy(str(entry), os.path.join(
|
|
340
|
+
shutil.copy(str(entry), os.path.join(full_path))
|
|
341
|
+
installed_resources.append(full_path)
|
|
334
342
|
elif entry.is_dir():
|
|
335
343
|
os.makedirs(full_path, exist_ok=True)
|
|
336
|
-
Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
|
|
344
|
+
await Controller.install_resource_files(full_path, os.path.join(resource_name, entry.name))
|
|
345
|
+
return installed_resources
|
|
337
346
|
|
|
338
|
-
def _install_base_configs(self):
|
|
347
|
+
async def _install_base_configs(self):
|
|
339
348
|
"""
|
|
340
349
|
At startup we copy base configs to the user location to allow
|
|
341
350
|
them to customize it
|
|
@@ -345,11 +354,11 @@ class Controller:
|
|
|
345
354
|
log.info(f"Installing base configs in '{dst_path}'")
|
|
346
355
|
try:
|
|
347
356
|
# do not overwrite base configs because they may have been customized by the user
|
|
348
|
-
Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
|
|
357
|
+
await Controller.install_resource_files(dst_path, "configs", upgrade_resources=False)
|
|
349
358
|
except OSError as e:
|
|
350
359
|
log.error(f"Could not install base config files to {dst_path}: {e}")
|
|
351
360
|
|
|
352
|
-
def _install_builtin_disks(self):
|
|
361
|
+
async def _install_builtin_disks(self):
|
|
353
362
|
"""
|
|
354
363
|
At startup we copy built-in Qemu disks to the user location to allow
|
|
355
364
|
them to use with appliances
|
|
@@ -358,7 +367,7 @@ class Controller:
|
|
|
358
367
|
dst_path = self.disks_path()
|
|
359
368
|
log.info(f"Installing built-in disks in '{dst_path}'")
|
|
360
369
|
try:
|
|
361
|
-
Controller.install_resource_files(dst_path, "disks")
|
|
370
|
+
return await Controller.install_resource_files(dst_path, "disks")
|
|
362
371
|
except OSError as e:
|
|
363
372
|
log.error(f"Could not install disk files to {dst_path}: {e}")
|
|
364
373
|
|
|
@@ -110,7 +110,7 @@ class ApplianceManager:
|
|
|
110
110
|
os.makedirs(appliances_dir, exist_ok=True)
|
|
111
111
|
return appliances_dir
|
|
112
112
|
|
|
113
|
-
def install_builtin_appliances(self):
|
|
113
|
+
async def install_builtin_appliances(self):
|
|
114
114
|
"""
|
|
115
115
|
At startup we copy the built-in appliances files.
|
|
116
116
|
"""
|
|
@@ -119,7 +119,7 @@ class ApplianceManager:
|
|
|
119
119
|
log.info(f"Installing built-in appliances in '{dst_path}'")
|
|
120
120
|
from . import Controller
|
|
121
121
|
try:
|
|
122
|
-
Controller.instance().install_resource_files(dst_path, "appliances")
|
|
122
|
+
await Controller.instance().install_resource_files(dst_path, "appliances")
|
|
123
123
|
except OSError as e:
|
|
124
124
|
log.error(f"Could not install built-in appliance files to {dst_path}: {e}")
|
|
125
125
|
|
gns3server/controller/compute.py
CHANGED
|
@@ -18,14 +18,19 @@
|
|
|
18
18
|
import ipaddress
|
|
19
19
|
import aiohttp
|
|
20
20
|
import asyncio
|
|
21
|
-
import async_timeout
|
|
22
21
|
import socket
|
|
23
22
|
import json
|
|
24
23
|
import sys
|
|
25
24
|
import io
|
|
25
|
+
|
|
26
26
|
from fastapi import HTTPException
|
|
27
27
|
from aiohttp import web
|
|
28
28
|
|
|
29
|
+
if sys.version_info >= (3, 11):
|
|
30
|
+
from asyncio import timeout as asynctimeout
|
|
31
|
+
else:
|
|
32
|
+
from async_timeout import timeout as asynctimeout
|
|
33
|
+
|
|
29
34
|
from ..utils import parse_version
|
|
30
35
|
from ..utils.asyncio import locking
|
|
31
36
|
from ..controller.controller_error import (
|
|
@@ -503,7 +508,7 @@ class Compute:
|
|
|
503
508
|
return self._getUrl(path)
|
|
504
509
|
|
|
505
510
|
async def _run_http_query(self, method, path, data=None, timeout=120, raw=False):
|
|
506
|
-
async with
|
|
511
|
+
async with asynctimeout(delay=timeout):
|
|
507
512
|
url = self._getUrl(path)
|
|
508
513
|
headers = {"content-type": "application/json"}
|
|
509
514
|
chunked = None
|
gns3server/crash_report.py
CHANGED
|
@@ -58,7 +58,7 @@ class CrashReport:
|
|
|
58
58
|
Report crash to a third party service
|
|
59
59
|
"""
|
|
60
60
|
|
|
61
|
-
DSN = "https://
|
|
61
|
+
DSN = "https://9cf53e6b9adfe49b867f1847b7cc4d72@o19455.ingest.us.sentry.io/38482"
|
|
62
62
|
_instance = None
|
|
63
63
|
|
|
64
64
|
def __init__(self):
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
20
|
from typing import Optional, List
|
|
21
|
-
from sqlalchemy import select, delete
|
|
21
|
+
from sqlalchemy import select, delete, update
|
|
22
22
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
23
23
|
|
|
24
24
|
from .base import BaseRepository
|
|
@@ -103,6 +103,22 @@ class ImagesRepository(BaseRepository):
|
|
|
103
103
|
await self._db_session.refresh(db_image)
|
|
104
104
|
return db_image
|
|
105
105
|
|
|
106
|
+
async def update_image(self, image_path: str, checksum: str, checksum_algorithm: str) -> models.Image:
|
|
107
|
+
"""
|
|
108
|
+
Update an image.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
query = update(models.Image).\
|
|
112
|
+
where(models.Image.path == image_path).\
|
|
113
|
+
values(checksum=checksum, checksum_algorithm=checksum_algorithm)
|
|
114
|
+
|
|
115
|
+
await self._db_session.execute(query)
|
|
116
|
+
await self._db_session.commit()
|
|
117
|
+
image_db = await self.get_image_by_checksum(checksum)
|
|
118
|
+
if image_db:
|
|
119
|
+
await self._db_session.refresh(image_db) # force refresh of updated_at value
|
|
120
|
+
return image_db
|
|
121
|
+
|
|
106
122
|
async def delete_image(self, image_path: str) -> bool:
|
|
107
123
|
"""
|
|
108
124
|
Delete an image.
|
|
@@ -119,7 +135,7 @@ class ImagesRepository(BaseRepository):
|
|
|
119
135
|
await self._db_session.commit()
|
|
120
136
|
return result.rowcount > 0
|
|
121
137
|
|
|
122
|
-
async def prune_images(self) -> int:
|
|
138
|
+
async def prune_images(self, skip_images: list[str] = None) -> int:
|
|
123
139
|
"""
|
|
124
140
|
Prune images not attached to any template.
|
|
125
141
|
"""
|
|
@@ -130,12 +146,15 @@ class ImagesRepository(BaseRepository):
|
|
|
130
146
|
images = result.scalars().all()
|
|
131
147
|
images_deleted = 0
|
|
132
148
|
for image in images:
|
|
149
|
+
if skip_images and image.filename in skip_images:
|
|
150
|
+
log.debug(f"Skipping image '{image.path}' for pruning")
|
|
151
|
+
continue
|
|
133
152
|
try:
|
|
134
153
|
log.debug(f"Deleting image '{image.path}'")
|
|
135
154
|
os.remove(image.path)
|
|
136
155
|
except OSError:
|
|
137
156
|
log.warning(f"Could not delete image file {image.path}")
|
|
138
|
-
if await self.delete_image(image.
|
|
157
|
+
if await self.delete_image(image.path):
|
|
139
158
|
images_deleted += 1
|
|
140
159
|
log.info(f"{images_deleted} image(s) have been deleted")
|
|
141
160
|
return images_deleted
|
|
@@ -170,3 +170,14 @@ class TemplatesRepository(BaseRepository):
|
|
|
170
170
|
await self._db_session.commit()
|
|
171
171
|
await self._db_session.refresh(template_in_db)
|
|
172
172
|
return template_in_db
|
|
173
|
+
|
|
174
|
+
async def get_template_images(self, template_id: UUID) -> List[models.Image]:
|
|
175
|
+
"""
|
|
176
|
+
Return all images attached to a template.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
query = select(models.Image).\
|
|
180
|
+
join(models.Image.templates).\
|
|
181
|
+
filter(models.Template.template_id == template_id)
|
|
182
|
+
result = await self._db_session.execute(query)
|
|
183
|
+
return result.scalars().all()
|