diff --git a/docs/index.md b/docs/index.md index bf4a331..02be3be 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,6 +94,7 @@ $ scm-clone --help │ decryption-profiles Clone decryption profiles. │ │ dns-security-profiles Clone DNS Security profiles. │ │ edls Clone external dynamic lists. │ +│ hip-objects Clone HIP objects. │ │ security-rules Clone security rules. │ │ service-groups Clone service groupss. │ │ services Clone services. │ diff --git a/docs/user-guide/python/commands.md b/docs/user-guide/python/commands.md index 3295ca2..4c00b98 100644 --- a/docs/user-guide/python/commands.md +++ b/docs/user-guide/python/commands.md @@ -85,6 +85,7 @@ available commands and their primary purposes. | application-filters | Clone application filters | | application-groups | Clone application groups | | edls | Clone external dynamic lists | +| hip-objects | Clone HIP objects | | services | Clone services | | service-groups | Clone service groups | | tags | Clone tag objects | diff --git a/poetry.lock b/poetry.lock index d7ce0c4..2cb37e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -342,27 +342,6 @@ sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] -[[package]] -name = "dynaconf" -version = "3.2.6" -description = "The dynamic configurator for your Python Project" -optional = false -python-versions = ">=3.8" -files = [ - {file = "dynaconf-3.2.6-py2.py3-none-any.whl", hash = "sha256:3911c740d717df4576ed55f616c7cbad6e06bc8ef23ffca444b6e2a12fb1c34c"}, - {file = "dynaconf-3.2.6.tar.gz", hash = "sha256:74cc1897396380bb957730eb341cc0976ee9c38bbcb53d3307c50caed0aedfb8"}, -] - -[package.extras] -all = ["configobj", "hvac", "redis", "ruamel.yaml"] -configobj = ["configobj"] -ini = ["configobj"] -redis = ["redis"] -test = ["configobj", "django", "flask (>=0.12)", "hvac (>=1.1.0)", "pytest", "pytest-cov", "pytest-mock", "pytest-xdist", "python-dotenv", "radon", "redis", "toml"] -toml = ["toml"] -vault = ["hvac"] -yaml = ["ruamel.yaml"] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -691,13 +670,13 @@ pyyaml = ">=5.1" [[package]] name = "mkdocs-material" -version = "9.5.48" +version = "9.5.49" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.48-py3-none-any.whl", hash = "sha256:b695c998f4b939ce748adbc0d3bff73fa886a670ece948cf27818fa115dc16f8"}, - {file = "mkdocs_material-9.5.48.tar.gz", hash = "sha256:a582531e8b34f4c7ed38c29d5c44763053832cf2a32f7409567e0c74749a47db"}, + {file = "mkdocs_material-9.5.49-py3-none-any.whl", hash = "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"}, + {file = "mkdocs_material-9.5.49.tar.gz", hash = "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d"}, ] [package.dependencies] @@ -874,13 +853,13 @@ lint = ["black"] [[package]] name = "pan-scm-sdk" -version = "0.3.6" +version = "0.3.7" description = "Python SDK for Palo Alto Networks Strata Cloud Manager." optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "pan_scm_sdk-0.3.6-py3-none-any.whl", hash = "sha256:ca0486755fd2bb7191ea0e0a142e3a3eaea31b2edbf90c65bd4d828ec43d1d9c"}, - {file = "pan_scm_sdk-0.3.6.tar.gz", hash = "sha256:25b1f08d3ae377c68d87a3bb9a41b52f7f05bb8e2141f3da3cfbcba5d428ace2"}, + {file = "pan_scm_sdk-0.3.7-py3-none-any.whl", hash = "sha256:3b2d0a758d724ba062af522bb0c9a127acfbb773cd92ee543aea06893b2916e1"}, + {file = "pan_scm_sdk-0.3.7.tar.gz", hash = "sha256:953c347a4d4f66130934f97f0b3b7de0461f1726086d82a75f011e32a1a6ab0d"}, ] [package.dependencies] @@ -1043,18 +1022,18 @@ files = [ [[package]] name = "pydantic" -version = "2.10.3" +version = "2.10.4" description = "Data validation using Python type hints" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"}, - {file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"}, + {file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"}, + {file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.27.1" +pydantic-core = "2.27.2" typing-extensions = ">=4.12.2" [package.extras] @@ -1063,111 +1042,111 @@ timezone = ["tzdata"] [[package]] name = "pydantic-core" -version = "2.27.1" +version = "2.27.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"}, - {file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"}, - {file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"}, - {file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"}, - {file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"}, - {file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"}, - {file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"}, - {file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"}, - {file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"}, - {file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"}, - {file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"}, - {file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"}, - {file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"}, - {file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"}, - {file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"}, - {file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"}, - {file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"}, - {file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"}, - {file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"}, - {file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"}, - {file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"}, - {file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"}, - {file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"}, - {file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"}, - {file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"}, - {file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"}, - {file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"}, - {file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"}, - {file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"}, - {file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"}, - {file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"}, - {file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"}, - {file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"}, - {file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"}, - {file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"}, - {file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"}, - {file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, ] [package.dependencies] @@ -1633,13 +1612,13 @@ files = [ [[package]] name = "typer" -version = "0.12.5" +version = "0.15.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, - {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, + {file = "typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847"}, + {file = "typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a"}, ] [package.dependencies] @@ -1732,4 +1711,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c57f9c1dbadba9bdf7d3ae960638d4a6fc55be6da73041184c503ea21c1c0247" +content-hash = "1a7592e434c15d560ed643de8f3cdbe19fbf7cdc2a36044c0f3513c6c6016d3c" diff --git a/pyproject.toml b/pyproject.toml index 8b3478d..4e69e08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scm-config-clone" -version = "0.2.2" +version = "0.2.4" description = "A command-line tool to clone configuration objects between Palo Alto Networks Strata Cloud Manager (SCM) tenants." authors = ["Calvin Remsburg "] license = "Apache 2.0" @@ -8,10 +8,9 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" -dynaconf = "^3.2.6" -typer = "^0.12.5" +typer = "^0.15.1" setuptools = "^75.1.0" -pan-scm-sdk = "^0.3.4" +pan-scm-sdk = "^0.3.7" tabulate = "^0.9.0" pandas = "^2.2.3" pyyaml = "^6.0.2" diff --git a/scm_config_clone/__init__.py b/scm_config_clone/__init__.py index 50ced6e..34f4438 100644 --- a/scm_config_clone/__init__.py +++ b/scm_config_clone/__init__.py @@ -6,6 +6,7 @@ from .commands.objects.application_filters import application_filters from .commands.objects.application_group import application_groups from .commands.objects.external_dynamic_lists import external_dynamic_lists +from .commands.objects.hip_objects import hip_objects from .commands.objects.service import services from .commands.objects.service_group import service_groups from .commands.objects.tag import tags diff --git a/scm_config_clone/commands/objects/address.py b/scm_config_clone/commands/objects/address.py index 02b6e8c..9b55954 100644 --- a/scm_config_clone/commands/objects/address.py +++ b/scm_config_clone/commands/objects/address.py @@ -16,7 +16,11 @@ from scm.models.objects.address import AddressCreateModel, AddressResponseModel from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params(src_obj: AddressResponseModel, folder: str) -> Dict[str, Any]: @@ -95,7 +99,6 @@ def addresses( help="If set, commit the changes on the destination tenant after object creation.", is_flag=True, ), - # Existing flag that already was present auto_approve: bool = typer.Option( None, "--auto-approve", @@ -103,7 +106,6 @@ def addresses( help="If set, skip the confirmation prompt and automatically proceed with creation.", is_flag=True, ), - # New flags introduced create_report: bool = typer.Option( None, "--create-report", @@ -176,7 +178,6 @@ def addresses( settings = load_settings(settings_file) # Apply fallback logic: if a flag wasn't provided at runtime, use settings.yaml values - # If a flag is provided (not None), use the provided value; otherwise, use settings default. auto_approve = settings["auto_approve"] if auto_approve is None else auto_approve create_report = ( settings["create_report"] if create_report is None else create_report @@ -197,7 +198,7 @@ def addresses( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -214,10 +215,29 @@ def addresses( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) - # Retrieve address objects from the source + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + + # Retrieve address objects from source try: source_addresses = Address(source_client, max_limit=5000) - address_objects = source_addresses.list( + source_objects = source_addresses.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -225,53 +245,52 @@ def addresses( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(address_objects)} address objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} address objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving address objects from source: {e}") raise typer.Exit(code=1) - # If not quiet_mode, display retrieved objects - if address_objects and not quiet_mode: + # Retrieve address objects from destination + try: + destination_addresses = Address(destination_client, max_limit=5000) + destination_objects = destination_addresses.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} address objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving address objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: addr_table = [] - for addr in address_objects: - if addr.ip_netmask: - addr_value = addr.ip_netmask - elif addr.fqdn: - addr_value = addr.fqdn - elif addr.ip_range: - addr_value = addr.ip_range - elif addr.ip_wildcard: - addr_value = addr.ip_wildcard - else: - addr_value = "Unknown Type" - - addr_table.append( - [ - addr.name, - addr.folder, - addr_value, - addr.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + addr_table.append([result["name"], status]) typer.echo( tabulate( addr_table, - headers=[ - "Name", - "Folder", - "Value", - "Description", - ], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not address_objects: - typer.echo("No address objects found in the source folder.") - # Prompt for confirmation if not auto-approved and objects found - if address_objects and not auto_approve: + # Prompt if not auto-approved and objects exist + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -279,50 +298,37 @@ def addresses( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create address objects in destination - destination_addresses = Address( - destination_client, - max_limit=5000, - ) + destination_addresses = Address(destination_client, max_limit=5000) created_objs: List[AddressResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in address_objects: - try: - create_params = build_create_params( - src_obj, - folder, + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of address object in destination (dry run): {src_obj.name}" ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Address,{src_obj.name},{src_obj.folder}\n") + + try: + create_params = build_create_params(src_obj, folder) except ValueError as ve: - error_objects.append( - [ - src_obj.name, - str(ve), - ] - ) + error_objects.append([src_obj.name, str(ve)]) continue - # If dry_run is True, we might skip actual creation in the future. - # For now, just proceed as normal until logic is implemented. try: new_obj = destination_addresses.create(create_params) created_objs.append(new_obj) @@ -333,46 +339,24 @@ def addresses( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue - # If not quiet_mode, display results + # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following address objects:") created_table = [] for obj in created_objs: - if obj.ip_netmask: - value = obj.ip_netmask - elif obj.fqdn: - value = obj.fqdn - elif obj.ip_range: - value = obj.ip_range - elif obj.ip_wildcard: - value = obj.ip_wildcard - else: - value = "Unknown Type" - - created_table.append( - [ - obj.name, - obj.folder, - value, - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=[ - "Name", - "Folder", - "Value", - "Description", - ], + headers=["Name"], tablefmt="fancy_grid", ) ) @@ -382,10 +366,7 @@ def addresses( typer.echo( tabulate( error_objects, - headers=[ - "Object Name", - "Error", - ], + headers=["Object Name", "Error"], tablefmt="fancy_grid", ) ) @@ -414,7 +395,4 @@ def addresses( else: logger.info("No new address objects were created, skipping commit.") - # If create_report is True, in the future we will append results to 'result.csv' - # For now, logic can be implemented later. - typer.echo("🎉 Address objects cloning completed successfully! 🎉") diff --git a/scm_config_clone/commands/objects/address_group.py b/scm_config_clone/commands/objects/address_group.py index 4b88430..e0af824 100644 --- a/scm_config_clone/commands/objects/address_group.py +++ b/scm_config_clone/commands/objects/address_group.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -194,7 +198,7 @@ def address_groups( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -211,10 +215,29 @@ def address_groups( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve address group objects from source try: source_address_groups = AddressGroup(source_client, max_limit=5000) - address_group_objects = source_address_groups.list( + source_objects = source_address_groups.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -222,48 +245,52 @@ def address_groups( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(address_group_objects)} address group objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} address group objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving address group objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved groups if not quiet_mode - if address_group_objects and not quiet_mode: + # Retrieve address group objects from destination + try: + destination_address_groups = AddressGroup(destination_client, max_limit=5000) + destination_objects = destination_address_groups.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} address group objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving address group objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: group_table = [] - for group in address_group_objects: - if group.static: - group_type = "Static" - group_value = ", ".join(group.static) - elif group.dynamic: - group_type = "Dynamic" - group_value = group.dynamic.filter - else: - group_type = "Unknown" - group_value = "N/A" - - group_table.append( - [ - group.name, - group.folder, - group_type, - group_value, - group.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + group_table.append([result["name"], status]) typer.echo( tabulate( group_table, - headers=["Name", "Folder", "Type", "Value", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not address_group_objects: - typer.echo("No address group objects found in the source folder.") # Prompt if not auto-approved and objects exist - if address_group_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -271,31 +298,31 @@ def address_groups( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create address group objects in destination destination_address_groups = AddressGroup(destination_client, max_limit=5000) created_objs: List[AddressGroupResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in address_group_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of address group object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Address Group,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -312,10 +339,11 @@ def address_groups( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -323,30 +351,12 @@ def address_groups( typer.echo("\nSuccessfully created the following address group objects:") created_table = [] for obj in created_objs: - if obj.static: - group_type = "Static" - group_value = ", ".join(obj.static) - elif obj.dynamic: - group_type = "Dynamic" - group_value = obj.dynamic.filter - else: - group_type = "Unknown" - group_value = "N/A" - - created_table.append( - [ - obj.name, - obj.folder, - group_type, - group_value, - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Type", "Value", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/application.py b/scm_config_clone/commands/objects/application.py index 608fab2..d503511 100644 --- a/scm_config_clone/commands/objects/application.py +++ b/scm_config_clone/commands/objects/application.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -189,7 +193,7 @@ def applications( exclude_folders_list = parse_csv_option(exclude_folders) exclude_snippets_list = parse_csv_option(exclude_snippets) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -206,58 +210,80 @@ def applications( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve application objects from source try: source_applications = Application(source_client, max_limit=5000) - application_objects = source_applications.list( + source_objects = source_applications.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, exclude_snippets=exclude_snippets_list, ) logger.info( - f"Retrieved {len(application_objects)} application objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} application objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving application objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved applications if not quiet_mode - if application_objects and not quiet_mode: - app_table = [ - [ - app.name, - app.folder, - app.category, - app.subcategory, - app.technology, - app.risk, - app.description or "", - ", ".join(app.ports) if app.ports else "", - ] - for app in application_objects - ] + # Retrieve application objects from destination + try: + destination_applications = Application(destination_client, max_limit=5000) + destination_objects = destination_applications.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} application objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving application objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + app_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + app_table.append([result["name"], status]) + typer.echo( tabulate( app_table, - headers=[ - "Name", - "Folder", - "Category", - "Subcategory", - "Technology", - "Risk", - "Description", - "Ports", - ], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not application_objects: - typer.echo("No application objects found in the source folder.") # Prompt if not auto-approved and objects exist - if application_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -265,31 +291,31 @@ def applications( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create application objects in destination destination_applications = Application(destination_client, max_limit=5000) created_objs: List[ApplicationResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in application_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of application object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Application,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -306,42 +332,24 @@ def applications( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following application objects:") - created_table = [ - [ - obj.name, - obj.folder, - obj.category, - obj.subcategory, - obj.technology, - obj.risk, - obj.description or "", - ", ".join(obj.ports) if obj.ports else "", - ] - for obj in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=[ - "Name", - "Folder", - "Category", - "Subcategory", - "Technology", - "Risk", - "Description", - "Ports", - ], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/application_filters.py b/scm_config_clone/commands/objects/application_filters.py index 299afe4..f0f5624 100644 --- a/scm_config_clone/commands/objects/application_filters.py +++ b/scm_config_clone/commands/objects/application_filters.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -188,7 +192,7 @@ def application_filters( exclude_folders_list = parse_csv_option(exclude_folders) exclude_snippets_list = parse_csv_option(exclude_snippets) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -205,52 +209,82 @@ def application_filters( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve application filter objects from source try: source_app_filters = ApplicationFilters(source_client, max_limit=5000) - app_filter_objects = source_app_filters.list( + source_objects = source_app_filters.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, exclude_snippets=exclude_snippets_list, ) logger.info( - f"Retrieved {len(app_filter_objects)} application filter objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} application filter objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving application filter objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved filters if not quiet_mode - if app_filter_objects and not quiet_mode: + # Retrieve application filter objects from destination + try: + destination_app_filters = ApplicationFilters(destination_client, max_limit=5000) + destination_objects = destination_app_filters.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} application filter objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving application filter objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: filter_table = [] - for app_filter in app_filter_objects: - filter_table.append( - [ - app_filter.name, - app_filter.folder, - ", ".join(app_filter.category) if app_filter.category else "", - ( - ", ".join(str(r) for r in app_filter.risk) - if app_filter.risk - else "" - ), - app_filter.is_saas, - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + filter_table.append([result["name"], status]) typer.echo( tabulate( filter_table, - headers=["Name", "Folder", "Categories", "Risk Levels", "SaaS"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not app_filter_objects: - typer.echo("No application filter objects found in the source folder.") # Prompt if not auto-approved and objects exist - if app_filter_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -258,31 +292,31 @@ def application_filters( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create application filter objects in destination destination_app_filters = ApplicationFilters(destination_client, max_limit=5000) created_objs: List[ApplicationFiltersResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in app_filter_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of application filter object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Application Filter,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -301,10 +335,11 @@ def application_filters( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -312,20 +347,12 @@ def application_filters( typer.echo("\nSuccessfully created the following application filter objects:") created_table = [] for obj in created_objs: - created_table.append( - [ - obj.name, - obj.folder, - ", ".join(obj.category) if obj.category else "", - ", ".join(str(r) for r in obj.risk) if obj.risk else "", - obj.is_saas, - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Categories", "Risk Levels", "SaaS"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/application_group.py b/scm_config_clone/commands/objects/application_group.py index a759854..ae57f9e 100644 --- a/scm_config_clone/commands/objects/application_group.py +++ b/scm_config_clone/commands/objects/application_group.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -181,7 +185,7 @@ def application_groups( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -198,10 +202,29 @@ def application_groups( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve application group objects from source try: source_application_groups = ApplicationGroup(source_client, max_limit=5000) - application_group_objects = source_application_groups.list( + source_objects = source_application_groups.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -209,34 +232,56 @@ def application_groups( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(application_group_objects)} application group objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} application group objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving application group objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved groups if not quiet_mode - if application_group_objects and not quiet_mode: - group_table = [ - [ - group.name, - group.folder, - ", ".join(group.members), - ] - for group in application_group_objects - ] + # Retrieve application group objects from destination + try: + destination_application_groups = ApplicationGroup( + destination_client, max_limit=5000 + ) + destination_objects = destination_application_groups.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} application group objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving application group objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + group_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + group_table.append([result["name"], status]) + typer.echo( tabulate( group_table, - headers=["Name", "Folder", "Members"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not application_group_objects: - typer.echo("No application group objects found in the source folder.") # Prompt if not auto-approved and objects exist - if application_group_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -244,24 +289,14 @@ def application_groups( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create application group objects in destination destination_application_groups = ApplicationGroup( @@ -270,7 +305,17 @@ def application_groups( created_objs: List[ApplicationGroupResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in application_group_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of application group object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Application Group,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -289,28 +334,24 @@ def application_groups( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following application group objects:") - created_table = [ - [ - obj.name, - obj.folder, - ", ".join(obj.members), - ] - for obj in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Members"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/external_dynamic_lists.py b/scm_config_clone/commands/objects/external_dynamic_lists.py index ffaa6c1..a91b3e7 100644 --- a/scm_config_clone/commands/objects/external_dynamic_lists.py +++ b/scm_config_clone/commands/objects/external_dynamic_lists.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -188,7 +192,7 @@ def external_dynamic_lists( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -205,10 +209,29 @@ def external_dynamic_lists( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve EDL objects from source try: source_edls = ExternalDynamicLists(source_client, max_limit=5000) - edl_objects = source_edls.list( + source_objects = source_edls.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -216,52 +239,52 @@ def external_dynamic_lists( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(edl_objects)} EDL objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} EDL objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving EDL objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved EDLs if not quiet_mode - if edl_objects and not quiet_mode: - edl_table = [] - for edl in edl_objects: - edl_type = ( - next(iter(edl.type.model_dump().keys())) if edl.type else "Unknown" - ) - edl_url = ( - getattr(getattr(edl.type, edl_type), "url", "N/A") - if edl.type - else "N/A" - ) - edl_description = ( - getattr(getattr(edl.type, edl_type), "description", "") - if edl.type - else "" - ) + # Retrieve EDL objects from destination + try: + destination_edls = ExternalDynamicLists(destination_client, max_limit=5000) + destination_objects = destination_edls.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} EDL objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving EDL objects from destination: {e}") + raise typer.Exit(code=1) - edl_table.append( - [ - edl.name, - edl.folder, - edl_type, - edl_url, - edl_description or "", - ] - ) + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + edl_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + edl_table.append([result["name"], status]) typer.echo( tabulate( edl_table, - headers=["Name", "Folder", "Type", "URL", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not edl_objects: - typer.echo("No EDL objects found in the source folder.") # Prompt if not auto-approved and objects exist - if edl_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -269,31 +292,31 @@ def external_dynamic_lists( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create EDL objects in destination destination_edls = ExternalDynamicLists(destination_client, max_limit=5000) created_objs: List[ExternalDynamicListsResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in edl_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of EDL object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"EDL,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -310,10 +333,11 @@ def external_dynamic_lists( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -321,34 +345,12 @@ def external_dynamic_lists( typer.echo("\nSuccessfully created the following EDL objects:") created_table = [] for obj in created_objs: - obj_type = ( - next(iter(obj.type.model_dump().keys())) if obj.type else "Unknown" - ) - obj_url = ( - getattr(getattr(obj.type, obj_type), "url", "N/A") - if obj.type - else "N/A" - ) - obj_description = ( - getattr(getattr(obj.type, obj_type), "description", "") - if obj.type - else "" - ) - - created_table.append( - [ - obj.name, - obj.folder, - obj_type, - obj_url, - obj_description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Type", "URL", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/hip_objects.py b/scm_config_clone/commands/objects/hip_objects.py new file mode 100644 index 0000000..b46c729 --- /dev/null +++ b/scm_config_clone/commands/objects/hip_objects.py @@ -0,0 +1,396 @@ +# scm_config_clone/commands/objects/hip_object.py + +import logging +from typing import List, Optional, Any, Dict + +import typer +from scm.client import Scm +from scm.config.objects import HIPObject +from scm.exceptions import ( + AuthenticationError, + InvalidObjectError, + MissingQueryParameterError, + NameNotUniqueError, + ObjectNotPresentError, +) +from scm.models.objects.hip_object import HIPObjectCreateModel, HIPObjectResponseModel +from tabulate import tabulate + +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) + + +def build_create_params(src_obj: HIPObjectResponseModel, folder: str) -> Dict[str, Any]: + """ + Construct the dictionary of parameters required to create a new HIP object. + + Given an existing HIPObjectResponseModel (source object) and a target folder, + this function builds a dictionary with all necessary fields for creating + a new HIP object in the destination tenant. It uses `model_dump` on a Pydantic model + to ensure only valid, explicitly set fields are included. + + Args: + src_obj: The HIPObjectResponseModel representing the source HIP object. + folder: The folder in the destination tenant where the object should be created. + + Returns: + A dictionary containing the fields required for `HIPObject.create()`. + This dictionary is validated and pruned by HIPObjectCreateModel. + """ + data = { + "name": src_obj.name, + "folder": folder, + "description": src_obj.description if src_obj.description is not None else None, + "host_info": src_obj.host_info.model_dump() if src_obj.host_info else None, + "network_info": ( + src_obj.network_info.model_dump() if src_obj.network_info else None + ), + "patch_management": ( + src_obj.patch_management.model_dump() if src_obj.patch_management else None + ), + "disk_encryption": ( + src_obj.disk_encryption.model_dump() if src_obj.disk_encryption else None + ), + "mobile_device": ( + src_obj.mobile_device.model_dump() if src_obj.mobile_device else None + ), + "certificate": ( + src_obj.certificate.model_dump() if src_obj.certificate else None + ), + } + + create_model = HIPObjectCreateModel(**data) + return create_model.model_dump( + exclude_unset=True, + exclude_none=True, + ) + + +def hip_objects( + folder: Optional[str] = typer.Option( + None, + "--folder", + prompt="Please enter the folder name", + help="The folder to focus on when retrieving and cloning HIP objects.", + ), + exclude_folders: str = typer.Option( + None, + "--exclude-folders", + help="Comma-separated list of folders to exclude from the retrieval.", + ), + exclude_snippets: str = typer.Option( + None, + "--exclude-snippets", + help="Comma-separated list of snippets to exclude from the retrieval.", + ), + exclude_devices: str = typer.Option( + None, + "--exclude-devices", + help="Comma-separated list of devices to exclude from the retrieval.", + ), + commit_and_push: bool = typer.Option( + False, + "--commit-and-push", + help="If set, commit the changes on the destination tenant after object creation.", + is_flag=True, + ), + auto_approve: bool = typer.Option( + None, + "--auto-approve", + "-A", + help="If set, skip the confirmation prompt and automatically proceed with creation.", + is_flag=True, + ), + create_report: bool = typer.Option( + None, + "--create-report", + "-R", + help="If set, create or append to a 'result.csv' file with the task results.", + is_flag=True, + ), + dry_run: bool = typer.Option( + None, + "--dry-run", + "-D", + help="If set, perform a dry run without applying any changes.", + is_flag=True, + ), + quiet_mode: bool = typer.Option( + None, + "--quiet-mode", + "-Q", + help="If set, hide all console output (except log messages).", + is_flag=True, + ), + logging_level: str = typer.Option( + None, + "--logging-level", + "-L", + help="Override the logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).", + ), + settings_file: str = typer.Option( + "settings.yaml", + "--settings-file", + "-s", + help="Path to the YAML settings file containing tenant credentials and configuration.", + ), +): + """ + Clone HIP objects from a source SCM tenant to a destination SCM tenant. + + This Typer CLI command automates the process of retrieving HIP objects + from a specified folder in a source tenant, optionally filters them out based + on user-defined exclusion criteria, and then creates them in a destination tenant. + + The workflow is: + 1. Load authentication and configuration settings (e.g., credentials, logging) from the YAML file. + 2. If any runtime flags are provided, they override the corresponding settings from the file. + 3. Authenticate to the source tenant and retrieve HIP objects from the given folder. + 4. Display the retrieved source objects. If not auto-approved, prompt the user before proceeding. + 5. Authenticate to the destination tenant and create the retrieved objects there. + 6. If `--commit-and-push` is provided and objects were created successfully, commit the changes. + 7. Display the results, including successfully created objects and any errors. + + Args: + folder: The source folder from which to retrieve HIP objects. + exclude_folders: Comma-separated folder names to exclude from source retrieval. + exclude_snippets: Comma-separated snippet names to exclude from source retrieval. + exclude_devices: Comma-separated device names to exclude from source retrieval. + commit_and_push: If True, commit changes in the destination tenant after creation. + auto_approve: If True or set in settings, skip the confirmation prompt before creating objects. + create_report: If True or set in settings, create/append a CSV file with task results. + dry_run: If True or set in settings, perform a dry run without applying changes (logic TBD). + quiet_mode: If True or set in settings, hide console output except log messages (logic TBD). + logging_level: If provided, override the logging level from settings.yaml. + settings_file: Path to the YAML settings file for loading authentication and configuration. + + Raises: + typer.Exit: Exits if authentication fails, retrieval fails, or if the user opts not to proceed. + """ + typer.echo("🚀 Starting HIP objects cloning...") + + # Load settings from file + settings = load_settings(settings_file) + + # Apply fallback logic: if a flag wasn't provided at runtime, use settings.yaml values + auto_approve = settings["auto_approve"] if auto_approve is None else auto_approve + create_report = ( + settings["create_report"] if create_report is None else create_report + ) + dry_run = settings["dry_run"] if dry_run is None else dry_run + quiet_mode = settings["quiet"] if quiet_mode is None else quiet_mode + + # Logging level fallback + if logging_level is None: + logging_level = settings["logging"] + logging_level = logging_level.upper() + + logger = logging.getLogger(__name__) + logger.setLevel(getattr(logging, logging_level, logging.INFO)) + + # Parse CSV options + exclude_folders_list = parse_csv_option(exclude_folders) + exclude_snippets_list = parse_csv_option(exclude_snippets) + exclude_devices_list = parse_csv_option(exclude_devices) + + # Authenticate with source + try: + source_creds = settings["source_scm"] + source_client = Scm( + client_id=source_creds["client_id"], + client_secret=source_creds["client_secret"], + tsg_id=source_creds["tenant"], + log_level=logging_level, + ) + logger.info(f"Authenticated with source SCM tenant: {source_creds['tenant']}") + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with source tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with source authentication: {e}") + raise typer.Exit(code=1) + + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + + # Retrieve HIP objects from source + try: + source_hip_objects = HIPObject(source_client, max_limit=5000) + source_objects = source_hip_objects.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(source_objects)} HIP objects from source tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving HIP objects from source: {e}") + raise typer.Exit(code=1) + + # Retrieve HIP objects from destination + try: + destination_hip_objects = HIPObject(destination_client, max_limit=5000) + destination_objects = destination_hip_objects.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} HIP objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving HIP objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + hip_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + hip_table.append([result["name"], status]) + + typer.echo( + tabulate( + hip_table, + headers=["Name", "Destination Status"], + tablefmt="fancy_grid", + ) + ) + + # Prompt if not auto-approved and objects exist + if source_objects and not auto_approve: + proceed = typer.confirm( + "Do you want to proceed with creating these objects in the destination tenant?" + ) + if not proceed: + typer.echo("Aborting cloning operation.") + raise typer.Exit(code=0) + + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] + + # Create HIP objects in destination + destination_hip_objects = HIPObject(destination_client, max_limit=5000) + created_objs: List[HIPObjectResponseModel] = [] + error_objects: List[List[str]] = [] + + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of HIP object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"HIP Object,{src_obj.name},{src_obj.folder}\n") + + try: + create_params = build_create_params(src_obj, folder) + except ValueError as ve: + error_objects.append([src_obj.name, str(ve)]) + continue + + try: + new_obj = destination_hip_objects.create(create_params) + created_objs.append(new_obj) + logger.info(f"Created HIP object in destination: {new_obj.name}") + except ( + InvalidObjectError, + MissingQueryParameterError, + NameNotUniqueError, + ObjectNotPresentError, + ) as e: + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) + continue + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) + continue + + # Display results if not quiet_mode + if created_objs and not quiet_mode: + typer.echo("\nSuccessfully created the following HIP objects:") + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) + + typer.echo( + tabulate( + created_table, + headers=["Name"], + tablefmt="fancy_grid", + ) + ) + + if error_objects and not quiet_mode: + typer.echo("\nSome HIP objects failed to be created:") + typer.echo( + tabulate( + error_objects, + headers=["Object Name", "Error"], + tablefmt="fancy_grid", + ) + ) + + # Commit changes if requested and objects were created + if commit_and_push and created_objs: + try: + commit_params = { + "folders": [folder], + "description": "Cloned HIP objects", + "sync": True, + } + result = destination_hip_objects.commit(**commit_params) + job_status = destination_hip_objects.get_job_status(result.job_id) + logger.info( + f"Commit job ID {result.job_id} status: {job_status.data[0].status_str}" + ) + except Exception as e: + logger.error(f"Error committing HIP objects in destination: {e}") + raise typer.Exit(code=1) + else: + if created_objs and not commit_and_push: + logger.info( + "Objects created, but --commit-and-push not specified, skipping commit." + ) + else: + logger.info("No new HIP objects were created, skipping commit.") + + typer.echo("🎉 HIP objects cloning completed successfully! 🎉") diff --git a/scm_config_clone/commands/objects/service.py b/scm_config_clone/commands/objects/service.py index 0240c91..9f51d52 100644 --- a/scm_config_clone/commands/objects/service.py +++ b/scm_config_clone/commands/objects/service.py @@ -16,7 +16,11 @@ from scm.models.objects.service import ServiceCreateModel, ServiceResponseModel from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params(src_obj: ServiceResponseModel, folder: str) -> Dict[str, Any]: @@ -174,7 +178,7 @@ def services( commit_and_push: If True, commit changes in the destination tenant after creation. auto_approve: If True or set in settings, skip the confirmation prompt before creating objects. create_report: If True or set in settings, create/append a CSV file with task results. - dry_run: If True or set in settings, perform a dry run without applying changes (logic TBD). + dry_run: If True or set in settings, perform a dry run without applying any changes (logic TBD). quiet_mode: If True or set in settings, hide console output except log messages (logic TBD). logging_level: If provided, override the logging level from settings.yaml. settings_file: Path to the YAML settings file for loading authentication and configuration. @@ -208,7 +212,7 @@ def services( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -225,10 +229,29 @@ def services( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve service objects from source try: source_services = Service(source_client, max_limit=5000) - service_objects = source_services.list( + source_objects = source_services.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -236,67 +259,52 @@ def services( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(service_objects)} service objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} service objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving service objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved services if not quiet_mode - if service_objects and not quiet_mode: + # Retrieve service objects from destination + try: + destination_services = Service(destination_client, max_limit=5000) + destination_objects = destination_services.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} service objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving service objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: service_table = [] - for svc in service_objects: - if svc.protocol.tcp: - protocol_type = "TCP" - port_value = svc.protocol.tcp.port - timeout = ( - svc.protocol.tcp.override.timeout - if svc.protocol.tcp.override - else None - ) - elif svc.protocol.udp: - protocol_type = "UDP" - port_value = svc.protocol.udp.port - timeout = ( - svc.protocol.udp.override.timeout - if svc.protocol.udp.override - else None - ) - else: - protocol_type = "Unknown" - port_value = "N/A" - timeout = None - - service_table.append( - [ - svc.name, - svc.folder, - protocol_type, - port_value, - timeout or "Default", - svc.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + service_table.append([result["name"], status]) typer.echo( tabulate( service_table, - headers=[ - "Name", - "Folder", - "Protocol", - "Ports", - "Timeout", - "Description", - ], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not service_objects: - typer.echo("No service objects found in the source folder.") # Prompt if not auto-approved and objects exist - if service_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -304,31 +312,31 @@ def services( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create service objects in destination destination_services = Service(destination_client, max_limit=5000) created_objs: List[ServiceResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in service_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of service object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Service,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -345,10 +353,11 @@ def services( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -356,49 +365,12 @@ def services( typer.echo("\nSuccessfully created the following service objects:") created_table = [] for obj in created_objs: - if obj.protocol.tcp: - protocol_type = "TCP" - port_value = obj.protocol.tcp.port - timeout = ( - obj.protocol.tcp.override.timeout - if obj.protocol.tcp.override - else None - ) - elif obj.protocol.udp: - protocol_type = "UDP" - port_value = obj.protocol.udp.port - timeout = ( - obj.protocol.udp.override.timeout - if obj.protocol.udp.override - else None - ) - else: - protocol_type = "Unknown" - port_value = "N/A" - timeout = None - - created_table.append( - [ - obj.name, - obj.folder, - protocol_type, - port_value, - timeout or "Default", - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=[ - "Name", - "Folder", - "Protocol", - "Ports", - "Timeout", - "Description", - ], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/service_group.py b/scm_config_clone/commands/objects/service_group.py index ecb21fd..0ce4d6d 100644 --- a/scm_config_clone/commands/objects/service_group.py +++ b/scm_config_clone/commands/objects/service_group.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -182,7 +186,7 @@ def service_groups( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -199,10 +203,29 @@ def service_groups( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve service group objects from source try: source_service_groups = ServiceGroup(source_client, max_limit=5000) - service_group_objects = source_service_groups.list( + source_objects = source_service_groups.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -210,35 +233,52 @@ def service_groups( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(service_group_objects)} service group objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} service group objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving service group objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved groups if not quiet_mode - if service_group_objects and not quiet_mode: - group_table = [ - [ - group.name, - group.folder, - ", ".join(group.members), - ", ".join(group.tag) if group.tag else "", - ] - for group in service_group_objects - ] + # Retrieve service group objects from destination + try: + destination_service_groups = ServiceGroup(destination_client, max_limit=5000) + destination_objects = destination_service_groups.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} service group objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving service group objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + group_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + group_table.append([result["name"], status]) + typer.echo( tabulate( group_table, - headers=["Name", "Folder", "Members", "Tags"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not service_group_objects: - typer.echo("No service group objects found in the source folder.") # Prompt if not auto-approved and objects exist - if service_group_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -246,31 +286,31 @@ def service_groups( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create service group objects in destination destination_service_groups = ServiceGroup(destination_client, max_limit=5000) created_objs: List[ServiceGroupResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in service_group_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of service group object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Service Group,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -287,28 +327,24 @@ def service_groups( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following service group objects:") - created_table = [ - [ - obj.name, - obj.folder, - ", ".join(obj.members), - ", ".join(obj.tag) if obj.tag else "", - ] - for obj in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) + typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Members", "Tags"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/objects/tag.py b/scm_config_clone/commands/objects/tag.py index 772aaf8..7658dde 100644 --- a/scm_config_clone/commands/objects/tag.py +++ b/scm_config_clone/commands/objects/tag.py @@ -1,3 +1,5 @@ +# scm_config_clone/commands/objects/tag.py + import logging from typing import List, Optional, Any, Dict @@ -14,7 +16,11 @@ from scm.models.objects.tag import TagCreateModel, TagResponseModel from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params(src_obj: TagResponseModel, folder: str) -> Dict[str, Any]: @@ -174,7 +180,7 @@ def tags( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -191,10 +197,29 @@ def tags( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve tag objects from source try: source_tags = Tag(source_client, max_limit=5000) - tag_objects = source_tags.list( + source_objects = source_tags.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -202,36 +227,52 @@ def tags( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(tag_objects)} tag objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} tag objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving tag objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved tags if not quiet_mode - if tag_objects and not quiet_mode: - tag_table = [ - [ - t.name, - t.folder, - t.comments or "", - t.snippet or "", - t.device or "", - ] - for t in tag_objects - ] + # Retrieve tag objects from destination + try: + destination_tags = Tag(destination_client, max_limit=5000) + destination_objects = destination_tags.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} tag objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving tag objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + tag_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + tag_table.append([result["name"], status]) + typer.echo( tabulate( tag_table, - headers=["Name", "Folder", "Description", "Snippet", "Device"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not tag_objects: - typer.echo("No tag objects found in the source folder.") # Prompt if not auto-approved and objects exist - if tag_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -239,38 +280,37 @@ def tags( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create tag objects in destination destination_tags = Tag(destination_client, max_limit=5000) created_objs: List[TagResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in tag_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of tag object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Tag,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: error_objects.append([src_obj.name, str(ve)]) continue - # If dry_run is True, we might skip actual creation in the future. try: new_obj = destination_tags.create(create_params) created_objs.append(new_obj) @@ -281,30 +321,24 @@ def tags( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following tag objects:") - created_table = [ - [ - obj.name, - obj.folder, - obj.comments or "", - obj.snippet or "", - obj.device or "", - ] - for obj in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Description", "Snippet", "Device"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/anti_spyware_profile.py b/scm_config_clone/commands/security/anti_spyware_profile.py index a129fdd..de92420 100644 --- a/scm_config_clone/commands/security/anti_spyware_profile.py +++ b/scm_config_clone/commands/security/anti_spyware_profile.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -195,7 +199,7 @@ def anti_spyware_profiles( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -212,10 +216,29 @@ def anti_spyware_profiles( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve anti-spyware profile objects from source try: source_profiles = AntiSpywareProfile(source_client, max_limit=5000) - profile_objects = source_profiles.list( + source_objects = source_profiles.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -223,36 +246,54 @@ def anti_spyware_profiles( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(profile_objects)} anti-spyware profile objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} anti-spyware profile objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving anti-spyware profile objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved profiles if not quiet_mode - if profile_objects and not quiet_mode: - profile_table = [ - [ - profile.name, - profile.folder, - len(profile.rules), - "Yes" if profile.cloud_inline_analysis else "No", - profile.description or "", - ] - for profile in profile_objects - ] + # Retrieve anti-spyware profile objects from destination + try: + destination_profiles = AntiSpywareProfile(destination_client, max_limit=5000) + destination_objects = destination_profiles.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} anti-spyware profile objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving anti-spyware profile objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + profile_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + profile_table.append([result["name"], status]) + typer.echo( tabulate( profile_table, - headers=["Name", "Folder", "Rules", "Cloud Analysis", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not profile_objects: - typer.echo("No anti-spyware profile objects found in the source folder.") # Prompt if not auto-approved and objects exist - if profile_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -260,31 +301,31 @@ def anti_spyware_profiles( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create anti-spyware profile objects in destination destination_profiles = AntiSpywareProfile(destination_client, max_limit=5000) created_objs: List[AntiSpywareProfileResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in profile_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of anti-spyware profile object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Anti-Spyware Profile,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -303,30 +344,24 @@ def anti_spyware_profiles( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following anti-spyware profile objects:") - created_table = [ - [ - obj.name, - obj.folder, - len(obj.rules), - "Yes" if obj.cloud_inline_analysis else "No", - obj.description or "", - ] - for obj in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Rules", "Cloud Analysis", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/decryption_profile.py b/scm_config_clone/commands/security/decryption_profile.py index f71fddf..a511a83 100644 --- a/scm_config_clone/commands/security/decryption_profile.py +++ b/scm_config_clone/commands/security/decryption_profile.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -198,7 +202,7 @@ def decryption_profiles( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -215,10 +219,29 @@ def decryption_profiles( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve decryption profile objects from source try: source_profiles = DecryptionProfile(source_client, max_limit=5000) - profile_objects = source_profiles.list( + source_objects = source_profiles.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -226,55 +249,54 @@ def decryption_profiles( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(profile_objects)} decryption profile objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} decryption profile objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving decryption profile objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved profiles if not quiet_mode - if profile_objects and not quiet_mode: + # Retrieve decryption profile objects from destination + try: + destination_profiles = DecryptionProfile(destination_client, max_limit=5000) + destination_objects = destination_profiles.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} decryption profile objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving decryption profile objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: profile_table = [] - for profile in profile_objects: - if profile.ssl_forward_proxy: - profile_type = "Forward Proxy" - elif profile.ssl_inbound_proxy: - profile_type = "Inbound Proxy" - elif profile.ssl_no_proxy: - profile_type = "No Proxy" - else: - profile_type = "Unknown" - - ssl_settings = [] - if profile.ssl_protocol_settings: - ssl_settings.extend( - [ - f"Min: {profile.ssl_protocol_settings.min_version}", - f"Max: {profile.ssl_protocol_settings.max_version}", - ] - ) - - profile_table.append( - [ - profile.name, - profile.folder, - profile_type, - ", ".join(ssl_settings) if ssl_settings else "Default Settings", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + profile_table.append([result["name"], status]) typer.echo( tabulate( profile_table, - headers=["Name", "Folder", "Type", "SSL Settings"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not profile_objects: - typer.echo("No decryption profile objects found in the source folder.") # Prompt if not auto-approved and objects exist - if profile_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -282,31 +304,31 @@ def decryption_profiles( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create decryption profile objects in destination destination_profiles = DecryptionProfile(destination_client, max_limit=5000) created_objs: List[DecryptionProfileResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in profile_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of decryption profile object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Decryption Profile,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -325,10 +347,11 @@ def decryption_profiles( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -336,37 +359,12 @@ def decryption_profiles( typer.echo("\nSuccessfully created the following decryption profile objects:") created_table = [] for obj in created_objs: - if obj.ssl_forward_proxy: - profile_type = "Forward Proxy" - elif obj.ssl_inbound_proxy: - profile_type = "Inbound Proxy" - elif obj.ssl_no_proxy: - profile_type = "No Proxy" - else: - profile_type = "Unknown" - - ssl_settings = [] - if obj.ssl_protocol_settings: - ssl_settings.extend( - [ - f"Min: {obj.ssl_protocol_settings.min_version}", - f"Max: {obj.ssl_protocol_settings.max_version}", - ] - ) - - created_table.append( - [ - obj.name, - obj.folder, - profile_type, - ", ".join(ssl_settings) if ssl_settings else "Default Settings", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Type", "SSL Settings"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/dns_security_profile.py b/scm_config_clone/commands/security/dns_security_profile.py index 69ce5db..3bb1fad 100644 --- a/scm_config_clone/commands/security/dns_security_profile.py +++ b/scm_config_clone/commands/security/dns_security_profile.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -184,7 +188,7 @@ def dns_security_profiles( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -201,10 +205,29 @@ def dns_security_profiles( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve DNS security profile objects from source try: source_profiles = DNSSecurityProfile(source_client, max_limit=5000) - profile_objects = source_profiles.list( + source_objects = source_profiles.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -212,47 +235,54 @@ def dns_security_profiles( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(profile_objects)} DNS security profile objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} DNS security profile objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving DNS security profile objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved profiles if not quiet_mode - if profile_objects and not quiet_mode: + # Retrieve DNS security profile objects from destination + try: + destination_profiles = DNSSecurityProfile(destination_client, max_limit=5000) + destination_objects = destination_profiles.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} DNS security profile objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving DNS security profile objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: profile_table = [] - for profile in profile_objects: - categories = [] - if ( - profile.botnet_domains - and profile.botnet_domains.dns_security_categories - ): - categories = [ - f"{cat.name}({cat.action})" - for cat in profile.botnet_domains.dns_security_categories - ] - - profile_table.append( - [ - profile.name, - profile.folder, - ", ".join(categories) or "No categories", - profile.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + profile_table.append([result["name"], status]) typer.echo( tabulate( profile_table, - headers=["Name", "Folder", "Categories", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not profile_objects: - typer.echo("No DNS security profile objects found in the source folder.") # Prompt if not auto-approved and objects exist - if profile_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -260,31 +290,31 @@ def dns_security_profiles( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create DNS security profile objects in destination destination_profiles = DNSSecurityProfile(destination_client, max_limit=5000) created_objs: List[DNSSecurityProfileResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in profile_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of DNS security profile object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"DNS Security Profile,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -303,10 +333,11 @@ def dns_security_profiles( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -314,26 +345,12 @@ def dns_security_profiles( typer.echo("\nSuccessfully created the following DNS security profile objects:") created_table = [] for obj in created_objs: - categories = [] - if obj.botnet_domains and obj.botnet_domains.dns_security_categories: - categories = [ - f"{cat.name}({cat.action})" - for cat in obj.botnet_domains.dns_security_categories - ] - - created_table.append( - [ - obj.name, - obj.folder, - ", ".join(categories) or "No categories", - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Categories", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/security_rule.py b/scm_config_clone/commands/security/security_rule.py index a98af62..a6ae9e0 100644 --- a/scm_config_clone/commands/security/security_rule.py +++ b/scm_config_clone/commands/security/security_rule.py @@ -16,7 +16,11 @@ from scm.models.security import SecurityRuleCreateModel, SecurityRuleResponseModel from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -205,7 +209,7 @@ def security_rules( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -222,10 +226,29 @@ def security_rules( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve security rules from source try: source_rules = SecurityRule(source_client, max_limit=5000) - rule_objects = source_rules.list( + source_objects = source_rules.list( folder=folder, rulebase=rulebase, exact_match=True, @@ -234,46 +257,53 @@ def security_rules( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(rule_objects)} security rules from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} security rules from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving security rules from source: {e}") raise typer.Exit(code=1) - # Display retrieved rules if not quiet_mode - if rule_objects and not quiet_mode: - rule_table = [ - [ - rule.name, - rule.folder, - rule.action, - ", ".join(rule.from_), - ", ".join(rule.to_), - ", ".join(rule.application), - rule.description or "", - ] - for rule in rule_objects - ] + # Retrieve security rules from destination + try: + destination_rules = SecurityRule(destination_client, max_limit=5000) + destination_objects = destination_rules.list( + folder=folder, + rulebase=rulebase, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} security rules from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving security rules from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: + rule_table = [] + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + rule_table.append([result["name"], status]) + typer.echo( tabulate( rule_table, - headers=[ - "Name", - "Folder", - "Action", - "From", - "To", - "Applications", - "Description", - ], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not rule_objects: - typer.echo("No security rules found in the source folder.") # Prompt if not auto-approved and objects exist - if rule_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these rules in the destination tenant?" ) @@ -281,31 +311,31 @@ def security_rules( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create security rules in destination destination_rules = SecurityRule(destination_client, max_limit=5000) created_objs: List[SecurityRuleResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in rule_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of security rule in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"Security Rule,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -322,39 +352,24 @@ def security_rules( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode if created_objs and not quiet_mode: typer.echo("\nSuccessfully created the following security rules:") - created_table = [ - [ - rule.name, - rule.folder, - rule.action, - ", ".join(rule.from_), - ", ".join(rule.to_), - ", ".join(rule.application), - rule.description or "", - ] - for rule in created_objs - ] + created_table = [] + for obj in created_objs: + created_table.append([obj.name]) + typer.echo( tabulate( created_table, - headers=[ - "Name", - "Folder", - "Action", - "From", - "To", - "Applications", - "Description", - ], + headers=["Name"], tablefmt="fancy_grid", ) ) @@ -364,7 +379,7 @@ def security_rules( typer.echo( tabulate( error_objects, - headers=["Rule Name", "Error"], + headers=["Object Name", "Error"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/url_category.py b/scm_config_clone/commands/security/url_category.py index 2571929..e681802 100644 --- a/scm_config_clone/commands/security/url_category.py +++ b/scm_config_clone/commands/security/url_category.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -183,7 +187,7 @@ def url_categories( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -200,10 +204,29 @@ def url_categories( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve URL category objects from source try: source_url_categories = URLCategories(source_client, max_limit=5000) - url_category_objects = source_url_categories.list( + source_objects = source_url_categories.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -211,38 +234,52 @@ def url_categories( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(url_category_objects)} URL category objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} URL category objects from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving URL category objects from source: {e}") raise typer.Exit(code=1) - # Display retrieved categories if not quiet_mode - if url_category_objects and not quiet_mode: + # Retrieve URL category objects from destination + try: + destination_url_categories = URLCategories(destination_client, max_limit=5000) + destination_objects = destination_url_categories.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} URL category objects from source destination folder '{folder}'." + ) + except Exception as e: + logger.error(f"Error retrieving URL category objects from destination: {e}") + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: category_table = [] - for category in url_category_objects: - category_table.append( - [ - category.name, - category.folder, - category.type, - ", ".join(category.list), - category.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + category_table.append([result["name"], status]) typer.echo( tabulate( category_table, - headers=["Name", "Folder", "Type", "URLs/Categories", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not url_category_objects: - typer.echo("No URL category objects found in the source folder.") # Prompt if not auto-approved and objects exist - if url_category_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -250,31 +287,31 @@ def url_categories( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create URL category objects in destination destination_url_categories = URLCategories(destination_client, max_limit=5000) created_objs: List[URLCategoriesResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in url_category_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of URL category object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"URL Category,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -291,10 +328,12 @@ def url_categories( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + # Use the exception's class name as the error message + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -305,17 +344,13 @@ def url_categories( created_table.append( [ obj.name, - obj.folder, - obj.type, - ", ".join(obj.list), - obj.description or "", ] ) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Type", "URLs/Categories", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/vulnerability_protection_profile.py b/scm_config_clone/commands/security/vulnerability_protection_profile.py index 5b0a999..3f96227 100644 --- a/scm_config_clone/commands/security/vulnerability_protection_profile.py +++ b/scm_config_clone/commands/security/vulnerability_protection_profile.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -185,7 +189,7 @@ def vulnerability_protection_profiles( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -202,10 +206,29 @@ def vulnerability_protection_profiles( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve vulnerability protection profile objects from source try: source_profiles = VulnerabilityProtectionProfile(source_client, max_limit=5000) - profile_objects = source_profiles.list( + source_objects = source_profiles.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -213,7 +236,7 @@ def vulnerability_protection_profiles( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(profile_objects)} vulnerability protection profile objects from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} vulnerability protection profile objects from source tenant folder '{folder}'." ) except Exception as e: logger.error( @@ -221,34 +244,50 @@ def vulnerability_protection_profiles( ) raise typer.Exit(code=1) - # Display retrieved profiles if not quiet_mode - if profile_objects and not quiet_mode: + # Retrieve vulnerability protection profile objects from destination + try: + destination_profiles = VulnerabilityProtectionProfile( + destination_client, max_limit=5000 + ) + destination_objects = destination_profiles.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} vulnerability protection profile objects from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving vulnerability protection profile objects from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: profile_table = [] - for profile in profile_objects: - profile_table.append( - [ - profile.name, - profile.folder, - len(profile.rules), - len(profile.threat_exception) if profile.threat_exception else 0, - profile.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + profile_table.append([result["name"], status]) typer.echo( tabulate( profile_table, - headers=["Name", "Folder", "Rules", "Exceptions", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not profile_objects: - typer.echo( - "No vulnerability protection profile objects found in the source folder." - ) # Prompt if not auto-approved and objects exist - if profile_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -256,24 +295,14 @@ def vulnerability_protection_profiles( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create vulnerability protection profile objects in destination destination_profiles = VulnerabilityProtectionProfile( @@ -282,7 +311,19 @@ def vulnerability_protection_profiles( created_objs: List[VulnerabilityProfileResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in profile_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of vulnerability protection profile object in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write( + f"Vulnerability Protection Profile,{src_obj.name},{src_obj.folder}\n" + ) + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -301,10 +342,11 @@ def vulnerability_protection_profiles( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -314,20 +356,12 @@ def vulnerability_protection_profiles( ) created_table = [] for obj in created_objs: - created_table.append( - [ - obj.name, - obj.folder, - len(obj.rules), - len(obj.threat_exception) if obj.threat_exception else 0, - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Rules", "Exceptions", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/commands/security/wildfire_antivirus_profile.py b/scm_config_clone/commands/security/wildfire_antivirus_profile.py index 663442b..b139afe 100644 --- a/scm_config_clone/commands/security/wildfire_antivirus_profile.py +++ b/scm_config_clone/commands/security/wildfire_antivirus_profile.py @@ -19,7 +19,11 @@ ) from tabulate import tabulate -from scm_config_clone.utilities import load_settings, parse_csv_option +from scm_config_clone.utilities import ( + compare_object_lists, + load_settings, + parse_csv_option, +) def build_create_params( @@ -191,7 +195,7 @@ def wildfire_antivirus_profiles( exclude_snippets_list = parse_csv_option(exclude_snippets) exclude_devices_list = parse_csv_option(exclude_devices) - # Authenticate and retrieve from source + # Authenticate with source try: source_creds = settings["source_scm"] source_client = Scm( @@ -208,10 +212,29 @@ def wildfire_antivirus_profiles( logger.error(f"Unexpected error with source authentication: {e}") raise typer.Exit(code=1) + # Authenticate with destination + try: + destination_creds = settings["destination_scm"] + destination_client = Scm( + client_id=destination_creds["client_id"], + client_secret=destination_creds["client_secret"], + tsg_id=destination_creds["tenant"], + log_level=logging_level, + ) + logger.info( + f"Authenticated with destination SCM tenant: {destination_creds['tenant']}" + ) + except (AuthenticationError, KeyError) as e: + logger.error(f"Error authenticating with destination tenant: {e}") + raise typer.Exit(code=1) + except Exception as e: + logger.error(f"Unexpected error with destination authentication: {e}") + raise typer.Exit(code=1) + # Retrieve profiles from source try: source_profiles = WildfireAntivirusProfile(source_client, max_limit=5000) - profile_objects = source_profiles.list( + source_objects = source_profiles.list( folder=folder, exact_match=True, exclude_folders=exclude_folders_list, @@ -219,38 +242,56 @@ def wildfire_antivirus_profiles( exclude_devices=exclude_devices_list, ) logger.info( - f"Retrieved {len(profile_objects)} WildFire antivirus profiles from source tenant folder '{folder}'." + f"Retrieved {len(source_objects)} WildFire antivirus profiles from source tenant folder '{folder}'." ) except Exception as e: logger.error(f"Error retrieving WildFire antivirus profiles from source: {e}") raise typer.Exit(code=1) - # Display retrieved profiles if not quiet_mode - if profile_objects and not quiet_mode: + # Retrieve profiles from destination + try: + destination_profiles = WildfireAntivirusProfile( + destination_client, max_limit=5000 + ) + destination_objects = destination_profiles.list( + folder=folder, + exact_match=True, + exclude_folders=exclude_folders_list, + exclude_snippets=exclude_snippets_list, + exclude_devices=exclude_devices_list, + ) + logger.info( + f"Retrieved {len(destination_objects)} WildFire antivirus profiles from destination tenant folder '{folder}'." + ) + except Exception as e: + logger.error( + f"Error retrieving WildFire antivirus profiles from destination: {e}" + ) + raise typer.Exit(code=1) + + # Compare and get the status information + comparison_results = compare_object_lists( + source_objects, + destination_objects, + ) + + if source_objects and not quiet_mode: profile_table = [] - for profile in profile_objects: - profile_table.append( - [ - profile.name, - profile.folder, - len(profile.rules), - "Yes" if profile.packet_capture else "No", - profile.description or "", - ] - ) + for result in comparison_results: + # 'x' if already configured else '' + status = "x" if result["already_configured"] else "" + profile_table.append([result["name"], status]) typer.echo( tabulate( profile_table, - headers=["Name", "Folder", "Rules", "Packet Capture", "Description"], + headers=["Name", "Destination Status"], tablefmt="fancy_grid", ) ) - elif not profile_objects: - typer.echo("No WildFire antivirus profiles found in the source folder.") # Prompt if not auto-approved and objects exist - if profile_objects and not auto_approve: + if source_objects and not auto_approve: proceed = typer.confirm( "Do you want to proceed with creating these objects in the destination tenant?" ) @@ -258,31 +299,31 @@ def wildfire_antivirus_profiles( typer.echo("Aborting cloning operation.") raise typer.Exit(code=0) - # Authenticate with destination tenant - try: - dest_creds = settings["destination_scm"] - destination_client = Scm( - client_id=dest_creds["client_id"], - client_secret=dest_creds["client_secret"], - tsg_id=dest_creds["tenant"], - log_level=logging_level, - ) - logger.info( - f"Authenticated with destination SCM tenant: {dest_creds['tenant']}" - ) - except (AuthenticationError, KeyError) as e: - logger.error(f"Error authenticating with destination tenant: {e}") - raise typer.Exit(code=1) - except Exception as e: - logger.error(f"Unexpected error with destination authentication: {e}") - raise typer.Exit(code=1) + # Determine which objects need to be created (those not already configured) + already_configured_names = { + res["name"] for res in comparison_results if res["already_configured"] + } + + objects_to_create = [ + obj for obj in source_objects if obj.name not in already_configured_names + ] # Create profiles in destination destination_profiles = WildfireAntivirusProfile(destination_client, max_limit=5000) created_objs: List[WildfireAvProfileResponseModel] = [] error_objects: List[List[str]] = [] - for src_obj in profile_objects: + for src_obj in objects_to_create: + if dry_run: + logger.info( + f"Skipping creation of WildFire antivirus profile in destination (dry run): {src_obj.name}" + ) + continue + + if create_report: + with open("result.csv", "a") as f: + f.write(f"WildFire Antivirus Profile,{src_obj.name},{src_obj.folder}\n") + try: create_params = build_create_params(src_obj, folder) except ValueError as ve: @@ -301,10 +342,11 @@ def wildfire_antivirus_profiles( NameNotUniqueError, ObjectNotPresentError, ) as e: - error_objects.append([src_obj.name, str(e)]) + error_type = type(e).__name__ + error_objects.append([src_obj.name, error_type]) continue - except Exception as e: - error_objects.append([src_obj.name, str(e)]) + except Exception: # noqa + error_objects.append([src_obj.name, "unknown error"]) continue # Display results if not quiet_mode @@ -312,20 +354,12 @@ def wildfire_antivirus_profiles( typer.echo("\nSuccessfully created the following WildFire antivirus profiles:") created_table = [] for obj in created_objs: - created_table.append( - [ - obj.name, - obj.folder, - len(obj.rules), - "Yes" if obj.packet_capture else "No", - obj.description or "", - ] - ) + created_table.append([obj.name]) typer.echo( tabulate( created_table, - headers=["Name", "Folder", "Rules", "Packet Capture", "Description"], + headers=["Name"], tablefmt="fancy_grid", ) ) diff --git a/scm_config_clone/main.py b/scm_config_clone/main.py index cf39342..4ad2257 100644 --- a/scm_config_clone/main.py +++ b/scm_config_clone/main.py @@ -29,8 +29,10 @@ decryption_profiles, dns_security_profiles, external_dynamic_lists, + hip_objects, security_rules, services, + service_groups, tags, url_categories, vulnerability_protection_profiles, @@ -97,6 +99,12 @@ help="Clone external dynamic lists.", )(external_dynamic_lists) +# HIP Objects +app.command( + name="hip-objects", + help="Clone hip objects.", +)(hip_objects) + # Services app.command( name="services", @@ -106,8 +114,8 @@ # Service Groups app.command( name="service-groups", - help="Clone service groupss.", -)(services) + help="Clone service groups.", +)(service_groups) # Tags app.command( diff --git a/scm_config_clone/utilities/__init__.py b/scm_config_clone/utilities/__init__.py index ccc297e..7e1421e 100644 --- a/scm_config_clone/utilities/__init__.py +++ b/scm_config_clone/utilities/__init__.py @@ -1,4 +1,5 @@ # scm_config_clone/utilities/__init__.py +from .compare_object_lists import compare_object_lists from .parse_csv import parse_csv_option from .settings import load_settings diff --git a/scm_config_clone/utilities/compare_object_lists.py b/scm_config_clone/utilities/compare_object_lists.py new file mode 100644 index 0000000..8795f2d --- /dev/null +++ b/scm_config_clone/utilities/compare_object_lists.py @@ -0,0 +1,30 @@ +def compare_object_lists(source_objects: list, destination_objects: list) -> list: + """ + Compare two lists of objects (source and destination) and determine + which source objects are already configured on the destination. + + Args: + source_objects: A list of objects from the source. + Each object should have a 'name' attribute. + destination_objects: A list of objects from the destination. + Each object should have a 'name' attribute. + + Returns: + A list of dictionaries. Each dictionary has: + "name": the object's name (string) + "already_configured": boolean indicating if the object + exists in the destination. + """ + # Create a set of names from the destination to allow O(1) lookups + destination_names = {obj.name for obj in destination_objects} + + results = [] + for src_obj in source_objects: + results.append( + { + "name": src_obj.name, + "already_configured": src_obj.name in destination_names, + } + ) + + return results