python-openstackclient 8.3.0__py3-none-any.whl → 10.0.0__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.
Files changed (292) hide show
  1. openstackclient/__init__.py +2 -6
  2. openstackclient/api/api.py +41 -23
  3. openstackclient/api/compute_v2.py +44 -25
  4. openstackclient/api/object_store_v1.py +75 -97
  5. openstackclient/api/volume_v2.py +2 -1
  6. openstackclient/api/volume_v3.py +2 -1
  7. openstackclient/common/availability_zone.py +58 -42
  8. openstackclient/common/clientmanager.py +56 -29
  9. openstackclient/common/configuration.py +10 -3
  10. openstackclient/common/envvars.py +2 -2
  11. openstackclient/common/extension.py +14 -5
  12. openstackclient/common/limits.py +10 -5
  13. openstackclient/common/module.py +14 -6
  14. openstackclient/common/pagination.py +8 -2
  15. openstackclient/common/progressbar.py +7 -6
  16. openstackclient/common/project_cleanup.py +13 -7
  17. openstackclient/common/quota.py +126 -114
  18. openstackclient/common/versions.py +8 -2
  19. openstackclient/compute/client.py +7 -3
  20. openstackclient/compute/v2/agent.py +17 -10
  21. openstackclient/compute/v2/aggregate.py +36 -22
  22. openstackclient/compute/v2/console.py +14 -8
  23. openstackclient/compute/v2/console_connection.py +11 -3
  24. openstackclient/compute/v2/flavor.py +39 -21
  25. openstackclient/compute/v2/host.py +14 -6
  26. openstackclient/compute/v2/hypervisor.py +14 -5
  27. openstackclient/compute/v2/hypervisor_stats.py +10 -2
  28. openstackclient/compute/v2/keypair.py +29 -14
  29. openstackclient/compute/v2/server.py +251 -171
  30. openstackclient/compute/v2/server_backup.py +10 -4
  31. openstackclient/compute/v2/server_event.py +21 -12
  32. openstackclient/compute/v2/server_group.py +21 -11
  33. openstackclient/compute/v2/server_image.py +19 -10
  34. openstackclient/compute/v2/server_migration.py +24 -10
  35. openstackclient/compute/v2/server_share.py +274 -0
  36. openstackclient/compute/v2/server_volume.py +10 -4
  37. openstackclient/compute/v2/service.py +14 -7
  38. openstackclient/compute/v2/usage.py +26 -21
  39. openstackclient/identity/client.py +8 -3
  40. openstackclient/identity/common.py +103 -41
  41. openstackclient/identity/v2_0/catalog.py +14 -7
  42. openstackclient/identity/v2_0/ec2creds.py +21 -10
  43. openstackclient/identity/v2_0/endpoint.py +23 -11
  44. openstackclient/identity/v2_0/project.py +25 -14
  45. openstackclient/identity/v2_0/role.py +28 -14
  46. openstackclient/identity/v2_0/role_assignment.py +9 -3
  47. openstackclient/identity/v2_0/service.py +26 -12
  48. openstackclient/identity/v2_0/token.py +12 -5
  49. openstackclient/identity/v2_0/user.py +26 -15
  50. openstackclient/identity/v3/access_rule.py +26 -12
  51. openstackclient/identity/v3/application_credential.py +59 -24
  52. openstackclient/identity/v3/catalog.py +14 -7
  53. openstackclient/identity/v3/consumer.py +22 -11
  54. openstackclient/identity/v3/credential.py +36 -16
  55. openstackclient/identity/v3/domain.py +37 -18
  56. openstackclient/identity/v3/ec2creds.py +25 -12
  57. openstackclient/identity/v3/endpoint.py +42 -20
  58. openstackclient/identity/v3/endpoint_group.py +28 -17
  59. openstackclient/identity/v3/federation_protocol.py +71 -50
  60. openstackclient/identity/v3/group.py +55 -32
  61. openstackclient/identity/v3/identity_provider.py +92 -57
  62. openstackclient/identity/v3/implied_role.py +21 -9
  63. openstackclient/identity/v3/limit.py +115 -92
  64. openstackclient/identity/v3/mapping.py +26 -13
  65. openstackclient/identity/v3/policy.py +23 -12
  66. openstackclient/identity/v3/project.py +211 -122
  67. openstackclient/identity/v3/region.py +36 -16
  68. openstackclient/identity/v3/registered_limit.py +116 -109
  69. openstackclient/identity/v3/role.py +61 -31
  70. openstackclient/identity/v3/role_assignment.py +23 -6
  71. openstackclient/identity/v3/service.py +36 -16
  72. openstackclient/identity/v3/service_provider.py +37 -15
  73. openstackclient/identity/v3/tag.py +23 -17
  74. openstackclient/identity/v3/token.py +30 -14
  75. openstackclient/identity/v3/trust.py +32 -14
  76. openstackclient/identity/v3/unscoped_saml.py +10 -2
  77. openstackclient/identity/v3/user.py +49 -26
  78. openstackclient/image/client.py +7 -3
  79. openstackclient/image/v1/image.py +33 -26
  80. openstackclient/image/v2/cache.py +14 -9
  81. openstackclient/image/v2/image.py +76 -49
  82. openstackclient/image/v2/info.py +7 -1
  83. openstackclient/image/v2/metadef_namespaces.py +109 -13
  84. openstackclient/image/v2/metadef_objects.py +28 -15
  85. openstackclient/image/v2/metadef_properties.py +24 -13
  86. openstackclient/image/v2/metadef_resource_type_association.py +14 -7
  87. openstackclient/image/v2/metadef_resource_types.py +7 -1
  88. openstackclient/image/v2/task.py +15 -6
  89. openstackclient/locale/tr_TR/LC_MESSAGES/openstackclient.po +7 -192
  90. openstackclient/network/client.py +7 -2
  91. openstackclient/network/common.py +16 -241
  92. openstackclient/network/utils.py +36 -22
  93. openstackclient/network/v2/address_group.py +27 -16
  94. openstackclient/network/v2/address_scope.py +24 -13
  95. openstackclient/network/v2/bgpvpn/bgpvpn.py +463 -0
  96. openstackclient/network/v2/bgpvpn/constants.py +30 -0
  97. openstackclient/network/v2/bgpvpn/network_association.py +214 -0
  98. openstackclient/network/v2/bgpvpn/port_association.py +490 -0
  99. openstackclient/network/v2/bgpvpn/router_association.py +288 -0
  100. openstackclient/network/v2/default_security_group_rule.py +19 -10
  101. openstackclient/network/v2/floating_ip.py +110 -159
  102. openstackclient/network/v2/floating_ip_port_forwarding.py +30 -18
  103. openstackclient/network/v2/fwaas/__init__.py +0 -0
  104. openstackclient/network/v2/fwaas/group.py +466 -0
  105. openstackclient/network/v2/fwaas/policy.py +518 -0
  106. openstackclient/network/v2/fwaas/rule.py +574 -0
  107. openstackclient/network/v2/ip_availability.py +13 -5
  108. openstackclient/network/v2/l3_conntrack_helper.py +22 -13
  109. openstackclient/network/v2/local_ip.py +24 -13
  110. openstackclient/network/v2/local_ip_association.py +14 -7
  111. openstackclient/network/v2/ndp_proxy.py +20 -11
  112. openstackclient/network/v2/network.py +129 -196
  113. openstackclient/network/v2/network_agent.py +46 -25
  114. openstackclient/network/v2/network_auto_allocated_topology.py +22 -11
  115. openstackclient/network/v2/network_flavor.py +27 -16
  116. openstackclient/network/v2/network_flavor_profile.py +23 -12
  117. openstackclient/network/v2/network_meter.py +21 -10
  118. openstackclient/network/v2/network_meter_rule.py +21 -11
  119. openstackclient/network/v2/network_qos_policy.py +25 -15
  120. openstackclient/network/v2/network_qos_rule.py +32 -17
  121. openstackclient/network/v2/network_qos_rule_type.py +13 -5
  122. openstackclient/network/v2/network_rbac.py +23 -12
  123. openstackclient/network/v2/network_segment.py +20 -11
  124. openstackclient/network/v2/network_segment_range.py +56 -29
  125. openstackclient/network/v2/network_service_provider.py +7 -1
  126. openstackclient/network/v2/network_trunk.py +38 -22
  127. openstackclient/network/v2/port.py +54 -29
  128. openstackclient/network/v2/router.py +75 -52
  129. openstackclient/network/v2/security_group.py +87 -157
  130. openstackclient/network/v2/security_group_rule.py +100 -280
  131. openstackclient/network/v2/subnet.py +49 -28
  132. openstackclient/network/v2/subnet_pool.py +30 -17
  133. openstackclient/network/v2/taas/tap_flow.py +22 -11
  134. openstackclient/network/v2/taas/tap_mirror.py +22 -11
  135. openstackclient/network/v2/taas/tap_service.py +23 -12
  136. openstackclient/object/client.py +7 -2
  137. openstackclient/object/v1/account.py +13 -6
  138. openstackclient/object/v1/container.py +25 -15
  139. openstackclient/object/v1/object.py +25 -15
  140. openstackclient/py.typed +0 -0
  141. openstackclient/shell.py +46 -10
  142. openstackclient/tests/functional/base.py +55 -20
  143. openstackclient/tests/functional/common/test_extension.py +4 -0
  144. openstackclient/tests/functional/common/test_quota.py +3 -1
  145. openstackclient/tests/functional/compute/v2/common.py +14 -13
  146. openstackclient/tests/functional/compute/v2/test_flavor.py +3 -1
  147. openstackclient/tests/functional/compute/v2/test_server.py +3 -0
  148. openstackclient/tests/functional/identity/v2/common.py +10 -6
  149. openstackclient/tests/functional/identity/v2/test_role.py +4 -4
  150. openstackclient/tests/functional/identity/v3/common.py +25 -19
  151. openstackclient/tests/functional/identity/v3/test_group.py +20 -20
  152. openstackclient/tests/functional/identity/v3/test_idp.py +3 -1
  153. openstackclient/tests/functional/identity/v3/test_limit.py +47 -0
  154. openstackclient/tests/functional/identity/v3/test_project.py +10 -10
  155. openstackclient/tests/functional/identity/v3/test_role.py +18 -18
  156. openstackclient/tests/functional/identity/v3/test_role_assignment.py +12 -12
  157. openstackclient/tests/functional/identity/v3/test_user.py +8 -8
  158. openstackclient/tests/functional/image/base.py +1 -6
  159. openstackclient/tests/functional/image/v2/test_metadef_objects.py +69 -0
  160. openstackclient/tests/functional/network/v2/common.py +5 -2
  161. openstackclient/tests/functional/network/v2/test_floating_ip.py +10 -4
  162. openstackclient/tests/functional/network/v2/test_ip_availability.py +4 -0
  163. openstackclient/tests/functional/network/v2/test_network_meter_rule.py +3 -2
  164. openstackclient/tests/functional/network/v2/test_network_segment.py +5 -0
  165. openstackclient/tests/functional/network/v2/test_subnet.py +13 -9
  166. openstackclient/tests/functional/object/v1/common.py +4 -0
  167. openstackclient/tests/functional/volume/v2/common.py +4 -0
  168. openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +27 -11
  169. openstackclient/tests/functional/volume/v2/test_volume_type.py +2 -2
  170. openstackclient/tests/functional/volume/v3/common.py +4 -0
  171. openstackclient/tests/functional/volume/v3/test_volume_snapshot.py +56 -138
  172. openstackclient/tests/functional/volume/v3/test_volume_type.py +2 -2
  173. openstackclient/tests/unit/common/test_availability_zone.py +35 -49
  174. openstackclient/tests/unit/common/test_extension.py +2 -2
  175. openstackclient/tests/unit/common/test_module.py +12 -7
  176. openstackclient/tests/unit/common/test_project_cleanup.py +3 -1
  177. openstackclient/tests/unit/common/test_quota.py +62 -23
  178. openstackclient/tests/unit/compute/v2/fakes.py +25 -0
  179. openstackclient/tests/unit/compute/v2/test_flavor.py +28 -2
  180. openstackclient/tests/unit/compute/v2/test_keypair.py +6 -6
  181. openstackclient/tests/unit/compute/v2/test_server.py +17 -104
  182. openstackclient/tests/unit/compute/v2/test_server_share.py +287 -0
  183. openstackclient/tests/unit/identity/v3/fakes.py +3 -0
  184. openstackclient/tests/unit/identity/v3/test_group.py +4 -14
  185. openstackclient/tests/unit/identity/v3/test_identity_provider.py +303 -299
  186. openstackclient/tests/unit/identity/v3/test_limit.py +197 -145
  187. openstackclient/tests/unit/identity/v3/test_project.py +831 -512
  188. openstackclient/tests/unit/identity/v3/test_protocol.py +97 -88
  189. openstackclient/tests/unit/identity/v3/test_registered_limit.py +355 -220
  190. openstackclient/tests/unit/identity/v3/test_user.py +4 -4
  191. openstackclient/tests/unit/image/v2/test_image.py +16 -16
  192. openstackclient/tests/unit/image/v2/test_metadef_namespaces.py +105 -6
  193. openstackclient/tests/unit/network/test_common.py +0 -155
  194. openstackclient/tests/unit/network/v2/bgpvpn/__init__.py +0 -0
  195. openstackclient/tests/unit/network/v2/bgpvpn/fakes.py +179 -0
  196. openstackclient/tests/unit/network/v2/bgpvpn/test_bgpvpn.py +584 -0
  197. openstackclient/tests/unit/network/v2/bgpvpn/test_network_association.py +285 -0
  198. openstackclient/tests/unit/network/v2/bgpvpn/test_port_association.py +384 -0
  199. openstackclient/tests/unit/network/v2/bgpvpn/test_router_association.py +297 -0
  200. openstackclient/tests/unit/network/v2/fwaas/__init__.py +0 -0
  201. openstackclient/tests/unit/network/v2/fwaas/test_group.py +897 -0
  202. openstackclient/tests/unit/network/v2/fwaas/test_policy.py +869 -0
  203. openstackclient/tests/unit/network/v2/fwaas/test_rule.py +980 -0
  204. openstackclient/tests/unit/network/v2/taas/{test_osc_tap_flow.py → test_tap_flow.py} +18 -25
  205. openstackclient/tests/unit/network/v2/taas/{test_osc_tap_mirror.py → test_tap_mirror.py} +19 -29
  206. openstackclient/tests/unit/network/v2/taas/{test_osc_tap_service.py → test_tap_service.py} +19 -29
  207. openstackclient/tests/unit/network/v2/test_address_group.py +2 -2
  208. openstackclient/tests/unit/network/v2/{test_floating_ip_network.py → test_floating_ip.py} +3 -2
  209. openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py +13 -13
  210. openstackclient/tests/unit/network/v2/test_network_agent.py +8 -4
  211. openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py +3 -3
  212. openstackclient/tests/unit/network/v2/test_network_flavor.py +2 -2
  213. openstackclient/tests/unit/network/v2/test_network_qos_policy.py +1 -1
  214. openstackclient/tests/unit/network/v2/test_network_qos_rule.py +2 -2
  215. openstackclient/tests/unit/network/v2/test_network_rbac.py +1 -1
  216. openstackclient/tests/unit/network/v2/test_network_segment.py +1 -1
  217. openstackclient/tests/unit/network/v2/test_network_segment_range.py +7 -10
  218. openstackclient/tests/unit/network/v2/test_network_trunk.py +1 -1
  219. openstackclient/tests/unit/network/v2/test_router.py +8 -9
  220. openstackclient/tests/unit/network/v2/{test_security_group_network.py → test_security_group.py} +1 -20
  221. openstackclient/tests/unit/network/v2/{test_security_group_rule_network.py → test_security_group_rule.py} +7 -41
  222. openstackclient/tests/unit/network/v2/test_subnet.py +2 -1
  223. openstackclient/tests/unit/network/v2/test_subnet_pool.py +2 -1
  224. openstackclient/tests/unit/object/v1/fakes.py +8 -7
  225. openstackclient/tests/unit/object/v1/test_container.py +65 -101
  226. openstackclient/tests/unit/object/v1/test_container_all.py +8 -1
  227. openstackclient/tests/unit/object/v1/test_object.py +44 -84
  228. openstackclient/tests/unit/object/v1/test_object_all.py +8 -1
  229. openstackclient/tests/unit/test_hacking.py +108 -0
  230. openstackclient/tests/unit/volume/v2/fakes.py +1 -0
  231. openstackclient/tests/unit/volume/v2/test_consistency_group.py +8 -2
  232. openstackclient/tests/unit/volume/v2/test_volume.py +7 -6
  233. openstackclient/tests/unit/volume/v2/test_volume_backup.py +1 -5
  234. openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +2 -1
  235. openstackclient/tests/unit/volume/v2/test_volume_type.py +2 -4
  236. openstackclient/tests/unit/volume/v3/fakes.py +1 -0
  237. openstackclient/tests/unit/volume/v3/test_volume.py +94 -15
  238. openstackclient/tests/unit/volume/v3/test_volume_attachment.py +1 -1
  239. openstackclient/tests/unit/volume/v3/test_volume_backup.py +1 -5
  240. openstackclient/tests/unit/volume/v3/test_volume_snapshot.py +55 -1
  241. openstackclient/tests/unit/volume/v3/test_volume_type.py +2 -4
  242. openstackclient/volume/client.py +7 -3
  243. openstackclient/volume/v2/backup_record.py +15 -6
  244. openstackclient/volume/v2/consistency_group.py +37 -25
  245. openstackclient/volume/v2/consistency_group_snapshot.py +27 -12
  246. openstackclient/volume/v2/qos_specs.py +30 -19
  247. openstackclient/volume/v2/service.py +17 -6
  248. openstackclient/volume/v2/volume.py +69 -34
  249. openstackclient/volume/v2/volume_backend.py +19 -6
  250. openstackclient/volume/v2/volume_backup.py +48 -22
  251. openstackclient/volume/v2/volume_host.py +6 -4
  252. openstackclient/volume/v2/volume_snapshot.py +52 -26
  253. openstackclient/volume/v2/volume_transfer_request.py +33 -15
  254. openstackclient/volume/v2/volume_type.py +46 -27
  255. openstackclient/volume/v3/block_storage_cleanup.py +11 -3
  256. openstackclient/volume/v3/block_storage_cluster.py +19 -7
  257. openstackclient/volume/v3/block_storage_log_level.py +15 -6
  258. openstackclient/volume/v3/block_storage_manage.py +10 -4
  259. openstackclient/volume/v3/block_storage_resource_filter.py +17 -5
  260. openstackclient/volume/v3/service.py +16 -6
  261. openstackclient/volume/v3/volume.py +103 -46
  262. openstackclient/volume/v3/volume_attachment.py +43 -21
  263. openstackclient/volume/v3/volume_backup.py +55 -26
  264. openstackclient/volume/v3/volume_group.py +23 -13
  265. openstackclient/volume/v3/volume_group_snapshot.py +32 -13
  266. openstackclient/volume/v3/volume_group_type.py +26 -13
  267. openstackclient/volume/v3/volume_message.py +15 -7
  268. openstackclient/volume/v3/volume_snapshot.py +71 -34
  269. openstackclient/volume/v3/volume_transfer_request.py +33 -15
  270. openstackclient/volume/v3/volume_type.py +45 -27
  271. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/METADATA +6 -6
  272. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/RECORD +279 -267
  273. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/WHEEL +1 -1
  274. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/entry_points.txt +53 -1
  275. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/licenses/AUTHORS +9 -0
  276. python_openstackclient-10.0.0.dist-info/pbr.json +1 -0
  277. openstackclient/api/image_v1.py +0 -69
  278. openstackclient/api/image_v2.py +0 -79
  279. openstackclient/network/v2/floating_ip_pool.py +0 -38
  280. openstackclient/tests/functional/image/v1/test_image.py +0 -97
  281. openstackclient/tests/unit/api/test_image_v1.py +0 -96
  282. openstackclient/tests/unit/api/test_image_v2.py +0 -96
  283. openstackclient/tests/unit/network/v2/test_floating_ip_compute.py +0 -248
  284. openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py +0 -49
  285. openstackclient/tests/unit/network/v2/test_floating_ip_pool_network.py +0 -39
  286. openstackclient/tests/unit/network/v2/test_network_compute.py +0 -404
  287. openstackclient/tests/unit/network/v2/test_security_group_compute.py +0 -392
  288. openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +0 -555
  289. python_openstackclient-8.3.0.dist-info/pbr.json +0 -1
  290. /openstackclient/{tests/functional/image/v1 → network/v2/bgpvpn}/__init__.py +0 -0
  291. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/licenses/LICENSE +0 -0
  292. {python_openstackclient-8.3.0.dist-info → python_openstackclient-10.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ # Copyright (c) 2016 Juniper Networks Inc.
2
+ # All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+ # not use this file except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations
14
+ # under the License.
15
+ #
16
+
17
+ import logging
18
+ import typing as ty
19
+
20
+ from osc_lib.cli import identity as osc_id
21
+ from osc_lib.cli import parseractions
22
+ from osc_lib import exceptions
23
+ from osc_lib import utils as osc_utils
24
+ from osc_lib.utils import columns as column_util
25
+
26
+ from openstackclient import command
27
+ from openstackclient.i18n import _
28
+
29
+
30
+ LOG = logging.getLogger(__name__)
31
+
32
+ _attr_map = (
33
+ ('id', 'ID', column_util.LIST_BOTH),
34
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
35
+ ('network_id', 'Network ID', column_util.LIST_BOTH),
36
+ )
37
+ _formatters: dict[str, ty.Any] = {}
38
+
39
+
40
+ def _get_columns(item):
41
+ column_map: dict[str, str] = {}
42
+ hidden_columns = ['location', 'name', 'tenant_id']
43
+ return osc_utils.get_osc_show_columns_for_sdk_resource(
44
+ item, column_map, hidden_columns
45
+ )
46
+
47
+
48
+ class CreateBgpvpnNetAssoc(command.ShowOne):
49
+ _description = _("Create a BGP VPN network association")
50
+
51
+ def get_parser(self, prog_name):
52
+ parser = super().get_parser(prog_name)
53
+ osc_id.add_project_owner_option_to_parser(parser)
54
+ parser.add_argument(
55
+ 'bgpvpn',
56
+ metavar="<bgpvpn>",
57
+ help=_("BGP VPN to apply the network association (name or ID)"),
58
+ )
59
+ parser.add_argument(
60
+ 'resource',
61
+ metavar="<network>",
62
+ help=_("Network to associate the BGP VPN (name or ID)"),
63
+ )
64
+ return parser
65
+
66
+ def take_action(self, parsed_args):
67
+ client = self.app.client_manager.network
68
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
69
+ network = client.find_network(
70
+ parsed_args.resource, ignore_missing=False
71
+ )
72
+ body: dict[str, ty.Any] = {'network_id': network['id']}
73
+ if 'project' in parsed_args and parsed_args.project is not None:
74
+ project_id = osc_id.find_project(
75
+ self.app.client_manager.sdk_connection,
76
+ parsed_args.project,
77
+ parsed_args.project_domain,
78
+ ).id
79
+ body['project_id'] = project_id
80
+
81
+ obj = client.create_bgpvpn_network_association(bgpvpn['id'], **body)
82
+ display_columns, columns = _get_columns(obj)
83
+ data = osc_utils.get_dict_properties(
84
+ obj, columns, formatters=_formatters
85
+ )
86
+ return display_columns, data
87
+
88
+
89
+ class DeleteBgpvpnNetAssoc(command.Command):
90
+ _description = _(
91
+ "Delete a BGP VPN network association(s) for a given BGP VPN"
92
+ )
93
+
94
+ def get_parser(self, prog_name):
95
+ parser = super().get_parser(prog_name)
96
+ parser.add_argument(
97
+ 'resource_association_ids',
98
+ metavar="<network association ID>",
99
+ nargs="+",
100
+ help=_("Network association ID(s) to remove"),
101
+ )
102
+ parser.add_argument(
103
+ 'bgpvpn',
104
+ metavar="<bgpvpn>",
105
+ help=_("BGP VPN the network association belongs to (name or ID)"),
106
+ )
107
+ return parser
108
+
109
+ def take_action(self, parsed_args):
110
+ client = self.app.client_manager.network
111
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
112
+ fails = 0
113
+ for id in parsed_args.resource_association_ids:
114
+ try:
115
+ client.delete_bgpvpn_network_association(bgpvpn['id'], id)
116
+ LOG.warning(
117
+ "Network association %(id)s deleted",
118
+ {'id': id},
119
+ )
120
+ except Exception as e:
121
+ fails += 1
122
+ LOG.error(
123
+ "Failed to delete network "
124
+ "association with ID '%(id)s': %(e)s",
125
+ {'id': id, 'e': e},
126
+ )
127
+ if fails > 0:
128
+ msg = _(
129
+ "Failed to delete %(fails)s of %(total)s "
130
+ "network BGP VPN association(s)."
131
+ ) % {
132
+ 'fails': fails,
133
+ 'total': len(parsed_args.resource_association_ids),
134
+ }
135
+ raise exceptions.CommandError(msg)
136
+
137
+
138
+ class ListBgpvpnNetAssoc(command.Lister):
139
+ _description = _("List BGP VPN network associations for a given BGP VPN")
140
+
141
+ def get_parser(self, prog_name):
142
+ parser = super().get_parser(prog_name)
143
+ parser.add_argument(
144
+ 'bgpvpn',
145
+ metavar="<bgpvpn>",
146
+ help=_("BGP VPN listed associations belong to (name or ID)"),
147
+ )
148
+ parser.add_argument(
149
+ '--long',
150
+ action='store_true',
151
+ help=_("List additional fields in output"),
152
+ )
153
+ parser.add_argument(
154
+ '--property',
155
+ metavar="<key=value>",
156
+ help=_(
157
+ "Filter property to apply on returned BGP VPNs (repeat to "
158
+ "filter on multiple properties)"
159
+ ),
160
+ action=parseractions.KeyValueAction,
161
+ )
162
+ return parser
163
+
164
+ def take_action(self, parsed_args):
165
+ client = self.app.client_manager.network
166
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
167
+ params = {}
168
+ if parsed_args.property:
169
+ params.update(parsed_args.property)
170
+ objs = client.bgpvpn_network_associations(
171
+ bgpvpn['id'], retrieve_all=True, **params
172
+ )
173
+ headers, columns = column_util.get_column_definitions(
174
+ list(_attr_map), long_listing=parsed_args.long
175
+ )
176
+ return (
177
+ headers,
178
+ (
179
+ osc_utils.get_dict_properties(
180
+ s, columns, formatters=_formatters
181
+ )
182
+ for s in objs
183
+ ),
184
+ )
185
+
186
+
187
+ class ShowBgpvpnNetAssoc(command.ShowOne):
188
+ _description = _("Show information of a given BGP VPN network association")
189
+
190
+ def get_parser(self, prog_name):
191
+ parser = super().get_parser(prog_name)
192
+ parser.add_argument(
193
+ 'resource_association_id',
194
+ metavar="<network association ID>",
195
+ help=_("Network association ID to look up"),
196
+ )
197
+ parser.add_argument(
198
+ 'bgpvpn',
199
+ metavar="<bgpvpn>",
200
+ help=_("BGP VPN the association belongs to (name or ID)"),
201
+ )
202
+ return parser
203
+
204
+ def take_action(self, parsed_args):
205
+ client = self.app.client_manager.network
206
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
207
+ obj = client.get_bgpvpn_network_association(
208
+ bgpvpn['id'], parsed_args.resource_association_id
209
+ )
210
+ display_columns, columns = _get_columns(obj)
211
+ data = osc_utils.get_dict_properties(
212
+ obj, columns, formatters=_formatters
213
+ )
214
+ return display_columns, data
@@ -0,0 +1,490 @@
1
+ # Copyright (c) 2017 Juniper networks Inc.
2
+ # All Rights Reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License"); you may
5
+ # not use this file except in compliance with the License. You may obtain
6
+ # a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
+ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
+ # License for the specific language governing permissions and limitations
14
+ # under the License.
15
+ #
16
+
17
+ import logging
18
+ import typing as ty
19
+
20
+ from osc_lib.cli import format_columns
21
+ from osc_lib.cli import identity as osc_id
22
+ from osc_lib.cli import parseractions
23
+ from osc_lib import exceptions
24
+ from osc_lib import utils as osc_utils
25
+ from osc_lib.utils import columns as column_util
26
+
27
+ from openstackclient import command
28
+ from openstackclient.i18n import _
29
+
30
+ LOG = logging.getLogger(__name__)
31
+
32
+ _attr_map = (
33
+ ('id', 'ID', column_util.LIST_BOTH),
34
+ ('project_id', 'Project', column_util.LIST_LONG_ONLY),
35
+ ('port_id', 'Port ID', column_util.LIST_BOTH),
36
+ (
37
+ 'prefix_routes',
38
+ 'Prefix Routes (BGP LOCAL_PREF)',
39
+ column_util.LIST_LONG_ONLY,
40
+ ),
41
+ (
42
+ 'bgpvpn_routes',
43
+ 'BGP VPN Routes (BGP LOCAL_PREF)',
44
+ column_util.LIST_LONG_ONLY,
45
+ ),
46
+ (
47
+ 'advertise_fixed_ips',
48
+ "Advertise Port's Fixed IPs",
49
+ column_util.LIST_LONG_ONLY,
50
+ ),
51
+ )
52
+ _formatters = {
53
+ 'prefix_routes': format_columns.ListColumn,
54
+ 'bgpvpn_routes': format_columns.ListColumn,
55
+ }
56
+
57
+
58
+ def _get_columns(item):
59
+ column_map: dict[str, str] = {}
60
+ hidden_columns = ['location', 'name', 'tenant_id']
61
+ return osc_utils.get_osc_show_columns_for_sdk_resource(
62
+ item, column_map, hidden_columns
63
+ )
64
+
65
+
66
+ def _transform_resource(data):
67
+ """Transforms BGP VPN port association routes property.
68
+
69
+ Separates the two route types and formats them with ListColumn.
70
+
71
+ {'routes':
72
+ [
73
+ {'type': 'prefix', 'local_pref': 100, 'prefix': '8.8.8.0/27'},
74
+ {'type': 'bgpvpn', 'local_pref': 50,
75
+ 'bgpvpn': '157d72a9-9968-48e7-8087-6c9a9bc7a181'},
76
+ ],
77
+ }
78
+
79
+ to
80
+
81
+ {
82
+ 'prefix_routes': ['8.8.8.0/27 (100)'],
83
+ 'bgpvpn_routes': ['157d72a9-9968-48e7-8087-6c9a9bc7a181 (50)'],
84
+ }
85
+ """
86
+ for route in data.get('routes', []):
87
+ local_pref = ''
88
+ if route.get('local_pref'):
89
+ local_pref = ' ({local_pref})'
90
+ if route['type'] == 'prefix':
91
+ data.setdefault('prefix_routes', []).append(
92
+ '{}{}'.format(route['prefix'], local_pref)
93
+ )
94
+ elif route['type'] == 'bgpvpn':
95
+ data.setdefault('bgpvpn_routes', []).append(
96
+ '{}{}'.format(route['bgpvpn_id'], local_pref)
97
+ )
98
+ else:
99
+ LOG.warning("Unknown route type %s (%s).", route['type'], route)
100
+ data.pop('routes', None)
101
+ return data
102
+
103
+
104
+ def _get_common_parser(parser, action):
105
+ """Adds to parser arguments common to create, set and unset commands.
106
+
107
+ :params ArgumentParser parser: argparse object contains all command's
108
+ arguments
109
+ :params string action: 'create', 'set' or 'unset'
110
+ """
111
+ ADVERTISE_ROUTE = _(
112
+ "Fixed IPs of the port will be advertised to the BGP VPN%s"
113
+ ) % (_(' (default)') if action == 'create' else "")
114
+ NOT_ADVERTISE_ROUTE = _(
115
+ "Fixed IPs of the port will not be advertised to the BGP VPN"
116
+ )
117
+
118
+ LOCAL_PREF_VALUE = _(
119
+ ". Optionally, can control the value of the BGP "
120
+ "LOCAL_PREF of the routes that will be "
121
+ "advertised"
122
+ )
123
+
124
+ ADD_PREFIX_ROUTE = (
125
+ _("Add prefix route in CIDR notation%s") % LOCAL_PREF_VALUE
126
+ )
127
+ REMOVE_PREFIX_ROUTE = _("Remove prefix route in CIDR notation")
128
+ REPEAT_PREFIX_ROUTE = _("repeat option for multiple prefix routes")
129
+
130
+ ADD_BGVPVPN_ROUTE = (
131
+ _("Add BGP VPN route for route leaking%s") % LOCAL_PREF_VALUE
132
+ )
133
+ REMOVE_BGPVPN_ROUTE = _("Remove BGP VPN route")
134
+ REPEAT_BGPVPN_ROUTE = _("repeat option for multiple BGP VPN routes")
135
+
136
+ group_advertise_fixed_ips = parser.add_mutually_exclusive_group()
137
+ group_advertise_fixed_ips.add_argument(
138
+ '--advertise-fixed-ips',
139
+ action='store_true',
140
+ help=NOT_ADVERTISE_ROUTE if action == 'unset' else ADVERTISE_ROUTE,
141
+ )
142
+ group_advertise_fixed_ips.add_argument(
143
+ '--no-advertise-fixed-ips',
144
+ action='store_true',
145
+ help=ADVERTISE_ROUTE if action == 'unset' else NOT_ADVERTISE_ROUTE,
146
+ )
147
+
148
+ if action in ['create', 'set']:
149
+ parser.add_argument(
150
+ '--prefix-route',
151
+ metavar="prefix=<cidr>[,local_pref=<integer>]",
152
+ dest='prefix_routes',
153
+ action=parseractions.MultiKeyValueAction,
154
+ required_keys=['prefix'],
155
+ optional_keys=['local_pref'],
156
+ help=f"{ADD_PREFIX_ROUTE} ({REPEAT_PREFIX_ROUTE})",
157
+ )
158
+ parser.add_argument(
159
+ '--bgpvpn-route',
160
+ metavar="bgpvpn=<BGP VPN ID or name>[,local_pref=<integer>]",
161
+ dest='bgpvpn_routes',
162
+ action=parseractions.MultiKeyValueAction,
163
+ required_keys=['bgpvpn'],
164
+ optional_keys=['local_pref'],
165
+ help=f"{ADD_BGVPVPN_ROUTE} ({REPEAT_BGPVPN_ROUTE})",
166
+ )
167
+ else:
168
+ parser.add_argument(
169
+ '--prefix-route',
170
+ metavar="<cidr>",
171
+ dest='prefix_routes',
172
+ action='append',
173
+ help=f"{REMOVE_PREFIX_ROUTE} ({REPEAT_PREFIX_ROUTE})",
174
+ )
175
+ parser.add_argument(
176
+ '--bgpvpn-route',
177
+ metavar="<BGP VPN ID or name>",
178
+ dest='bgpvpn_routes',
179
+ action='append',
180
+ help=f"{REMOVE_BGPVPN_ROUTE} ({REPEAT_BGPVPN_ROUTE})",
181
+ )
182
+ if action != 'create':
183
+ parser.add_argument(
184
+ '--no-prefix-route' if action == 'set' else '--all-prefix-routes',
185
+ dest='purge_prefix_route',
186
+ action='store_true',
187
+ help=_('Empty prefix route list'),
188
+ )
189
+ parser.add_argument(
190
+ '--no-bgpvpn-route' if action == 'set' else '--all-bgpvpn-routes',
191
+ dest='purge_bgpvpn_route',
192
+ action='store_true',
193
+ help=_('Empty BGP VPN route list'),
194
+ )
195
+
196
+
197
+ def _args2body(client, action, bgpvpn_id, args):
198
+ attrs: dict[str, ty.Any] = {}
199
+
200
+ if action != 'create':
201
+ assoc = client.find_bgpvpn_port_association(
202
+ args.port_association_id,
203
+ bgpvpn_id=bgpvpn_id,
204
+ ignore_missing=False,
205
+ )
206
+ else:
207
+ assoc = {'routes': []}
208
+
209
+ if args.advertise_fixed_ips:
210
+ attrs['advertise_fixed_ips'] = action != 'unset'
211
+ elif args.no_advertise_fixed_ips:
212
+ attrs['advertise_fixed_ips'] = action == 'unset'
213
+
214
+ prefix_routes: dict[str, ty.Any] | None = None
215
+ if 'purge_prefix_route' in args and args.purge_prefix_route:
216
+ prefix_routes = {}
217
+ else:
218
+ prefix_routes = {
219
+ r['prefix']: r.get('local_pref')
220
+ for r in assoc['routes']
221
+ if r['type'] == 'prefix'
222
+ }
223
+ if args.prefix_routes:
224
+ if action in ['create', 'set']:
225
+ prefix_routes.update(
226
+ {
227
+ r['prefix']: r.get('local_pref')
228
+ for r in args.prefix_routes
229
+ }
230
+ )
231
+ elif action == 'unset':
232
+ for prefix in args.prefix_routes:
233
+ prefix_routes.pop(prefix, None)
234
+
235
+ bgpvpn_routes: dict[str, ty.Any] | None = None
236
+ if 'purge_bgpvpn_route' in args and args.purge_bgpvpn_route:
237
+ bgpvpn_routes = {}
238
+ else:
239
+ bgpvpn_routes = {
240
+ r['bgpvpn_id']: r.get('local_pref')
241
+ for r in assoc['routes']
242
+ if r['type'] == 'bgpvpn'
243
+ }
244
+ if args.bgpvpn_routes:
245
+ if action == 'unset':
246
+ routes = [{'bgpvpn': bgpvpn} for bgpvpn in args.bgpvpn_routes]
247
+ else:
248
+ routes = args.bgpvpn_routes
249
+ args_bgpvpn_routes = {
250
+ client.find_bgpvpn(
251
+ r['bgpvpn'], ignore_missing=False
252
+ ).id: r.get('local_pref')
253
+ for r in routes
254
+ }
255
+ if action in ['create', 'set']:
256
+ bgpvpn_routes.update(args_bgpvpn_routes)
257
+ elif action == 'unset':
258
+ for bgpvpn_id in args_bgpvpn_routes:
259
+ bgpvpn_routes.pop(bgpvpn_id, None)
260
+
261
+ if prefix_routes is not None and not prefix_routes:
262
+ attrs.setdefault('routes', [])
263
+ elif prefix_routes is not None:
264
+ for prefix, local_pref in prefix_routes.items():
265
+ route: dict[str, ty.Any] = {
266
+ 'type': 'prefix',
267
+ 'prefix': prefix,
268
+ }
269
+ if local_pref:
270
+ route['local_pref'] = int(local_pref)
271
+ attrs.setdefault('routes', []).append(route)
272
+ if bgpvpn_routes is not None and not bgpvpn_routes:
273
+ attrs.setdefault('routes', [])
274
+ elif bgpvpn_routes is not None:
275
+ for bgpvpn_id, local_pref in bgpvpn_routes.items():
276
+ route = {
277
+ 'type': 'bgpvpn',
278
+ 'bgpvpn_id': bgpvpn_id,
279
+ }
280
+ if local_pref:
281
+ route['local_pref'] = int(local_pref)
282
+ attrs.setdefault('routes', []).append(route)
283
+
284
+ return attrs
285
+
286
+
287
+ class CreateBgpvpnPortAssoc(command.ShowOne):
288
+ _description = _("Create a BGP VPN port association")
289
+
290
+ def get_parser(self, prog_name):
291
+ parser = super().get_parser(prog_name)
292
+ osc_id.add_project_owner_option_to_parser(parser)
293
+ parser.add_argument(
294
+ 'bgpvpn',
295
+ metavar="<bgpvpn>",
296
+ help=_("BGP VPN to apply the port association (name or ID)"),
297
+ )
298
+ parser.add_argument(
299
+ 'port',
300
+ metavar="<port>",
301
+ help=_("Port to associate the BGP VPN (name or ID)"),
302
+ )
303
+ _get_common_parser(parser, 'create')
304
+ return parser
305
+
306
+ def take_action(self, parsed_args):
307
+ client = self.app.client_manager.network
308
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
309
+ port = client.find_port(parsed_args.port, ignore_missing=False)
310
+ body: dict[str, ty.Any] = {'port_id': port['id']}
311
+ if 'project' in parsed_args and parsed_args.project is not None:
312
+ project_id = osc_id.find_project(
313
+ self.app.client_manager.sdk_connection,
314
+ parsed_args.project,
315
+ parsed_args.project_domain,
316
+ ).id
317
+ body['project_id'] = project_id
318
+
319
+ body.update(_args2body(client, 'create', bgpvpn['id'], parsed_args))
320
+
321
+ obj = client.create_bgpvpn_port_association(bgpvpn['id'], **body)
322
+ _transform_resource(obj)
323
+ display_columns, columns = _get_columns(obj)
324
+ data = osc_utils.get_dict_properties(
325
+ obj, columns, formatters=_formatters
326
+ )
327
+ return display_columns, data
328
+
329
+
330
+ class SetBgpvpnPortAssoc(command.Command):
331
+ _description = _("Set BGP VPN port association properties")
332
+ _action = 'set'
333
+
334
+ def get_parser(self, prog_name):
335
+ parser = super().get_parser(prog_name)
336
+ parser.add_argument(
337
+ 'port_association_id',
338
+ metavar="<port association ID>",
339
+ help=_("Port association ID to update"),
340
+ )
341
+ parser.add_argument(
342
+ 'bgpvpn',
343
+ metavar="<bgpvpn>",
344
+ help=_("BGP VPN the port association belongs to (name or ID)"),
345
+ )
346
+ _get_common_parser(parser, self._action)
347
+ return parser
348
+
349
+ def take_action(self, parsed_args):
350
+ client = self.app.client_manager.network
351
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
352
+ body = _args2body(client, self._action, bgpvpn['id'], parsed_args)
353
+ client.update_bgpvpn_port_association(
354
+ bgpvpn['id'], parsed_args.port_association_id, **body
355
+ )
356
+
357
+
358
+ class UnsetBgpvpnPortAssoc(SetBgpvpnPortAssoc):
359
+ _description = _("Unset BGP VPN port association properties")
360
+ _action = 'unset'
361
+
362
+
363
+ class DeleteBgpvpnPortAssoc(command.Command):
364
+ _description = _(
365
+ "Delete a BGP VPN port association(s) for a given BGP VPN"
366
+ )
367
+
368
+ def get_parser(self, prog_name):
369
+ parser = super().get_parser(prog_name)
370
+ parser.add_argument(
371
+ 'port_association_ids',
372
+ metavar="<port association ID>",
373
+ nargs="+",
374
+ help=_("Port association ID(s) to remove"),
375
+ )
376
+ parser.add_argument(
377
+ 'bgpvpn',
378
+ metavar="<bgpvpn>",
379
+ help=_("BGP VPN the port association belongs to (name or ID)"),
380
+ )
381
+ return parser
382
+
383
+ def take_action(self, parsed_args):
384
+ client = self.app.client_manager.network
385
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
386
+ fails = 0
387
+ for id in parsed_args.port_association_ids:
388
+ try:
389
+ client.delete_bgpvpn_port_association(bgpvpn['id'], id)
390
+ LOG.warning(
391
+ "Port association %(id)s deleted",
392
+ {'id': id},
393
+ )
394
+ except Exception as e:
395
+ fails += 1
396
+ LOG.error(
397
+ "Failed to delete port "
398
+ "association with ID '%(id)s': %(e)s",
399
+ {'id': id, 'e': e},
400
+ )
401
+ if fails > 0:
402
+ msg = _(
403
+ "Failed to delete %(fails)s of %(total)s "
404
+ "port BGP VPN association(s)."
405
+ ) % {
406
+ 'fails': fails,
407
+ 'total': len(parsed_args.port_association_ids),
408
+ }
409
+ raise exceptions.CommandError(msg)
410
+
411
+
412
+ class ListBgpvpnPortAssoc(command.Lister):
413
+ _description = _("List BGP VPN port associations for a given BGP VPN")
414
+
415
+ def get_parser(self, prog_name):
416
+ parser = super().get_parser(prog_name)
417
+ parser.add_argument(
418
+ 'bgpvpn',
419
+ metavar="<bgpvpn>",
420
+ help=_("BGP VPN listed associations belong to (name or ID)"),
421
+ )
422
+ parser.add_argument(
423
+ '--long',
424
+ action='store_true',
425
+ help=_("List additional fields in output"),
426
+ )
427
+ parser.add_argument(
428
+ '--property',
429
+ metavar="<key=value>",
430
+ help=_(
431
+ "Filter property to apply on returned BGP VPNs (repeat to "
432
+ "filter on multiple properties)"
433
+ ),
434
+ action=parseractions.KeyValueAction,
435
+ )
436
+ return parser
437
+
438
+ def take_action(self, parsed_args):
439
+ client = self.app.client_manager.network
440
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
441
+ params = {}
442
+ if parsed_args.property:
443
+ params.update(parsed_args.property)
444
+ objs = client.bgpvpn_port_associations(
445
+ bgpvpn['id'], retrieve_all=True, **params
446
+ )
447
+ transformed_objs = [_transform_resource(obj) for obj in objs]
448
+ headers, columns = column_util.get_column_definitions(
449
+ list(_attr_map), long_listing=parsed_args.long
450
+ )
451
+ return (
452
+ headers,
453
+ (
454
+ osc_utils.get_dict_properties(
455
+ s, columns, formatters=_formatters
456
+ )
457
+ for s in transformed_objs
458
+ ),
459
+ )
460
+
461
+
462
+ class ShowBgpvpnPortAssoc(command.ShowOne):
463
+ _description = _("Show information of a given BGP VPN port association")
464
+
465
+ def get_parser(self, prog_name):
466
+ parser = super().get_parser(prog_name)
467
+ parser.add_argument(
468
+ 'port_association_id',
469
+ metavar="<port association ID>",
470
+ help=_("Port association ID to look up"),
471
+ )
472
+ parser.add_argument(
473
+ 'bgpvpn',
474
+ metavar="<bgpvpn>",
475
+ help=_("BGP VPN the association belongs to (name or ID)"),
476
+ )
477
+ return parser
478
+
479
+ def take_action(self, parsed_args):
480
+ client = self.app.client_manager.network
481
+ bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)
482
+ obj = client.get_bgpvpn_port_association(
483
+ bgpvpn['id'], parsed_args.port_association_id
484
+ )
485
+ _transform_resource(obj)
486
+ display_columns, columns = _get_columns(obj)
487
+ data = osc_utils.get_dict_properties(
488
+ obj, columns, formatters=_formatters
489
+ )
490
+ return display_columns, data