├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── ciscodnacbackupctl ├── __init__.py ├── cli.py ├── config.py ├── debug.py └── format.py ├── requirements.txt ├── setup.py └── tests ├── main.py └── mockup_data ├── create.json ├── delete.json ├── history.json ├── list.json ├── progress.json └── purge.json /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | ciscodnacbackupctl/__pycache__/* 3 | ciscodnacbackupctl.egg-info/* 4 | dist/* 5 | build/* -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as maintainers of this Cisco Sample Code pledge to making participation with our project a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Showing empathy towards other people 15 | 16 | Examples of unacceptable behavior include: 17 | 18 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 19 | * Trolling, insulting/derogatory comments, and personal or political attacks 20 | * Public or private harassment 21 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 22 | * Other conduct which could reasonably be considered inappropriate in a professional setting 23 | 24 | ## Our Responsibilities 25 | 26 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 27 | 28 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other interactions with this project that are not aligned to this Code of Conduct, or to ban temporarily or permanently any person for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 29 | 30 | ## Scope 31 | 32 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project. Examples of representing a project include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 33 | 34 | ## Enforcement 35 | 36 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the Cisco SE GitHub team at ciscose-github@cisco.com. The team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 37 | 38 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project or Cisco SE Leadership. 39 | 40 | ## Attribution 41 | 42 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 43 | 44 | [homepage]: http://contributor-covenant.org 45 | [version]: http://contributor-covenant.org/version/1/4/ 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Cisco Sample Code 2 | 3 | This project, and the code contained herein, is provided for example or demonstration purposes by Cisco for use by our partners and customers in working with Cisco's products and services. While Cisco's customers and partners are free to use this code according to the terms of the [LICENSE](./LICENSE) associated with this project, this is not an *Open Source* project in as much as we are not seeking to build a community around the project and its capabilities. 4 | 5 | We do desire to provide functional and high-quality examples and demonstrations. If you should discover some bug, issue, or opportunity for enhancement with the sample code contained in this project, please notify us by creating a new **Issue**. 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | WORKDIR app/ 3 | COPY ./ ./ 4 | RUN pip install -r requirements.txt 5 | RUN pip install -e . 6 | ENTRYPOINT ["ciscodnacbackupctl"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | CISCO SAMPLE CODE LICENSE 2 | Version 1.1 3 | Copyright (c) 2018 Cisco and/or its affiliates 4 | 5 | These terms govern this Cisco Systems, Inc. ("Cisco"), example or demo 6 | source code and its associated documentation (together, the "Sample 7 | Code"). By downloading, copying, modifying, compiling, or redistributing 8 | the Sample Code, you accept and agree to be bound by the following terms 9 | and conditions (the "License"). If you are accepting the License on 10 | behalf of an entity, you represent that you have the authority to do so 11 | (either you or the entity, "you"). Sample Code is not supported by Cisco 12 | TAC and is not tested for quality or performance. This is your only 13 | license to the Sample Code and all rights not expressly granted are 14 | reserved. 15 | 16 | 1. LICENSE GRANT: Subject to the terms and conditions of this License, 17 | Cisco hereby grants to you a perpetual, worldwide, non-exclusive, non- 18 | transferable, non-sublicensable, royalty-free license to copy and 19 | modify the Sample Code in source code form, and compile and 20 | redistribute the Sample Code in binary/object code or other executable 21 | forms, in whole or in part, solely for use with Cisco products and 22 | services. For interpreted languages like Java and Python, the 23 | executable form of the software may include source code and 24 | compilation is not required. 25 | 26 | 2. CONDITIONS: You shall not use the Sample Code independent of, or to 27 | replicate or compete with, a Cisco product or service. Cisco products 28 | and services are licensed under their own separate terms and you shall 29 | not use the Sample Code in any way that violates or is inconsistent 30 | with those terms (for more information, please visit: 31 | www.cisco.com/go/terms). 32 | 33 | 3. OWNERSHIP: Cisco retains sole and exclusive ownership of the Sample 34 | Code, including all intellectual property rights therein, except with 35 | respect to any third-party material that may be used in or by the 36 | Sample Code. Any such third-party material is licensed under its own 37 | separate terms (such as an open source license) and all use must be in 38 | full accordance with the applicable license. This License does not 39 | grant you permission to use any trade names, trademarks, service 40 | marks, or product names of Cisco. If you provide any feedback to Cisco 41 | regarding the Sample Code, you agree that Cisco, its partners, and its 42 | customers shall be free to use and incorporate such feedback into the 43 | Sample Code, and Cisco products and services, for any purpose, and 44 | without restriction, payment, or additional consideration of any kind. 45 | If you initiate or participate in any litigation against Cisco, its 46 | partners, or its customers (including cross-claims and counter-claims) 47 | alleging that the Sample Code and/or its use infringe any patent, 48 | copyright, or other intellectual property right, then all rights 49 | granted to you under this License shall terminate immediately without 50 | notice. 51 | 52 | 4. LIMITATION OF LIABILITY: CISCO SHALL HAVE NO LIABILITY IN CONNECTION 53 | WITH OR RELATING TO THIS LICENSE OR USE OF THE SAMPLE CODE, FOR 54 | DAMAGES OF ANY KIND, INCLUDING BUT NOT LIMITED TO DIRECT, INCIDENTAL, 55 | AND CONSEQUENTIAL DAMAGES, OR FOR ANY LOSS OF USE, DATA, INFORMATION, 56 | PROFITS, BUSINESS, OR GOODWILL, HOWEVER CAUSED, EVEN IF ADVISED OF THE 57 | POSSIBILITY OF SUCH DAMAGES. 58 | 59 | 5. DISCLAIMER OF WARRANTY: SAMPLE CODE IS INTENDED FOR EXAMPLE PURPOSES 60 | ONLY AND IS PROVIDED BY CISCO "AS IS" WITH ALL FAULTS AND WITHOUT 61 | WARRANTY OR SUPPORT OF ANY KIND. TO THE MAXIMUM EXTENT PERMITTED BY 62 | LAW, ALL EXPRESS AND IMPLIED CONDITIONS, REPRESENTATIONS, AND 63 | WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OR 64 | CONDITION OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON- 65 | INFRINGEMENT, SATISFACTORY QUALITY, NON-INTERFERENCE, AND ACCURACY, 66 | ARE HEREBY EXCLUDED AND EXPRESSLY DISCLAIMED BY CISCO. CISCO DOES NOT 67 | WARRANT THAT THE SAMPLE CODE IS SUITABLE FOR PRODUCTION OR COMMERCIAL 68 | USE, WILL OPERATE PROPERLY, IS ACCURATE OR COMPLETE, OR IS WITHOUT 69 | ERROR OR DEFECT. 70 | 71 | 6. GENERAL: This License shall be governed by and interpreted in 72 | accordance with the laws of the State of California, excluding its 73 | conflict of laws provisions. You agree to comply with all applicable 74 | United States export laws, rules, and regulations. If any provision of 75 | this License is judged illegal, invalid, or otherwise unenforceable, 76 | that provision shall be severed and the rest of the License shall 77 | remain in full force and effect. No failure by Cisco to enforce any of 78 | its rights related to the Sample Code or to a breach of this License 79 | in a particular situation will act as a waiver of such rights. In the 80 | event of any inconsistencies with any other terms, this License shall 81 | take precedence. 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ciscodnacbackupctl 2 | ![PyPI](https://img.shields.io/pypi/v/ciscodnacbackupctl) 3 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/ciscodnacbackupctl) 4 | ![GitHub last commit](https://img.shields.io/github/last-commit/cskoglun/ciscodnacbackupctl) 5 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/cskoglun/ciscodnacbackupctl) 6 | ![GitHub issues](https://img.shields.io/github/issues/cskoglun/ciscodnacbackupctl) 7 | ![GitHub closed issues](https://img.shields.io/github/issues-closed-raw/cskoglun/ciscodnacbackupctl) 8 | ![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/cskoglun/ciscodnacbackupctl) 9 | ![GitHub Repo stars](https://img.shields.io/github/stars/cskoglun/ciscodnacbackupctl?style=social) 10 | 11 | Cisco DNA Center Backup Tool (as a CLI tool) 12 | _Helps you to manage your backups and purge previous backups and also incompatible backups (between versions)_ 13 | 14 | --- 15 | 16 | ## Why? 17 | 18 | In the current state of Cisco DNA Center, it's possible to schedule and perform backups. 19 | But there's no automated way of purging backups and users are asked to purge backups through the Web UI 20 | ```ciscodnacbackupctl``` offers a CLI interface to handle your backups from your terminal or as a daemon/docker container 21 | 22 | ## Features 23 | - [x] List all backups 24 | - [x] History/On-going backups 25 | - [x] Delete backup 26 | - [x] Purge backups (all or just incompatible ones) 27 | - [x] Schedule backups 28 | - [x] New Backup 29 | - [x] Purge 30 | - [x] Debug HTTP 31 | 32 | ### Unsupported features 33 | - Assurance backups (NFS) 34 | - _"The Assurance data consists of network assurance and analytics data. The first backup of Assurance data is a full backup. After that, backups are incremental."_ 35 | - [Details](https://www.cisco.com/c/en/us/td/docs/cloud-systems-management/network-automation-and-management/dna-center/2-3-3/admin_guide/b_cisco_dna_center_admin_guide_2_3_3/b_cisco_dna_center_admin_guide_2_3_3_chapter_0110.html) 36 | 37 | ## Installation 38 | 39 | ```pip install ciscodnacbackupctl``` 40 | 41 | ### Config 42 | ``` 43 | ciscodnacbackupctl config --help 44 | Usage: ciscodnacbackupctl config [OPTIONS] 45 | 46 | Options: 47 | --env / --no-env 48 | --hostname TEXT [required] 49 | --username TEXT [required] 50 | --password TEXT [required] 51 | --secure Secure HTTPS towards Cisco DNA Center 52 | --encode Encode Cisco DNA Center config to Base64 string 53 | --overwrite Writes over the existing config for Cisco DNA Center 54 | --help Show this message and exit. 55 | ``` 56 | 57 | ``` 58 | Usage: ciscodnacbackupctl [OPTIONS] COMMAND [ARGS]... 59 | 60 | Options: 61 | --debug / --no-debug 62 | --version Show the version and exit. 63 | --help Show this message and exit. 64 | 65 | Commands: 66 | config 67 | create 68 | daemon ['start', 'stop', 'restart', 'status'] 69 | delete 70 | history 71 | list 72 | progress 73 | purge 74 | schedule_backup 75 | schedule_purge 76 | whoami 77 | ``` 78 | 79 | ## Technologies & Frameworks Used 80 | 81 | **Cisco Products & Services:** 82 | 83 | - Cisco DNA Center 84 | 85 | **Tools & Frameworks:** 86 | 87 | - rich 88 | - tabulate 89 | - hurry 90 | - click 91 | - schedule 92 | - daemonocle 93 | 94 | ## Daemon or Docker 95 | This makes it possible to automatically purge old backups daily 96 | 97 | ### Daemon 98 | ```ciscodnacbackupctl daemon start --keep 3``` 99 | 100 | ### Docker Support 101 | Generate Cisco DNA Center config as Base64 string 102 | ```docker run -it --rm robertcsapo/ciscodnacbackupctl config --env --hostname --username --password --encode``` 103 | 104 | Use the ENV to exucute commands 105 | 106 | List 107 | ``` 108 | docker run -it --rm \ 109 | -e DNAC_CONFIG=ewogICAgImRuYWMiOiB7CiAgICAgICAgImhvc3RuYW1lIjogInNhbXBsZS5ob3N0LnRsZCIsCiAgICAgICAgInVzZXJuYW1lIjogImRuYWMiLAogICAgICAgICJwYXNzd29yZCI6ICJwYXNzdzByZCIsCiAgICAgICAgInNlY3VyZSI6IGZhbHNlCiAgICB9Cn0 \ 110 | robertcsapo/ciscodnacbackupctl \ 111 | list 112 | ``` 113 | Purge 114 | ``` 115 | docker run -it --rm \ 116 | -e DNAC_CONFIG=ewogICAgImRuYWMiOiB7CiAgICAgICAgImhvc3RuYW1lIjogInNhbXBsZS5ob3N0LnRsZCIsCiAgICAgICAgInVzZXJuYW1lIjogImRuYWMiLAogICAgICAgICJwYXNzd29yZCI6ICJwYXNzdzByZCIsCiAgICAgICAgInNlY3VyZSI6IGZhbHNlCiAgICB9Cn0 \ 117 | robertcsapo/ciscodnacbackupctl \ 118 | purge 119 | ``` 120 | 121 | ## Authors & Maintainers 122 | 123 | Smart people responsible for the creation and maintenance of this project: 124 | 125 | - Christina Skoglund 126 | - Robert Csapo 127 | 128 | ## License 129 | 130 | This project is licensed to you under the terms of the [Cisco Sample 131 | Code License](./LICENSE). 132 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | python3 setup.py sdist bdist_wheel 2 | -------------------------------------------------------------------------------- /ciscodnacbackupctl/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from datetime import timezone, timedelta, datetime 4 | from collections import OrderedDict 5 | import json 6 | import requests 7 | import schedule 8 | from requests.auth import HTTPBasicAuth 9 | import urllib3 10 | from rich.console import Console 11 | import click 12 | from ciscodnacbackupctl.format import Format 13 | from ciscodnacbackupctl.config import Config 14 | from ciscodnacbackupctl.debug import Debug 15 | 16 | author = "Robert Csapo" 17 | email = "rcsapo@cisco.com" 18 | description = "Cisco DNA Center Backup CLI" 19 | repo_url = "https://github.com/cskoglun/ciscodnacbackupctl" 20 | copyright = "Copyright (c) 2020 Cisco and/or its affiliates." 21 | license = "Cisco Sample Code License, Version 1.1" 22 | version = "0.2.12" 23 | 24 | 25 | class Api: 26 | def __init__(self, config=False): 27 | """ 28 | Create client session 29 | Checking config (env/local file) 30 | """ 31 | _client = Config() 32 | if _client.config[0] is False: 33 | return 34 | """ 35 | Authenticate towards Cisco DNA Center 36 | """ 37 | if config is False: 38 | self.settings = _client.config[1] 39 | self._auth() 40 | return 41 | 42 | @classmethod 43 | def config(cls, hostname, username, password, secure, **kwargs): 44 | """ 45 | if "read" in kwargs["operation"]: 46 | return Config.read() 47 | """ 48 | if "write" in kwargs["operation"]: 49 | return Config.write(hostname, username, password, secure, **kwargs) 50 | 51 | return 52 | 53 | def _auth(self): 54 | """Cisco DNA Center Auth""" 55 | url = "https://{}/dna/system/api/v1/auth/token".format( 56 | self.settings["dnac"]["hostname"] 57 | ) 58 | logging.info("Cisco DNA Center Authentication ({})".format(url)) 59 | data = self._request(type="auth", url=url) 60 | self.settings["dnac"]["token"] = data["Token"] 61 | return 62 | 63 | def get(self, reverse=False): 64 | """ 65 | Get Cisco DNA Center Backups 66 | """ 67 | url = "https://{}{}".format( 68 | self.settings["dnac"]["hostname"], "/api/system/v1/maglev/backup" 69 | ) 70 | 71 | data = self._request(type="get", url=url) 72 | data["response"] = sorted( 73 | data["response"], key=lambda k: k["end_timestamp"], reverse=reverse 74 | ) 75 | 76 | data = self.remove_columns(data) 77 | 78 | return data 79 | 80 | def remove_columns(self, data): 81 | 82 | i = 0 83 | remove_columns = ["tenantId", "_id", "_version"] 84 | for backup in data["response"]: 85 | for k in list(backup): 86 | if k in remove_columns: 87 | del data["response"][i][k] 88 | data["response"][i] = OrderedDict(sorted(backup.items())) 89 | i += 1 90 | return data 91 | 92 | def get_history(self): 93 | url = "https://{}{}".format( 94 | self.settings["dnac"]["hostname"], "/api/system/v1/maglev/backup/history" 95 | ) 96 | data = self._request(type="get", url=url) 97 | data = self.remove_columns(data) 98 | return data 99 | 100 | def get_progress(self): 101 | url = "https://{}{}".format( 102 | self.settings["dnac"]["hostname"], "/api/system/v1/maglev/backup/progress" 103 | ) 104 | data = self._request(type="get", url=url) 105 | data = self.remove_columns(data) 106 | return data 107 | 108 | def schedule_interval(self, **kwargs): 109 | """ 110 | Function that returns the correct Schedule function based on which interval day that has been chosen 111 | """ 112 | 113 | intervals = ["weekly", "daily"] 114 | 115 | if kwargs["interval"].lower() == intervals[0]: 116 | if kwargs["day"].lower() == "monday": 117 | ret = schedule.every().monday.at(kwargs["time"]) 118 | if kwargs["day"].lower() == "tuesday": 119 | ret = schedule.every().tuesday.at(kwargs["time"]) 120 | if kwargs["day"].lower() == "wednesday": 121 | ret = schedule.every().wednesday.at(kwargs["time"]) 122 | if kwargs["day"].lower() == "thursday": 123 | ret = schedule.every().thursday.at(kwargs["time"]) 124 | if kwargs["day"].lower() == "friday": 125 | ret = schedule.every().friday.at(kwargs["time"]) 126 | if kwargs["day"].lower() == "saturday": 127 | ret = schedule.every().saturday.at(kwargs["time"]) 128 | if kwargs["day"].lower() == "sunday": 129 | ret = schedule.every().sunday.at(kwargs["time"]) 130 | elif kwargs["interval"].lower() == intervals[1]: 131 | ret = schedule.every().day.at(kwargs["time"]) 132 | else: 133 | raise Exception 134 | 135 | return ret 136 | 137 | def _request(self, **kwargs): 138 | urllib3.disable_warnings() 139 | """ 140 | HTTP Requests 141 | """ 142 | 143 | if "auth" in kwargs["type"].lower(): 144 | url = kwargs["url"] 145 | headers = { 146 | "Content-Type": "application/json", 147 | "Accept": "application/json", 148 | } 149 | response = requests.request( 150 | "POST", 151 | url, 152 | auth=HTTPBasicAuth( 153 | self.settings["dnac"]["username"], 154 | self.settings["dnac"]["password"], 155 | ), 156 | headers=headers, 157 | verify=self.settings["dnac"]["secure"], 158 | ) 159 | 160 | Debug.payload(str(response.text)) 161 | 162 | if response.ok: 163 | data = response.json() 164 | else: 165 | if response.status_code == 429: 166 | raise Exception( 167 | "Can't login to Cisco DNA Center - Too Many Requests - Please try later" 168 | ) 169 | raise Exception( 170 | "Can't login to Cisco DNA Center ({})".format(response.json()) 171 | ) 172 | return data 173 | 174 | if "get" in kwargs["type"].lower(): 175 | """ 176 | HTTP Method GET 177 | """ 178 | url = kwargs["url"] 179 | headers = { 180 | "X-Auth-Token": self.settings["dnac"]["token"], 181 | "Content-Type": "application/json", 182 | "Accept": "application/json", 183 | } 184 | response = requests.request( 185 | "GET", 186 | url, 187 | headers=headers, 188 | verify=self.settings["dnac"]["secure"], 189 | ) 190 | 191 | Debug.payload(str(response.text)) 192 | 193 | response.raise_for_status() 194 | if response.ok: 195 | data = response.json() 196 | else: 197 | if response.status_code == 404: 198 | raise Exception("Error: Not found") 199 | data = response.json() 200 | print( 201 | "Error: ({})".format(data["response"].get("error", "Not Available")) 202 | ) 203 | return data 204 | if "delete" in kwargs["type"].lower(): 205 | """ 206 | HTTP Method DELETE 207 | """ 208 | url = kwargs["url"] 209 | headers = { 210 | "X-Auth-Token": self.settings["dnac"]["token"], 211 | "Content-Type": "application/json", 212 | "Accept": "application/json", 213 | } 214 | response = requests.request( 215 | "DELETE", 216 | url, 217 | headers=headers, 218 | verify=self.settings["dnac"]["secure"], 219 | ) 220 | 221 | Debug.payload(str(response.text)) 222 | 223 | if response.ok: 224 | data = response.json() 225 | else: 226 | if response.status_code == 404: 227 | if "response" in response.json(): 228 | raise Exception( 229 | f"Error: Not found ({response.json()['response']})" 230 | ) 231 | raise Exception(f"Error: Not found ({response.text})") 232 | data = response.json() 233 | raise Exception( 234 | "Error: ({})".format(data["response"].get("error", "Not Available")) 235 | ) 236 | return data 237 | 238 | if "post" in kwargs["type"].lower(): 239 | url = kwargs["url"] 240 | headers = { 241 | "X-Auth-Token": self.settings["dnac"]["token"], 242 | "Content-Type": "application/json", 243 | "Accept": "application/json", 244 | } 245 | response = requests.request( 246 | "POST", 247 | url, 248 | headers=headers, 249 | verify=self.settings["dnac"]["secure"], 250 | data=kwargs["payload"], 251 | ) 252 | 253 | 254 | Debug.payload(str(response.text)) 255 | 256 | if response.ok: 257 | data = response.json() 258 | elif response.status_code == 409: 259 | data = response.json() 260 | else: 261 | if response.status_code == 404: 262 | if "response" in response.json(): 263 | raise Exception( 264 | f"Error: Not found ({response.json()['response']})" 265 | ) 266 | raise Exception(f"Error: Not found ({response.text})") 267 | data = response.json() 268 | raise Exception( 269 | "Error: ({})".format(data["response"].get("error", "Not Available")) 270 | ) 271 | return data 272 | 273 | class CLI: 274 | def __init__(self, debug=False): 275 | if debug: 276 | Debug() 277 | self.api = Api() 278 | 279 | 280 | def whoami(self, **kwargs): 281 | if "DNAC_CONFIG" in os.environ: 282 | self.api.settings["dnac"]["method"] = "environment (encoded)" 283 | elif "DNA_CENTER_BASE_URL" in os.environ: 284 | self.api.settings["dnac"]["method"] = "environment" 285 | else: 286 | self.api.settings["dnac"]["method"] = "file" 287 | Format.cli( 288 | style="standard", 289 | data=self.api.settings["dnac"], 290 | source="dict", 291 | ) 292 | return True 293 | 294 | def list(self, reverse): 295 | data = self.api.get(reverse=reverse) 296 | Format.cli(style="standard", data=data, source="list") 297 | return True 298 | 299 | def history(self): 300 | data = self.api.get_history() 301 | Format.cli(style="standard", data=data, source="history") 302 | return True 303 | 304 | def create(self, name): 305 | console = Console() 306 | url = "https://{}{}".format( 307 | self.api.settings["dnac"]["hostname"], "/api/system/v1/maglev/backup" 308 | ) 309 | payload = {"description": name} 310 | payload = json.dumps(payload) 311 | 312 | data = self.api._request(type="post", url=url, payload=payload) 313 | 314 | if type(data["response"]) == str: 315 | message = data["response"] 316 | console.log(f"Creating backup with id: {message}") 317 | else: 318 | message = data["response"]["error"] 319 | console.log(f"Error: {message}") 320 | 321 | return True 322 | 323 | def progress(self): 324 | """ 325 | Cisco DNA Center Backups in Progress 326 | """ 327 | data = self.api.get_progress() 328 | Format.cli(style="standard", data=data, source="progress") 329 | return True 330 | 331 | def delete(self, backup_id): 332 | console = Console() 333 | """ 334 | Task to delete backups, either from str or list 335 | """ 336 | def task(id): 337 | if len(id) != 36: 338 | raise ValueError(f"Invalid Backup ID - {id}") 339 | url = "https://{}{}{}".format( 340 | self.api.settings["dnac"]["hostname"], 341 | "/api/system/v1/maglev/backup/", 342 | id, 343 | ) 344 | data = self.api._request(type="delete", url=url) 345 | 346 | if "status" in data["response"]: 347 | if data["response"]["status"] == "ok": 348 | console.log(data["response"]["message"]) 349 | else: 350 | console.log("Error: {}".format(data["response"])) 351 | 352 | return True 353 | if isinstance(backup_id, str): 354 | task(backup_id) 355 | elif isinstance(backup_id, list): 356 | for id in backup_id: 357 | task(id) 358 | elif isinstance(backup_id, tuple): 359 | for id in backup_id: 360 | task(id) 361 | else: 362 | raise TypeError(f"Incorrect type for backup_id - {type(backup_id)}") 363 | 364 | return True 365 | 366 | def schedule_backup(self, **kwargs): 367 | name = kwargs["name"] 368 | url = "https://{}{}".format( 369 | self.api.settings["dnac"]["hostname"], 370 | "/api/system/v1/maglev/schedule/backup", 371 | ) 372 | url_post = url + f"/{name}" 373 | existing_data = self.api._request(type="get", url=url) 374 | 375 | if not existing_data["response"]: 376 | 377 | if kwargs["action"] == "create": 378 | """ 379 | Create list of number 0-6 representing each weekday. This is needed for the DNAC payload. 380 | """ 381 | weekdays = [ 382 | "sunday", 383 | "monday", 384 | "tuesday", 385 | "wednesday", 386 | "thursday", 387 | "friday", 388 | "saturday", 389 | ] 390 | 391 | day_list = kwargs["day"] 392 | if len(day_list) >= 1: 393 | if day_list[0].lower() == "everyday": 394 | _list = [] 395 | for day in weekdays: 396 | _list.append(str(weekdays.index(day))) 397 | interval = ",".join(_list) 398 | 399 | else: 400 | days = list(day_list) 401 | _list = [] 402 | 403 | for day in days: 404 | day = day.lower() 405 | _list.append(str(weekdays.index(day))) 406 | interval = ",".join(_list) 407 | else: 408 | raise Exception( 409 | "Error in day argument when using schedule_backup()" 410 | ) 411 | 412 | time = kwargs["time"] 413 | time_list = time.split(":") 414 | time_list[0] = int(time_list[0]) - 1 415 | """ 416 | Payload for the POST request 417 | """ 418 | payload = { 419 | "schedule": "{} {} * * {}".format( 420 | time_list[1], time_list[0], interval 421 | ), 422 | "json_payload": { 423 | "description": kwargs["name"], 424 | "appstacks": {"ndp": {}}, 425 | }, 426 | "env": {}, 427 | "url": "http://glusterfs-brick.maglev-system.svc.cluster.local:8080/api/v1/sidecar/backup/1234", 428 | } 429 | payload = json.dumps(payload) 430 | 431 | data = self.api._request(type="post", url=url_post, payload=payload) 432 | message = "There is now a scheduled backup" 433 | data = [message, name, None] 434 | else: 435 | message = "There is no scheduled backup available to delete" 436 | data = [message] 437 | 438 | else: 439 | if kwargs["action"] == "create": 440 | message = "There already exists a scheduled backup" 441 | name = existing_data["response"][0]["name"] 442 | ts = int(existing_data["response"][0]["upcoming_run"]) 443 | upcoming_time = datetime.utcfromtimestamp(ts).strftime( 444 | "%Y-%m-%d %H:%M" 445 | ) 446 | data = [message, name, upcoming_time] 447 | else: 448 | if existing_data["response"][0]["name"] == name: 449 | data = self.api._request(type="delete", url=url_post) 450 | message = f"Backup with the name '{name}' has been deleted" 451 | data = [message] 452 | else: 453 | message = f"No backup with the name '{name}' exists." 454 | data = [message] 455 | 456 | return data 457 | 458 | def backup_delta(self, data, days): 459 | """ Calc delta times to keep backups based on days """ 460 | timenow = datetime.now(tz=timezone.utc) 461 | keep = days[:-1] 462 | delta = timenow - timedelta(int(keep)) 463 | delta_ts = datetime.timestamp(delta) 464 | res = [b for b in data["response"] if b["end_timestamp"] >= delta_ts] 465 | return res 466 | 467 | def backups_to_delete(self, **kwargs): 468 | """ 469 | Purge (delete) Cisco DNA Center Backups 470 | """ 471 | 472 | data = self.api.get(reverse=True) 473 | 474 | """ 475 | List of {backup_ids} to keep 476 | """ 477 | backups_to_keep = [] 478 | 479 | if kwargs["incompatible"]: 480 | """ 481 | Amount of {backup_ids} that is compatible 482 | """ 483 | 484 | for backup in data["response"]: 485 | if backup["compatible"].upper() == "TRUE": 486 | backups_to_keep.append(backup) 487 | elif "d" in str(kwargs["keep"]): 488 | """ 489 | Amount of {backup_ids} to keep based on days 490 | """ 491 | backups = self.backup_delta(data, kwargs["keep"]) 492 | for backup in backups: 493 | backups_to_keep.append(backup) 494 | else: 495 | """ 496 | Amount of {backup_ids} to keep 497 | """ 498 | i = 0 499 | for backup in data["response"]: 500 | if i != int(kwargs["keep"]): 501 | backups_to_keep.append(backup) 502 | i += 1 503 | else: 504 | break 505 | """ 506 | Removing {backup_ids} in {backups_to_keep} from data["response"] 507 | """ 508 | for backup in backups_to_keep: 509 | data["response"].remove(backup) 510 | 511 | return data 512 | 513 | def purge(self, keep, incompatible, force): 514 | """ 515 | Purge (delete) Cisco DNA Center Backups 516 | """ 517 | console = Console() 518 | data = self.backups_to_delete( 519 | incompatible=incompatible, keep=keep, force=force 520 | ) # backups_to_delete 521 | 522 | if len(data["response"]) == 0: 523 | console.log("No backup to delete") 524 | return False 525 | else: 526 | backup_id_to_delete = [] 527 | for item in range(0, len(data["response"])): 528 | backup_id_to_delete.append(data["response"][item]["backup_id"]) 529 | 530 | if force is True: 531 | """ 532 | Displayed deleted backups 533 | """ 534 | for i in range(0, len(backup_id_to_delete)): 535 | self.delete(backup_id_to_delete[i]) 536 | Format.cli(style="standard", data=data, source="list") 537 | console.log( 538 | f"Success: Backups ({len(data['response'])}) deleted", style="green" 539 | ) 540 | return True 541 | else: 542 | """ 543 | Display candidates to be deleted 544 | """ 545 | Format.cli(style="standard", data=data, source="list") 546 | 547 | """ 548 | Confirm action to delete 549 | """ 550 | confirm = click.prompt( 551 | click.style( 552 | "Warning: Confirm if you want to delete these backups (y/n)", 553 | fg="red", 554 | ), 555 | type=bool, 556 | ) 557 | if not confirm: 558 | console.log("Warning: Purge aborted", style="red") 559 | return True 560 | 561 | """ 562 | Purging backups with force 563 | """ 564 | console.log( 565 | "Deleting... (this could take a while - as it's synchronous API calls)", 566 | style="red", 567 | ) 568 | 569 | data = self.backups_to_delete( 570 | incompatible=incompatible, keep=keep, force=True 571 | ) 572 | 573 | backup_id = [x["backup_id"] for x in data["response"]] 574 | self.delete(backup_id) 575 | console.log( 576 | f"Success: Backups ({len(data['response'])}) deleted {backup_id}", 577 | style="green", 578 | ) 579 | return True 580 | -------------------------------------------------------------------------------- /ciscodnacbackupctl/cli.py: -------------------------------------------------------------------------------- 1 | import time 2 | import click 3 | import ciscodnacbackupctl 4 | import rich 5 | from rich.console import Console 6 | import daemonocle 7 | import schedule 8 | from ciscodnacbackupctl import Format 9 | from ciscodnacbackupctl import Config 10 | 11 | # Need to pass the KEEP variable to daemon worker 12 | KEEP = 3 13 | 14 | 15 | def app_daemon( 16 | interval="daily", incompatible=False, keep=KEEP, day="monday", hour="23:00" 17 | ): 18 | global KEEP 19 | ctl = ciscodnacbackupctl.Api() 20 | interval = interval.lower() 21 | _day = day.lower() 22 | _time = hour.lower() 23 | """ 24 | job() function makes sure to purge the backups when called upon 25 | """ 26 | 27 | def job(): 28 | console = Console() 29 | ctl = ciscodnacbackupctl.Api() 30 | cli = ctl.CLI() 31 | force = True 32 | try: 33 | purge = cli.purge(keep=KEEP, incompatible=incompatible, force=force) 34 | except Exception as error_msg: 35 | console.log( 36 | "Cisco DNA Center isn't available - {}".format(error_msg), style="green" 37 | ) 38 | pass 39 | 40 | schedule_function = ctl.schedule_interval( 41 | interval=interval, day=_day, time=_time 42 | ).do(job) 43 | while True: 44 | schedule.run_pending() 45 | time.sleep(1) 46 | return 47 | 48 | @click.group() 49 | @click.option('--debug/--no-debug', default=False) 50 | @click.version_option() 51 | @click.pass_context 52 | def cli(ctx, debug): 53 | ctx.obj["DEBUG"] = debug 54 | if debug: 55 | print("Debug mode: {}".format(debug)) 56 | pass 57 | 58 | @cli.command("daemon", help="{}".format(daemonocle.Daemon.list_actions())) 59 | @click.option("--detach/--debug", default=True, help="Attach and debug") 60 | @click.option( 61 | "--keep", type=str, default=3, help="Amount of backups to keep (default 3)" 62 | ) 63 | @click.argument("command", nargs=1) 64 | @click.pass_context 65 | def daemon(ctx, detach, keep, command): 66 | global KEEP 67 | KEEP = keep 68 | daemon = daemonocle.Daemon( 69 | worker=app_daemon, 70 | pid_file="/tmp/ciscodnacbackupctl.pid", 71 | detach=detach, 72 | ) 73 | if command.lower() in daemonocle.Daemon.list_actions(): 74 | if command.lower() == "status": 75 | data = daemon.get_status() 76 | output = Format.cli(style="standard", data=data, source="dict") 77 | return 78 | daemon.do_action(command) 79 | return 80 | click.echo(ctx.get_help()) 81 | return 82 | 83 | 84 | @cli.command("whoami") 85 | @click.option("--cfg", is_flag=True, help="Read local cfg") 86 | @click.pass_context 87 | def whoami(ctx, cfg): 88 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 89 | cli.whoami(cfg=cfg) 90 | return 91 | 92 | 93 | @cli.command("config") 94 | @click.option("--env/--no-env", default=False) 95 | @click.option("--hostname", required=True) 96 | @click.option("--username", required=True) 97 | @click.option("--password", required=True) 98 | @click.option("--secure", is_flag=True, help="Secure HTTPS towards Cisco DNA Center") 99 | @click.option( 100 | "--encode", is_flag=True, help="Encode Cisco DNA Center config to Base64 string" 101 | ) 102 | @click.option( 103 | "--overwrite", 104 | is_flag=True, 105 | help="Writes over the existing config for Cisco DNA Center", 106 | ) 107 | @click.pass_context 108 | def config(ctx, env, hostname, username, password, secure, overwrite, encode): 109 | ctl = ciscodnacbackupctl.Api(config=True) 110 | console = Console() 111 | if env: 112 | if encode: 113 | data = { 114 | "dnac": { 115 | "hostname": hostname, 116 | "username": username, 117 | "password": password, 118 | "secure": secure, 119 | } 120 | } 121 | data = Config.base64_cfg(operation="encode", data=data) 122 | environments = { 123 | "DNAC_CONFIG": data[1], 124 | } 125 | else: 126 | environments = { 127 | "DNA_CENTER_BASE_URL": hostname, 128 | "DNA_CENTER_USERNAME": username, 129 | "DNA_CENTER_PASSWORD": password, 130 | "DNA_CENTER_VERIFY": secure, 131 | } 132 | console.print( 133 | "Environment Settings Generated (copy paste below)", style="green" 134 | ) 135 | for k, v in environments.items(): 136 | console.print("export {}={}".format(k, v), soft_wrap=True) 137 | else: 138 | """ TODO move to config.py """ 139 | config = ciscodnacbackupctl.Api.config( 140 | hostname=hostname, 141 | username=username, 142 | password=password, 143 | secure=secure, 144 | operation="write", 145 | ) 146 | 147 | if "Config already exist" in str(config[1]) and config[0] is False: 148 | if overwrite is False: 149 | confirm = click.prompt( 150 | click.style( 151 | "Warning: Config already exist ({}), are you sure you want override? (y/n)".format( 152 | ciscodnacbackupctl.config.DNAC_FULL_CONFIG_PATH 153 | ), 154 | fg="red", 155 | ), 156 | type=bool, 157 | ) 158 | if not confirm: 159 | console.print("Warning: Config aborted", style="red") 160 | return 161 | else: 162 | config = ciscodnacbackupctl.Api.config( 163 | hostname=hostname, 164 | username=username, 165 | password=password, 166 | secure=secure, 167 | operation="overwrite", 168 | ) 169 | console.print( 170 | "Success: Config created ({})".format( 171 | ciscodnacbackupctl.config.DNAC_FULL_CONFIG_PATH 172 | ), 173 | style="green", 174 | ) 175 | return 176 | 177 | 178 | @cli.command("list") 179 | @click.option("--reverse/--no-reverse", default=False) 180 | @click.pass_context 181 | def list(ctx, reverse): 182 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 183 | cli.list(reverse) 184 | return 185 | 186 | 187 | @cli.command("progress") 188 | @click.pass_context 189 | def progress(ctx): 190 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 191 | cli.progress() 192 | return 193 | 194 | 195 | @cli.command("history") 196 | @click.pass_context 197 | def history(ctx): 198 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 199 | cli.history() 200 | return 201 | 202 | 203 | @cli.command("create") 204 | @click.option("--name", required=True, help="Name of backup") 205 | @click.pass_context 206 | def create(ctx, name): 207 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 208 | cli.create(name) 209 | return 210 | 211 | 212 | @cli.command("delete") 213 | @click.option("--backup_id", required=True, is_flag=True) 214 | @click.argument('id', nargs=-1) 215 | #@click.argument("--backup_id", required=True, nargs=-1) 216 | @click.pass_context 217 | def delete(ctx, backup_id, id): 218 | cli = ciscodnacbackupctl.Api.CLI(debug=ctx.obj["DEBUG"]) 219 | cli.delete(id) 220 | return 221 | 222 | 223 | @cli.command("schedule_backup") 224 | @click.option( 225 | "--action", 226 | "-a", 227 | required=True, 228 | help="Type " 229 | "Create" 230 | " or " 231 | "Delete" 232 | " to either create or delete scheduled backup", 233 | type=str, 234 | ) 235 | @click.option("--name", "-n", required=True, help="Name of backup", type=str) 236 | @click.option( 237 | "--day", 238 | "-d", 239 | multiple=True, 240 | help="Type which day you want backup or " 241 | "everyday" 242 | " if you want everyday backup.", 243 | default=["everyday"], 244 | ) 245 | @click.option( 246 | "--hour", 247 | "-t", 248 | required=False, 249 | default="23:00", 250 | type=str, 251 | help="At what time hh:mm should the backup be created? Default is 23:00", 252 | ) 253 | @click.pass_context 254 | def schedule_backup(ctx, action, name, day, hour): 255 | action = action.lower() 256 | cli = ciscodnacbackupctl.Api().CLI(debug=ctx.obj["DEBUG"]) 257 | console = rich.get_console() 258 | data = cli.schedule_backup(name=name, day=day, time=hour, action=action) 259 | print(data) 260 | 261 | # TODO move this part to CLI class 262 | if action.lower() == "create": 263 | if "now" in data[0]: 264 | console.print(f"{data[0]} with name '{data[1]}'", style="green") 265 | else: 266 | console.print(f"{data[0]} with name '{data[1]}' ", style="red") 267 | else: 268 | if "no" in data[0]: 269 | console.print(f"{data[0]}", style="red") 270 | else: 271 | console.print(f"{data[0]}", style="red") 272 | 273 | 274 | @cli.command("purge") 275 | @click.option( 276 | "--keep", type=str, default=3, help="Amount of backups to keep (default 3)" 277 | ) 278 | @click.option("--incompatible", is_flag=True, help="Remove incompatible backups") 279 | @click.option("--force", is_flag=True, help="No interactive prompt to confirm purge") 280 | @click.pass_context 281 | def purge(ctx, keep, incompatible, force): 282 | cli = ciscodnacbackupctl.Api().CLI(debug=ctx.obj["DEBUG"]) 283 | purge = cli.purge(keep=keep, incompatible=incompatible, force=force) 284 | 285 | 286 | @cli.command("schedule_purge") 287 | @click.option( 288 | "--interval", "-i", required=True, help=" " "daily" " or " "weekly" " ", type=str 289 | ) 290 | @click.option("--incompatible", is_flag=True, help="Remove incompatible backups") 291 | @click.option( 292 | "--keep", 293 | "-k", 294 | required=False, 295 | default=3, 296 | type=str, 297 | help="Amount of backups to keep, default is 3", 298 | ) 299 | @click.option( 300 | "--day", 301 | "-d", 302 | required=False, 303 | default="monday", 304 | type=str, 305 | help="Day to delete backups on", 306 | ) 307 | @click.option( 308 | "--hour", 309 | "-h", 310 | required=False, 311 | default="23:00", 312 | type=str, 313 | help="Time to delete backups HH:MM, default is 23:00", 314 | ) 315 | @click.pass_context 316 | def schedule_purge(ctx, interval, incompatible, keep, day, hour): 317 | ctl = ciscodnacbackupctl.Api() 318 | cli = ctl.CLI(debug=ctx.obj["DEBUG"]) 319 | console = rich.get_console() 320 | 321 | interval = interval.lower() 322 | _day = day.lower() 323 | _time = hour.lower() 324 | 325 | """ 326 | job() function makes sure to purge the backups when called upon 327 | """ 328 | 329 | def job(): 330 | force = True 331 | ctl = ciscodnacbackupctl.Api() 332 | cli = ctl.CLI(debug=ctx.obj["DEBUG"]) 333 | try: 334 | purge = cli.purge(keep=keep, incompatible=incompatible, force=force) 335 | except Exception as error_msg: 336 | console.log( 337 | "Cisco DNA Center isn't available - {}".format(error_msg), style="green" 338 | ) 339 | pass 340 | 341 | """ 342 | Print out confirmation that the backup is scheduled 343 | """ 344 | # TODO move this part to class CLI 345 | if interval.lower() == "daily": 346 | click.echo( 347 | click.style(f"\nYour backups will be deleted daily at {_time}", fg="green") 348 | ) 349 | else: 350 | click.echo( 351 | click.style( 352 | f"\nYour backups will be deleted {_day}s at {_time}", fg="green" 353 | ) 354 | ) 355 | 356 | schedule_function = ctl.schedule_interval( 357 | interval=interval, day=_day, time=_time 358 | ).do(job) 359 | 360 | while True: 361 | schedule.run_pending() 362 | time.sleep(1) 363 | 364 | 365 | if __name__ == "__main__": 366 | cli(obj={}) 367 | 368 | def entry(): 369 | cli(obj={}) -------------------------------------------------------------------------------- /ciscodnacbackupctl/config.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import os 3 | import json 4 | import base64 5 | 6 | DNAC_CONFIG = "config.json" 7 | DNAC_CONFIG_PATH = "/.ciscodnac/" 8 | DNAC_FULL_CONFIG_PATH = "{}{}{}".format(Path.home(), DNAC_CONFIG_PATH, DNAC_CONFIG) 9 | 10 | 11 | class Config: 12 | def __init__(self): 13 | self.config = self._check_settings() 14 | pass 15 | 16 | def _check_settings(self): 17 | """ 18 | Check Environment cfg 19 | """ 20 | env_cfg = self.read_env() 21 | if env_cfg[0] is True: 22 | return env_cfg[0], env_cfg[1] 23 | """ 24 | Check local file cfg 25 | """ 26 | file_cfg = self.read_file() 27 | if file_cfg[0] is True: 28 | return file_cfg[0], file_cfg[1] 29 | 30 | return False, "Can't find Cisco DNA Center Config" 31 | 32 | @classmethod 33 | def read_env(cls): 34 | """ 35 | Mandatory Environment variables if using Base64 36 | """ 37 | enviroment_cfg_base64 = "DNAC_CONFIG" 38 | if enviroment_cfg_base64 in os.environ: 39 | """ Validate cfg as base64 string """ 40 | data = cls.base64_cfg(operation="decode", data=os.environ["DNAC_CONFIG"]) 41 | if data[0] is True: 42 | return True, data[1] 43 | return False, data[1] 44 | """ 45 | Mandatory Environment variables 46 | """ 47 | enviroment_cfg = [ 48 | "DNA_CENTER_BASE_URL", 49 | "DNA_CENTER_USERNAME", 50 | "DNA_CENTER_PASSWORD", 51 | ] 52 | valid_env_cfg = True 53 | for env in enviroment_cfg: 54 | if env not in os.environ: 55 | valid_env_cfg = False 56 | break 57 | 58 | """ 59 | Validate cfg 60 | """ 61 | if valid_env_cfg: 62 | if "DNA_CENTER_VERIFY" in os.environ: 63 | if "true" in os.environ["DNA_CENTER_VERIFY"].lower(): 64 | secure = True 65 | else: 66 | secure = False 67 | else: 68 | secure = False 69 | data = { 70 | "dnac": { 71 | "hostname": os.environ["DNA_CENTER_BASE_URL"], 72 | "username": os.environ["DNA_CENTER_USERNAME"], 73 | "password": os.environ["DNA_CENTER_PASSWORD"], 74 | "secure": secure, 75 | } 76 | } 77 | return True, data 78 | 79 | return False, "Environment variables missing" 80 | 81 | @classmethod 82 | def base64_cfg(cls, **kwargs): 83 | """ Handle base64 encoding """ 84 | if "encode" in kwargs["operation"]: 85 | """ Encode json cfg to base64 str """ 86 | data = str(json.dumps(kwargs["data"], indent=4)) 87 | data = base64.b64encode(data.encode("ascii")) 88 | return True, data.decode("utf-8") 89 | if "decode" in kwargs["operation"]: 90 | """ Decode base64 str to json cfg """ 91 | data = kwargs["data"] 92 | try: 93 | data == base64.b64encode(base64.b64decode(data)).decode("utf-8") 94 | data = json.loads(base64.b64decode(data).decode("utf-8")) 95 | except Exception as e: 96 | return False, f"Can't decode config ({e})" 97 | return True, data 98 | return False, "Unknown base64 error" 99 | 100 | @classmethod 101 | def read_file(cls): 102 | """ 103 | Reading local cfg file (json) 104 | """ 105 | try: 106 | with open(DNAC_FULL_CONFIG_PATH, "r") as f: 107 | data = f.read() 108 | f.close() 109 | try: 110 | """ Read base64 encoded config """ 111 | data == base64.b64encode(base64.b64decode(data)).decode("utf-8") 112 | data = json.loads(base64.b64decode(data).decode("utf-8")) 113 | except Exception: 114 | try: 115 | """ Read JSON config (if not base64 encoded) """ 116 | data = json.loads(data) 117 | except Exception: 118 | return False, "Can't load json from config" 119 | return True, data 120 | except Exception as e: 121 | return False, f"Can't read config file ({e})" 122 | 123 | @classmethod 124 | def write(cls, hostname, username, password, secure, **kwargs): 125 | """ 126 | Write config to local file (json) 127 | """ 128 | 129 | """ 130 | Check if existing folder structure exist 131 | """ 132 | local_user_path = "{}{}".format(Path.home(), DNAC_CONFIG_PATH) 133 | if os.path.exists(local_user_path) is False: 134 | os.makedirs("{}{}".format(Path.home(), DNAC_CONFIG_PATH)) 135 | 136 | """ 137 | Don't override existing cfg without {overwrite} 138 | """ 139 | if ( 140 | "overwrite" not in kwargs["operation"] 141 | and cls._config_exist(DNAC_FULL_CONFIG_PATH) is True 142 | ): 143 | return False, "Config already exist" 144 | 145 | """ 146 | Write json config to file 147 | """ 148 | data = { 149 | "dnac": { 150 | "hostname": hostname, 151 | "username": username, 152 | "password": password, 153 | "secure": secure, 154 | } 155 | } 156 | try: 157 | with open(DNAC_FULL_CONFIG_PATH, "w") as f: 158 | f.write(json.dumps(data, indent=4)) 159 | f.close() 160 | return True, "Success" 161 | except Exception as e: 162 | return False, f"Can't update config file ({e})" 163 | 164 | @classmethod 165 | def _config_exist(cls, path): 166 | """ 167 | Check if local config file already exist 168 | """ 169 | if os.path.exists(path): 170 | return True 171 | return False 172 | -------------------------------------------------------------------------------- /ciscodnacbackupctl/debug.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from http.client import HTTPConnection 4 | 5 | 6 | class Debug: 7 | """Debug to Console""" 8 | 9 | def __init__(self) -> None: 10 | """Setup""" 11 | self.filename = "debug.txt" 12 | HTTPConnection.debuglevel = 2 13 | logging.basicConfig( 14 | level=logging.DEBUG, 15 | format="%(asctime)s %(levelname)s: %(message)s", 16 | stream=sys.stdout, 17 | ) 18 | requests_log = logging.getLogger("requests.packages.urllib3") 19 | requests_log.setLevel(logging.DEBUG) 20 | requests_log.propagate = True 21 | pass 22 | 23 | @staticmethod 24 | def payload(data): 25 | """HTTP Payload""" 26 | logger = logging.getLogger(__name__) 27 | logger.debug(str(data)) 28 | -------------------------------------------------------------------------------- /ciscodnacbackupctl/format.py: -------------------------------------------------------------------------------- 1 | import time 2 | import rich 3 | from hurry.filesize import size 4 | from tabulate import tabulate 5 | 6 | 7 | class Format: 8 | def __init__(self): 9 | pass 10 | 11 | @staticmethod 12 | def cli(**kwargs): 13 | """ 14 | Handle in coming CLI Output Style 15 | """ 16 | if "standard" in kwargs["style"]: 17 | return Format._default(**kwargs) 18 | return "Error" 19 | 20 | @classmethod 21 | def _default(cls, **kwargs): 22 | table = [] 23 | columns = [] 24 | rows = [] 25 | if kwargs["source"] == "dict": 26 | columns, rows = cls.__dict(kwargs["data"]) 27 | table.append(rows) 28 | 29 | elif kwargs["source"] == "list": 30 | for d in kwargs["data"]["response"]: 31 | columns, rows = cls.__list(d) 32 | table.append(rows) 33 | 34 | elif kwargs["source"] == "history": 35 | for d in kwargs["data"]["response"]: 36 | __columns, __rows = cls.__history(d) 37 | 38 | """ Don't override existing table """ 39 | if len(__columns) != 0: 40 | columns = __columns 41 | if len(__rows) != 0: 42 | rows = __rows 43 | 44 | table.append(rows) 45 | 46 | elif kwargs["source"] == "progress": 47 | for d in kwargs["data"]["response"]: 48 | columns, rows = cls.__progress(d) 49 | table.append(rows) 50 | 51 | if len(rows) != 0: 52 | console = rich.get_console() 53 | console.print( 54 | tabulate( 55 | table, 56 | columns, 57 | tablefmt="plain", 58 | stralign="left", 59 | disable_numparse=True, 60 | ), 61 | soft_wrap=True, 62 | ) 63 | return True 64 | return False 65 | 66 | @classmethod 67 | def __dict(cls, data): 68 | columns = [] 69 | rows = [] 70 | for k, v in data.items(): 71 | if k.lower() != "token": 72 | columns.append(k.upper()) 73 | rows.append(v) 74 | return columns, rows 75 | 76 | @classmethod 77 | def __list(cls, d): 78 | excluded_columns = [ 79 | "versions", 80 | "backup_services", 81 | "compatible_error", 82 | ] 83 | columns = [] 84 | rows = [] 85 | for k, v in d.items(): 86 | if k not in excluded_columns: 87 | columns.append(k.upper()) 88 | if "time" in k: 89 | v = str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(v))) 90 | if "size" in k: 91 | v = str(size(v)) 92 | if "percentage" in k: 93 | v = "{}%".format(int(v)) 94 | if "_version" in k: 95 | v = str(v) 96 | rows.append(v) 97 | return columns, rows 98 | 99 | @classmethod 100 | def __history(cls, d): 101 | included_columns = [ 102 | "backup_id", 103 | "start_timestamp", 104 | "status", 105 | "operation", 106 | "id", 107 | "progress_in_percentage", 108 | "description", 109 | "backup_size", 110 | ] 111 | columns = [] 112 | rows = [] 113 | if "PENDING" not in d["status"]: 114 | for k, v in d.items(): 115 | if k in included_columns: 116 | columns.append(k.upper()) 117 | if "time" in k: 118 | v = str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(v))) 119 | if "size" in k: 120 | v = str(size(v)) 121 | if "percentage" in k: 122 | v = "{}%".format(int(v)) 123 | rows.append(v) 124 | return columns, rows 125 | 126 | @classmethod 127 | def __progress(cls, d): 128 | included_columns = [ 129 | "backup_id", 130 | "start_timestamp", 131 | "status", 132 | "operation", 133 | "id", 134 | "progress_in_percentage", 135 | "description", 136 | "backup_size", 137 | ] 138 | columns = [] 139 | rows = [] 140 | for k, v in d.items(): 141 | if k in included_columns: 142 | columns.append(k.upper()) 143 | if "time" in k: 144 | v = str(time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(v))) 145 | if "size" in k: 146 | v = str(size(v)) 147 | if "percentage" in k: 148 | v = "{}%".format(int(v)) 149 | rows.append(v) 150 | return columns, rows 151 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | rich 3 | tabulate 4 | requests 5 | hurry.filesize 6 | click 7 | schedule 8 | daemonocle -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup, find_packages 3 | import ciscodnacbackupctl 4 | 5 | with open("requirements.txt") as f: 6 | requirements = f.read().splitlines() 7 | 8 | with open( 9 | path.join(path.abspath(path.dirname(__file__)), "README.md"), encoding="utf-8" 10 | ) as f: 11 | long_description = f.read() 12 | 13 | setup( 14 | name="ciscodnacbackupctl", 15 | author=ciscodnacbackupctl.author, 16 | author_email=ciscodnacbackupctl.email, 17 | description=ciscodnacbackupctl.description, 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | url=ciscodnacbackupctl.repo_url, 21 | version=ciscodnacbackupctl.version, 22 | packages=find_packages(), 23 | py_modules=["ciscodnacbackupctl"], 24 | install_requires=requirements, 25 | entry_points=""" 26 | [console_scripts] 27 | ciscodnacbackupctl=ciscodnacbackupctl.cli:entry 28 | """, 29 | python_requires=">=3.8", 30 | ) 31 | -------------------------------------------------------------------------------- /tests/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | from unittest.mock import patch 5 | import unittest 6 | 7 | sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 8 | import ciscodnacbackupctl 9 | 10 | 11 | class TestBackend(unittest.TestCase): 12 | 13 | """Mockup settings for backend""" 14 | 15 | os.environ["DNA_CENTER_BASE_URL"] = "mockup-dnac" 16 | os.environ["DNA_CENTER_USERNAME"] = "mockup-user" 17 | os.environ["DNA_CENTER_PASSWORD"] = "mockup-pass" 18 | os.environ["DNA_CENTER_VERIFY"] = "False" 19 | 20 | @staticmethod 21 | def mockup_response(data): 22 | with open(f"mockup_data/{data}.json", "r") as f: 23 | data = json.loads(f.read()) 24 | return data 25 | 26 | """Test mockup user with whoami""" 27 | 28 | @patch("ciscodnacbackupctl.Api._auth") 29 | def test_whoami(self, auth): 30 | print("") 31 | cli = ciscodnacbackupctl.Api.CLI() 32 | res = cli.whoami() 33 | self.assertEqual(res, True) 34 | return 35 | 36 | """ Test list backups """ 37 | 38 | @patch("ciscodnacbackupctl.Api._auth") 39 | def test_list(self, auth): 40 | print("") 41 | with patch("ciscodnacbackupctl.Api._request") as api: 42 | api.return_value = self.mockup_response("list") 43 | cli = ciscodnacbackupctl.Api.CLI() 44 | res = cli.list(True) 45 | self.assertEqual(res, True) 46 | return 47 | 48 | """ Test list backups with Debug enabled """ 49 | 50 | @patch("ciscodnacbackupctl.Api._auth") 51 | def test_list_debug(self, auth): 52 | print("") 53 | with patch("ciscodnacbackupctl.Api._request") as api: 54 | api.return_value = self.mockup_response("list") 55 | cli = ciscodnacbackupctl.Api.CLI(debug=True) 56 | res = cli.list(True) 57 | self.assertEqual(res, True) 58 | return 59 | 60 | """ Test list history """ 61 | 62 | @patch("ciscodnacbackupctl.Api._auth") 63 | def test_history(self, auth): 64 | print("") 65 | with patch("ciscodnacbackupctl.Api._request") as api: 66 | api.return_value = self.mockup_response("history") 67 | cli = ciscodnacbackupctl.Api.CLI() 68 | res = cli.history() 69 | self.assertEqual(res, True) 70 | return 71 | 72 | """ Test list progress """ 73 | 74 | @patch("ciscodnacbackupctl.Api._auth") 75 | def test_progress(self, auth): 76 | print("") 77 | with patch("ciscodnacbackupctl.Api._request") as api: 78 | api.return_value = self.mockup_response("progress") 79 | cli = ciscodnacbackupctl.Api.CLI() 80 | res = cli.progress() 81 | self.assertEqual(res, True) 82 | return 83 | 84 | """ Test create backup """ 85 | 86 | @patch("ciscodnacbackupctl.Api._auth") 87 | def test_create(self, auth): 88 | print("") 89 | with patch("ciscodnacbackupctl.Api._request") as api: 90 | api.return_value = self.mockup_response("create") 91 | cli = ciscodnacbackupctl.Api.CLI() 92 | res = cli.create(name="mockup") 93 | self.assertEqual(res, True) 94 | return 95 | 96 | """ Test delete one backup """ 97 | 98 | @patch("ciscodnacbackupctl.Api._auth") 99 | def test_delete_working(self, auth): 100 | print("") 101 | with patch("ciscodnacbackupctl.Api._request") as api: 102 | api.return_value = self.mockup_response("delete") 103 | cli = ciscodnacbackupctl.Api.CLI() 104 | res = cli.delete("54d7930a-057c-4c41-b35c-30494133234a") 105 | self.assertEqual(res, True) 106 | return 107 | 108 | """ Test delete multiple backups """ 109 | 110 | @patch("ciscodnacbackupctl.Api._auth") 111 | def test_delete_multi_working(self, auth): 112 | print("") 113 | with patch("ciscodnacbackupctl.Api._request") as api: 114 | api.return_value = self.mockup_response("delete") 115 | cli = ciscodnacbackupctl.Api.CLI() 116 | res = cli.delete(["54d7930a-057c-4c41-b35c-30494133234a", "54d7930a-057c-4c41-b35c-30494133234b", "54d7930a-057c-4c41-b35c-30494133234c"]) 117 | self.assertEqual(res, True) 118 | return 119 | 120 | """ Test delete backup with invalid uuid length """ 121 | 122 | @patch("ciscodnacbackupctl.Api._auth") 123 | def test_delete_incorrect_uuid(self, auth): 124 | print("") 125 | with patch("ciscodnacbackupctl.Api._request") as api: 126 | api.return_value = self.mockup_response("delete") 127 | cli = ciscodnacbackupctl.Api.CLI() 128 | self.assertRaises(ValueError, cli.delete, "123") 129 | return 130 | 131 | """ Test delete multiple backups with invalid uuid length """ 132 | 133 | @patch("ciscodnacbackupctl.Api._auth") 134 | def test_delete_incorrect_uuids(self, auth): 135 | print("") 136 | with patch("ciscodnacbackupctl.Api._request") as api: 137 | api.return_value = self.mockup_response("delete") 138 | cli = ciscodnacbackupctl.Api.CLI() 139 | self.assertRaises(ValueError, cli.delete, ["123", "123", "32553"]) 140 | return 141 | 142 | """ Test delete backup with invalid type """ 143 | 144 | @patch("ciscodnacbackupctl.Api._auth") 145 | def test_delete_incorrect_type(self, auth): 146 | print("") 147 | with patch("ciscodnacbackupctl.Api._request") as api: 148 | api.return_value = self.mockup_response("delete") 149 | cli = ciscodnacbackupctl.Api.CLI() 150 | self.assertRaises(TypeError, cli.delete, {"id": "dict"}) 151 | return 152 | 153 | """ Test purge incompatible backups """ 154 | 155 | @patch("ciscodnacbackupctl.Api._auth") 156 | def test_purge_incompatible(self, auth): 157 | print("") 158 | with patch("ciscodnacbackupctl.Api._request") as api: 159 | api.return_value = self.mockup_response("list") 160 | delete_patcher = patch("ciscodnacbackupctl.Api.CLI.delete") 161 | delete = delete_patcher.start() 162 | delete.return_value = None 163 | cli = ciscodnacbackupctl.Api().CLI() 164 | res = cli.purge(keep=3, incompatible=True, force=True) 165 | self.assertEqual(res, True) 166 | delete = delete_patcher.stop() 167 | return 168 | 169 | """ Test purge keep 3 backups """ 170 | 171 | @patch("ciscodnacbackupctl.Api._auth") 172 | def test_purge_int(self, auth): 173 | print("") 174 | with patch("ciscodnacbackupctl.Api._request") as api: 175 | api.return_value = self.mockup_response("purge") 176 | delete_patcher = patch("ciscodnacbackupctl.Api.CLI.delete") 177 | delete = delete_patcher.start() 178 | delete.return_value = None 179 | cli = ciscodnacbackupctl.Api().CLI() 180 | res = cli.purge(keep=3, incompatible=False, force=True) 181 | self.assertEqual(res, True) 182 | delete = delete_patcher.stop() 183 | return 184 | 185 | """ Test purge keep 30 days backups """ 186 | 187 | @patch("ciscodnacbackupctl.Api._auth") 188 | def test_purge_days(self, auth): 189 | print("") 190 | with patch("ciscodnacbackupctl.Api._request") as api: 191 | api.return_value = self.mockup_response("purge") 192 | delete_patcher = patch("ciscodnacbackupctl.Api.CLI.delete") 193 | delete = delete_patcher.start() 194 | delete.return_value = None 195 | cli = ciscodnacbackupctl.Api().CLI() 196 | res = cli.purge(keep="30d", incompatible=False, force=True) 197 | self.assertEqual(res, True) 198 | delete = delete_patcher.stop() 199 | return 200 | 201 | """ Test purge no backups to delete """ 202 | 203 | @patch("ciscodnacbackupctl.Api._auth") 204 | def test_purge_no_backups_to_delete(self, auth): 205 | print("") 206 | with patch("ciscodnacbackupctl.Api._request") as api: 207 | api.return_value = self.mockup_response("purge") 208 | delete_patcher = patch("ciscodnacbackupctl.Api.CLI.delete") 209 | delete = delete_patcher.start() 210 | delete.return_value = None 211 | cli = ciscodnacbackupctl.Api().CLI() 212 | res = cli.purge(keep="720d", incompatible=False, force=True) 213 | self.assertEqual(res, False) 214 | delete = delete_patcher.stop() 215 | return 216 | 217 | 218 | if __name__ == "__main__": 219 | unittest.main(verbosity=3) 220 | -------------------------------------------------------------------------------- /tests/mockup_data/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "response": "378cc8e5-7d16-4eff-a747-7b5609219f42" 4 | } -------------------------------------------------------------------------------- /tests/mockup_data/delete.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "response": { 4 | "message": "Deleted backup (backup_id='378cc8e5-7d16-4eff-a747-7b5609219f42')", 5 | "status": "ok" 6 | } 7 | } -------------------------------------------------------------------------------- /tests/mockup_data/history.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "response": [ 4 | { 5 | "backup_id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 6 | "backup_size": 135669110, 7 | "description": "backup-failure", 8 | "end_timestamp": 1623263625.6521692, 9 | "group_name": "BACKUP c0ccd153-c0a9-4a68-b4db-66e16449a255", 10 | "id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 11 | "labels": { 12 | "taskId": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 13 | "versions": [ 14 | "device-onboarding-ui:2.1.23.60287", 15 | "rogue-management:1.4.1.41", 16 | "wide-area-bonjour:2.4.1.10016", 17 | "sensor-assurance:1.4.2.447", 18 | "ndp-ui:1.4.1.152", 19 | "dnac-platform:1.2.1.603", 20 | "dnac-search:1.0.0.72", 21 | "system:1.3.0.167", 22 | "ncp-system:2.1.119.62020", 23 | "automation-core:2.1.119.62020", 24 | "platform-ui:1.4.1.140", 25 | "network-visibility:2.1.119.62203", 26 | "command-runner:2.1.119.62020", 27 | "image-management:2.1.119.62020", 28 | "device-onboarding:2.1.119.62020", 29 | "base-provision-core:2.1.119.62020", 30 | "access-control-application:2.1.119.62020", 31 | "sd-access:2.1.119.62020", 32 | "machine-reasoning:2.1.119.210019", 33 | "application-policy:2.1.118.170001", 34 | "ndp-platform:1.4.1.428", 35 | "icap-automation:2.1.119.62020", 36 | "sensor-automation:2.1.119.62020", 37 | "ndp-base-analytics:1.4.1.175", 38 | "assurance:1.4.2.477", 39 | "ssa:2.1.119.1090015", 40 | "path-trace:2.1.119.62020", 41 | "ai-network-analytics:2.1.26.0", 42 | "app-hosting:1.1.0.200914" 43 | ], 44 | "description": "backup-failure", 45 | "action": "BACKUP" 46 | }, 47 | "operation": "BACKUP", 48 | "progress_completed_counts": 10, 49 | "progress_in_percentage": 100.0, 50 | "progress_total_counts": 10, 51 | "start_timestamp": 1623261354.0717318, 52 | "status": "FAILURE", 53 | "tasks": [ 54 | { 55 | "task_name": "BACKUP.PREPARE_BACKUP", 56 | "task_id": "72c0493a-d214-470c-a798-f95d9a06ba37", 57 | "task_callable": "maglev_app_workflow.task.backup_tasks.prepare_backup", 58 | "result": { 59 | "task_result": {}, 60 | "start_timestamp": 1623261354.0717318, 61 | "end_timestamp": 1623261357.6897721 62 | }, 63 | "status": "SUCCESS", 64 | "start_timestamp": 1623261354.0717318, 65 | "end_timestamp": 1623261357.6897721 66 | }, 67 | { 68 | "task_name": "BACKUP.ndp:redis", 69 | "task_id": "81fa6bb0-aabb-4d67-ba72-59da988c6894", 70 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 71 | "result": { 72 | "error_message": "", 73 | "backup_id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 74 | "_id": "60c100ad8d8e98b0a91edb1f", 75 | "status": "completed", 76 | "id": "da637567-4806-4c20-9b89-f4aaf30c208f", 77 | "backup_size": 0, 78 | "tenantId": "SYS0", 79 | "process_time": 4.067683696746826, 80 | "description": "backup-failure", 81 | "versions": [ 82 | "device-onboarding-ui:2.1.23.60287", 83 | "rogue-management:1.4.1.41", 84 | "wide-area-bonjour:2.4.1.10016", 85 | "sensor-assurance:1.4.2.447", 86 | "ndp-ui:1.4.1.152", 87 | "dnac-platform:1.2.1.603", 88 | "dnac-search:1.0.0.72", 89 | "system:1.3.0.167", 90 | "ncp-system:2.1.119.62020", 91 | "automation-core:2.1.119.62020", 92 | "platform-ui:1.4.1.140", 93 | "network-visibility:2.1.119.62203", 94 | "command-runner:2.1.119.62020", 95 | "image-management:2.1.119.62020", 96 | "device-onboarding:2.1.119.62020", 97 | "base-provision-core:2.1.119.62020", 98 | "access-control-application:2.1.119.62020", 99 | "sd-access:2.1.119.62020", 100 | "machine-reasoning:2.1.119.210019", 101 | "application-policy:2.1.118.170001", 102 | "ndp-platform:1.4.1.428", 103 | "icap-automation:2.1.119.62020", 104 | "sensor-automation:2.1.119.62020", 105 | "ndp-base-analytics:1.4.1.175", 106 | "assurance:1.4.2.477", 107 | "ssa:2.1.119.1090015", 108 | "path-trace:2.1.119.62020", 109 | "ai-network-analytics:2.1.26.0", 110 | "app-hosting:1.1.0.200914" 111 | ], 112 | "operation": "BACKUP", 113 | "_version": 2.0, 114 | "service_fqn": "ndp:redis", 115 | "target_node": "", 116 | "start_timestamp": 1623261357.7916703, 117 | "end_timestamp": 1623261361.859354, 118 | "last_modified": 1623261361.8609319 119 | }, 120 | "status": "SUCCESS", 121 | "start_timestamp": 1623261357.7038596, 122 | "end_timestamp": 1623261363.8542595 123 | }, 124 | { 125 | "task_name": "BACKUP.fusion:postgres", 126 | "task_id": "e4cb5fd1-125d-477f-9664-2bb7bf0c42d5", 127 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 128 | "error_type": "Exception", 129 | "status": "FAILURE", 130 | "error": "Error during _process_backup(): Backup (pg_dump) is terminated: Shell termination requested, terminating process.", 131 | "start_timestamp": 1623263624.4623735, 132 | "end_timestamp": 1623263625.6521692 133 | }, 134 | { 135 | "task_name": "BACKUP.app-hosting:postgres", 136 | "task_id": "c771e8bc-e457-4d3a-b973-f314b8aa827f", 137 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 138 | "result": { 139 | "error_message": "", 140 | "service_fqn": "app-hosting:postgres", 141 | "status": "completed", 142 | "id": "bea07852-4565-42f1-95de-9b91000597fa", 143 | "backup_size": 164706, 144 | "tenantId": "SYS0", 145 | "process_time": 6.432929039001465, 146 | "description": "backup-failure", 147 | "versions": [ 148 | "device-onboarding-ui:2.1.23.60287", 149 | "rogue-management:1.4.1.41", 150 | "wide-area-bonjour:2.4.1.10016", 151 | "sensor-assurance:1.4.2.447", 152 | "ndp-ui:1.4.1.152", 153 | "dnac-platform:1.2.1.603", 154 | "dnac-search:1.0.0.72", 155 | "system:1.3.0.167", 156 | "ncp-system:2.1.119.62020", 157 | "automation-core:2.1.119.62020", 158 | "platform-ui:1.4.1.140", 159 | "network-visibility:2.1.119.62203", 160 | "command-runner:2.1.119.62020", 161 | "image-management:2.1.119.62020", 162 | "device-onboarding:2.1.119.62020", 163 | "base-provision-core:2.1.119.62020", 164 | "access-control-application:2.1.119.62020", 165 | "sd-access:2.1.119.62020", 166 | "machine-reasoning:2.1.119.210019", 167 | "application-policy:2.1.118.170001", 168 | "ndp-platform:1.4.1.428", 169 | "icap-automation:2.1.119.62020", 170 | "sensor-automation:2.1.119.62020", 171 | "ndp-base-analytics:1.4.1.175", 172 | "assurance:1.4.2.477", 173 | "ssa:2.1.119.1090015", 174 | "path-trace:2.1.119.62020", 175 | "ai-network-analytics:2.1.26.0", 176 | "app-hosting:1.1.0.200914" 177 | ], 178 | "operation": "BACKUP", 179 | "_version": 2.0, 180 | "backup_id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 181 | "_id": "60c100ad8d8e98b0a91edb17", 182 | "target_node": "", 183 | "start_timestamp": 1623261357.784335, 184 | "end_timestamp": 1623261364.217264, 185 | "last_modified": 1623261364.2179003 186 | }, 187 | "status": "SUCCESS", 188 | "start_timestamp": 1623261357.7074382, 189 | "end_timestamp": 1623261367.38091 190 | }, 191 | { 192 | "task_name": "BACKUP.maglev-system:glusterfs-server", 193 | "task_id": "ad8515c7-57fe-415b-b5e6-da501abeba97", 194 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 195 | "error_type": "Exception", 196 | "status": "FAILURE", 197 | "error": "\"Remote backup server does not have enough space (available='10822377472' bytes, required='22534968872' bytes)\"", 198 | "start_timestamp": 1623261597.1818595, 199 | "end_timestamp": 1623261609.2928183 200 | }, 201 | { 202 | "task_name": "BACKUP.maglev-system:credentialmanager", 203 | "task_id": "4d65b242-7cfe-4359-b845-e0f4ae6eee1f", 204 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 205 | "result": { 206 | "error_message": "", 207 | "backup_id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 208 | "_id": "60c100af8d8e98b0a91ede81", 209 | "status": "completed", 210 | "id": "21453328-2bf1-4a14-9a66-e625bf9be4cd", 211 | "backup_size": 849012, 212 | "tenantId": "SYS0", 213 | "process_time": 10.52857255935669, 214 | "description": "backup-failure", 215 | "versions": [ 216 | "device-onboarding-ui:2.1.23.60287", 217 | "rogue-management:1.4.1.41", 218 | "wide-area-bonjour:2.4.1.10016", 219 | "sensor-assurance:1.4.2.447", 220 | "ndp-ui:1.4.1.152", 221 | "dnac-platform:1.2.1.603", 222 | "dnac-search:1.0.0.72", 223 | "system:1.3.0.167", 224 | "ncp-system:2.1.119.62020", 225 | "automation-core:2.1.119.62020", 226 | "platform-ui:1.4.1.140", 227 | "network-visibility:2.1.119.62203", 228 | "command-runner:2.1.119.62020", 229 | "image-management:2.1.119.62020", 230 | "device-onboarding:2.1.119.62020", 231 | "base-provision-core:2.1.119.62020", 232 | "access-control-application:2.1.119.62020", 233 | "sd-access:2.1.119.62020", 234 | "machine-reasoning:2.1.119.210019", 235 | "application-policy:2.1.118.170001", 236 | "ndp-platform:1.4.1.428", 237 | "icap-automation:2.1.119.62020", 238 | "sensor-automation:2.1.119.62020", 239 | "ndp-base-analytics:1.4.1.175", 240 | "assurance:1.4.2.477", 241 | "ssa:2.1.119.1090015", 242 | "path-trace:2.1.119.62020", 243 | "ai-network-analytics:2.1.26.0", 244 | "app-hosting:1.1.0.200914" 245 | ], 246 | "operation": "BACKUP", 247 | "_version": 2.0, 248 | "service_fqn": "maglev-system:credentialmanager", 249 | "target_node": "", 250 | "start_timestamp": 1623261357.8648968, 251 | "end_timestamp": 1623261368.3934693, 252 | "last_modified": 1623261368.394709 253 | }, 254 | "status": "SUCCESS", 255 | "start_timestamp": 1623261357.711763, 256 | "end_timestamp": 1623261369.2535887 257 | }, 258 | { 259 | "task_name": "BACKUP.maglev-system:mongodb", 260 | "task_id": "806cb6e1-9d40-4973-b862-caf569bd042c", 261 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 262 | "result": { 263 | "error_message": "", 264 | "backup_id": "c0ccd153-c0a9-4a68-b4db-66e16449a255", 265 | "_id": "60c100ad8d8e98b0a91edb06", 266 | "status": "completed", 267 | "id": "8c8bc2fa-f504-47a4-bb43-af11305000df", 268 | "backup_size": 134655392, 269 | "tenantId": "SYS0", 270 | "process_time": 4.9941723346710205, 271 | "description": "backup-failure", 272 | "versions": [ 273 | "device-onboarding-ui:2.1.23.60287", 274 | "rogue-management:1.4.1.41", 275 | "wide-area-bonjour:2.4.1.10016", 276 | "sensor-assurance:1.4.2.447", 277 | "ndp-ui:1.4.1.152", 278 | "dnac-platform:1.2.1.603", 279 | "dnac-search:1.0.0.72", 280 | "system:1.3.0.167", 281 | "ncp-system:2.1.119.62020", 282 | "automation-core:2.1.119.62020", 283 | "platform-ui:1.4.1.140", 284 | "network-visibility:2.1.119.62203", 285 | "command-runner:2.1.119.62020", 286 | "image-management:2.1.119.62020", 287 | "device-onboarding:2.1.119.62020", 288 | "base-provision-core:2.1.119.62020", 289 | "access-control-application:2.1.119.62020", 290 | "sd-access:2.1.119.62020", 291 | "machine-reasoning:2.1.119.210019", 292 | "application-policy:2.1.118.170001", 293 | "ndp-platform:1.4.1.428", 294 | "icap-automation:2.1.119.62020", 295 | "sensor-automation:2.1.119.62020", 296 | "ndp-base-analytics:1.4.1.175", 297 | "assurance:1.4.2.477", 298 | "ssa:2.1.119.1090015", 299 | "path-trace:2.1.119.62020", 300 | "ai-network-analytics:2.1.26.0", 301 | "app-hosting:1.1.0.200914" 302 | ], 303 | "operation": "BACKUP", 304 | "_version": 2.0, 305 | "service_fqn": "maglev-system:mongodb", 306 | "target_node": "", 307 | "start_timestamp": 1623261357.765684, 308 | "end_timestamp": 1623261362.7598562, 309 | "last_modified": 1623261362.7606282 310 | }, 311 | "status": "SUCCESS", 312 | "start_timestamp": 1623261357.7138236, 313 | "end_timestamp": 1623261363.823915 314 | }, 315 | { 316 | "task_name": "BACKUP.POST_BACKUP.fusion:maintenance-service", 317 | "task_id": "aba3657f-1ab6-4285-908e-4ad5d2cd142f", 318 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 319 | "result": {}, 320 | "status": "FAILURE", 321 | "start_timestamp": 1623263625.6521692, 322 | "end_timestamp": 1623263625.6521692 323 | }, 324 | { 325 | "task_name": "BACKUP.POST_BACKUP.ndp:pipelineadmin", 326 | "task_id": "48bd5bd1-12f0-48fa-8d65-3d1db6812ae6", 327 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 328 | "result": {}, 329 | "status": "FAILURE", 330 | "start_timestamp": 1623263625.6521692, 331 | "end_timestamp": 1623263625.6521692 332 | }, 333 | { 334 | "task_name": "BACKUP.FINALIZE_BACKUP", 335 | "task_id": "7bbc8fa9-81c4-400e-9836-4163d7b3269c", 336 | "task_callable": "maglev_app_workflow.task.backup_tasks.finalize_backup", 337 | "result": {}, 338 | "status": "FAILURE", 339 | "start_timestamp": 1623263625.6521692, 340 | "end_timestamp": 1623263625.6521692 341 | } 342 | ], 343 | "versions": [ 344 | "device-onboarding-ui:2.1.23.60287", 345 | "rogue-management:1.4.1.41", 346 | "wide-area-bonjour:2.4.1.10016", 347 | "sensor-assurance:1.4.2.447", 348 | "ndp-ui:1.4.1.152", 349 | "dnac-platform:1.2.1.603", 350 | "dnac-search:1.0.0.72", 351 | "system:1.3.0.167", 352 | "ncp-system:2.1.119.62020", 353 | "automation-core:2.1.119.62020", 354 | "platform-ui:1.4.1.140", 355 | "network-visibility:2.1.119.62203", 356 | "command-runner:2.1.119.62020", 357 | "image-management:2.1.119.62020", 358 | "device-onboarding:2.1.119.62020", 359 | "base-provision-core:2.1.119.62020", 360 | "access-control-application:2.1.119.62020", 361 | "sd-access:2.1.119.62020", 362 | "machine-reasoning:2.1.119.210019", 363 | "application-policy:2.1.118.170001", 364 | "ndp-platform:1.4.1.428", 365 | "icap-automation:2.1.119.62020", 366 | "sensor-automation:2.1.119.62020", 367 | "ndp-base-analytics:1.4.1.175", 368 | "assurance:1.4.2.477", 369 | "ssa:2.1.119.1090015", 370 | "path-trace:2.1.119.62020", 371 | "ai-network-analytics:2.1.26.0", 372 | "app-hosting:1.1.0.200914" 373 | ], 374 | "workflow_id": "f66cc792-8088-4055-98a9-baa6aff3d853" 375 | }, 376 | { 377 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 378 | "backup_size": 24361465880, 379 | "description": "backup-1-3-3-9", 380 | "end_timestamp": 1623272030.3171797, 381 | "group_name": "BACKUP 8760ed00-c81c-4bb2-b60d-3738a27c7866", 382 | "id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 383 | "labels": { 384 | "taskId": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 385 | "versions": [ 386 | "device-onboarding-ui:2.1.23.60287", 387 | "rogue-management:1.4.1.41", 388 | "wide-area-bonjour:2.4.1.10016", 389 | "sensor-assurance:1.4.2.447", 390 | "ndp-ui:1.4.1.152", 391 | "dnac-platform:1.2.1.603", 392 | "dnac-search:1.0.0.72", 393 | "system:1.3.0.167", 394 | "ncp-system:2.1.119.62020", 395 | "automation-core:2.1.119.62020", 396 | "platform-ui:1.4.1.140", 397 | "network-visibility:2.1.119.62203", 398 | "command-runner:2.1.119.62020", 399 | "image-management:2.1.119.62020", 400 | "device-onboarding:2.1.119.62020", 401 | "base-provision-core:2.1.119.62020", 402 | "access-control-application:2.1.119.62020", 403 | "sd-access:2.1.119.62020", 404 | "machine-reasoning:2.1.119.210019", 405 | "application-policy:2.1.118.170001", 406 | "ndp-platform:1.4.1.428", 407 | "icap-automation:2.1.119.62020", 408 | "sensor-automation:2.1.119.62020", 409 | "ndp-base-analytics:1.4.1.175", 410 | "assurance:1.4.2.477", 411 | "ssa:2.1.119.1090015", 412 | "path-trace:2.1.119.62020", 413 | "ai-network-analytics:2.1.26.0", 414 | "app-hosting:1.1.0.200914" 415 | ], 416 | "description": "backup-1-3-3-9", 417 | "action": "BACKUP" 418 | }, 419 | "operation": "BACKUP", 420 | "progress_completed_counts": 10, 421 | "progress_in_percentage": 100.0, 422 | "progress_total_counts": 10, 423 | "start_timestamp": 1623263434.4387012, 424 | "status": "SUCCESS", 425 | "tasks": [ 426 | { 427 | "task_name": "BACKUP.PREPARE_BACKUP", 428 | "task_id": "54b45796-4753-43ec-b6c3-adae5b44cd90", 429 | "task_callable": "maglev_app_workflow.task.backup_tasks.prepare_backup", 430 | "result": { 431 | "task_result": {}, 432 | "start_timestamp": 1623263434.4387012, 433 | "end_timestamp": 1623263438.652167 434 | }, 435 | "status": "SUCCESS", 436 | "start_timestamp": 1623263434.4387012, 437 | "end_timestamp": 1623263438.652167 438 | }, 439 | { 440 | "task_name": "BACKUP.ndp:redis", 441 | "task_id": "27dc9545-e1a3-41a9-9eb9-312e08dbbe45", 442 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 443 | "result": { 444 | "error_message": "", 445 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 446 | "_id": "60c108ce8d8e98b0a91f6d0c", 447 | "status": "completed", 448 | "id": "f9d37317-21d3-4fd2-b454-b25d08fd4f28", 449 | "backup_size": 0, 450 | "tenantId": "SYS0", 451 | "process_time": 3.3499886989593506, 452 | "description": "backup-1-3-3-9", 453 | "versions": [ 454 | "device-onboarding-ui:2.1.23.60287", 455 | "rogue-management:1.4.1.41", 456 | "wide-area-bonjour:2.4.1.10016", 457 | "sensor-assurance:1.4.2.447", 458 | "ndp-ui:1.4.1.152", 459 | "dnac-platform:1.2.1.603", 460 | "dnac-search:1.0.0.72", 461 | "system:1.3.0.167", 462 | "ncp-system:2.1.119.62020", 463 | "automation-core:2.1.119.62020", 464 | "platform-ui:1.4.1.140", 465 | "network-visibility:2.1.119.62203", 466 | "command-runner:2.1.119.62020", 467 | "image-management:2.1.119.62020", 468 | "device-onboarding:2.1.119.62020", 469 | "base-provision-core:2.1.119.62020", 470 | "access-control-application:2.1.119.62020", 471 | "sd-access:2.1.119.62020", 472 | "machine-reasoning:2.1.119.210019", 473 | "application-policy:2.1.118.170001", 474 | "ndp-platform:1.4.1.428", 475 | "icap-automation:2.1.119.62020", 476 | "sensor-automation:2.1.119.62020", 477 | "ndp-base-analytics:1.4.1.175", 478 | "assurance:1.4.2.477", 479 | "ssa:2.1.119.1090015", 480 | "path-trace:2.1.119.62020", 481 | "ai-network-analytics:2.1.26.0", 482 | "app-hosting:1.1.0.200914" 483 | ], 484 | "operation": "BACKUP", 485 | "_version": 2.0, 486 | "service_fqn": "ndp:redis", 487 | "target_node": "", 488 | "start_timestamp": 1623263438.6888947, 489 | "end_timestamp": 1623263442.0388834, 490 | "last_modified": 1623263442.0395207 491 | }, 492 | "status": "SUCCESS", 493 | "start_timestamp": 1623263438.6621644, 494 | "end_timestamp": 1623263444.8031452 495 | }, 496 | { 497 | "task_name": "BACKUP.fusion:postgres", 498 | "task_id": "9ca29aca-70e0-429d-bbfb-5df394f44e35", 499 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 500 | "result": { 501 | "error_message": "", 502 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 503 | "_id": "60c108d08d8e98b0a91f7061", 504 | "status": "completed", 505 | "id": "5d85a8b2-945d-402f-b601-a12767322384", 506 | "backup_size": 1690828185, 507 | "tenantId": "SYS0", 508 | "process_time": 2797.4439578056335, 509 | "description": "backup-1-3-3-9", 510 | "versions": [ 511 | "device-onboarding-ui:2.1.23.60287", 512 | "rogue-management:1.4.1.41", 513 | "wide-area-bonjour:2.4.1.10016", 514 | "sensor-assurance:1.4.2.447", 515 | "ndp-ui:1.4.1.152", 516 | "dnac-platform:1.2.1.603", 517 | "dnac-search:1.0.0.72", 518 | "system:1.3.0.167", 519 | "ncp-system:2.1.119.62020", 520 | "automation-core:2.1.119.62020", 521 | "platform-ui:1.4.1.140", 522 | "network-visibility:2.1.119.62203", 523 | "command-runner:2.1.119.62020", 524 | "image-management:2.1.119.62020", 525 | "device-onboarding:2.1.119.62020", 526 | "base-provision-core:2.1.119.62020", 527 | "access-control-application:2.1.119.62020", 528 | "sd-access:2.1.119.62020", 529 | "machine-reasoning:2.1.119.210019", 530 | "application-policy:2.1.118.170001", 531 | "ndp-platform:1.4.1.428", 532 | "icap-automation:2.1.119.62020", 533 | "sensor-automation:2.1.119.62020", 534 | "ndp-base-analytics:1.4.1.175", 535 | "assurance:1.4.2.477", 536 | "ssa:2.1.119.1090015", 537 | "path-trace:2.1.119.62020", 538 | "ai-network-analytics:2.1.26.0", 539 | "app-hosting:1.1.0.200914" 540 | ], 541 | "operation": "BACKUP", 542 | "_version": 2.0, 543 | "service_fqn": "fusion:postgres", 544 | "target_node": "", 545 | "start_timestamp": 1623269217.5723884, 546 | "end_timestamp": 1623272015.0163462, 547 | "last_modified": 1623272015.0172884 548 | }, 549 | "status": "SUCCESS", 550 | "start_timestamp": 1623269217.4553406, 551 | "end_timestamp": 1623272017.5821428 552 | }, 553 | { 554 | "task_name": "BACKUP.app-hosting:postgres", 555 | "task_id": "298ffe0f-0625-41a2-8831-51b5a6f39071", 556 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 557 | "result": { 558 | "error_message": "", 559 | "service_fqn": "app-hosting:postgres", 560 | "status": "completed", 561 | "id": "4ef03455-e796-4908-aca3-779c6b40c67c", 562 | "backup_size": 164293, 563 | "tenantId": "SYS0", 564 | "process_time": 5.046298265457153, 565 | "description": "backup-1-3-3-9", 566 | "versions": [ 567 | "device-onboarding-ui:2.1.23.60287", 568 | "rogue-management:1.4.1.41", 569 | "wide-area-bonjour:2.4.1.10016", 570 | "sensor-assurance:1.4.2.447", 571 | "ndp-ui:1.4.1.152", 572 | "dnac-platform:1.2.1.603", 573 | "dnac-search:1.0.0.72", 574 | "system:1.3.0.167", 575 | "ncp-system:2.1.119.62020", 576 | "automation-core:2.1.119.62020", 577 | "platform-ui:1.4.1.140", 578 | "network-visibility:2.1.119.62203", 579 | "command-runner:2.1.119.62020", 580 | "image-management:2.1.119.62020", 581 | "device-onboarding:2.1.119.62020", 582 | "base-provision-core:2.1.119.62020", 583 | "access-control-application:2.1.119.62020", 584 | "sd-access:2.1.119.62020", 585 | "machine-reasoning:2.1.119.210019", 586 | "application-policy:2.1.118.170001", 587 | "ndp-platform:1.4.1.428", 588 | "icap-automation:2.1.119.62020", 589 | "sensor-automation:2.1.119.62020", 590 | "ndp-base-analytics:1.4.1.175", 591 | "assurance:1.4.2.477", 592 | "ssa:2.1.119.1090015", 593 | "path-trace:2.1.119.62020", 594 | "ai-network-analytics:2.1.26.0", 595 | "app-hosting:1.1.0.200914" 596 | ], 597 | "operation": "BACKUP", 598 | "_version": 2.0, 599 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 600 | "_id": "60c108ce8d8e98b0a91f6d12", 601 | "target_node": "", 602 | "start_timestamp": 1623263438.7157223, 603 | "end_timestamp": 1623263443.7620206, 604 | "last_modified": 1623263443.762765 605 | }, 606 | "status": "SUCCESS", 607 | "start_timestamp": 1623263438.6674333, 608 | "end_timestamp": 1623263444.916157 609 | }, 610 | { 611 | "task_name": "BACKUP.maglev-system:glusterfs-server", 612 | "task_id": "3f960856-d37f-41e9-a076-b7d8faad82c2", 613 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 614 | "result": { 615 | "error_message": "", 616 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 617 | "_id": "60c108ce8d8e98b0a91f6d10", 618 | "status": "completed", 619 | "id": "a6051c21-8083-4007-bf0f-a55c0db6184b", 620 | "backup_size": 22534968872, 621 | "tenantId": "SYS0", 622 | "process_time": 568.4002614021301, 623 | "description": "backup-1-3-3-9", 624 | "versions": [ 625 | "device-onboarding-ui:2.1.23.60287", 626 | "rogue-management:1.4.1.41", 627 | "wide-area-bonjour:2.4.1.10016", 628 | "sensor-assurance:1.4.2.447", 629 | "ndp-ui:1.4.1.152", 630 | "dnac-platform:1.2.1.603", 631 | "dnac-search:1.0.0.72", 632 | "system:1.3.0.167", 633 | "ncp-system:2.1.119.62020", 634 | "automation-core:2.1.119.62020", 635 | "platform-ui:1.4.1.140", 636 | "network-visibility:2.1.119.62203", 637 | "command-runner:2.1.119.62020", 638 | "image-management:2.1.119.62020", 639 | "device-onboarding:2.1.119.62020", 640 | "base-provision-core:2.1.119.62020", 641 | "access-control-application:2.1.119.62020", 642 | "sd-access:2.1.119.62020", 643 | "machine-reasoning:2.1.119.210019", 644 | "application-policy:2.1.118.170001", 645 | "ndp-platform:1.4.1.428", 646 | "icap-automation:2.1.119.62020", 647 | "sensor-automation:2.1.119.62020", 648 | "ndp-base-analytics:1.4.1.175", 649 | "assurance:1.4.2.477", 650 | "ssa:2.1.119.1090015", 651 | "path-trace:2.1.119.62020", 652 | "ai-network-analytics:2.1.26.0", 653 | "app-hosting:1.1.0.200914" 654 | ], 655 | "operation": "BACKUP", 656 | "_version": 2.0, 657 | "service_fqn": "maglev-system:glusterfs", 658 | "target_node": "100.65.2.10", 659 | "start_timestamp": 1623263635.1839082, 660 | "end_timestamp": 1623264203.5841696, 661 | "last_modified": 1623264203.5848665 662 | }, 663 | "status": "SUCCESS", 664 | "start_timestamp": 1623263635.1560674, 665 | "end_timestamp": 1623264204.7968285 666 | }, 667 | { 668 | "task_name": "BACKUP.maglev-system:credentialmanager", 669 | "task_id": "a730e2f4-6b53-45dc-859c-104492612d85", 670 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 671 | "result": { 672 | "error_message": "", 673 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 674 | "_id": "60c108ce8d8e98b0a91f6d0f", 675 | "status": "completed", 676 | "id": "efd63a39-a953-4561-afde-e47d8fd9329a", 677 | "backup_size": 849017, 678 | "tenantId": "SYS0", 679 | "process_time": 6.487714529037476, 680 | "description": "backup-1-3-3-9", 681 | "versions": [ 682 | "device-onboarding-ui:2.1.23.60287", 683 | "rogue-management:1.4.1.41", 684 | "wide-area-bonjour:2.4.1.10016", 685 | "sensor-assurance:1.4.2.447", 686 | "ndp-ui:1.4.1.152", 687 | "dnac-platform:1.2.1.603", 688 | "dnac-search:1.0.0.72", 689 | "system:1.3.0.167", 690 | "ncp-system:2.1.119.62020", 691 | "automation-core:2.1.119.62020", 692 | "platform-ui:1.4.1.140", 693 | "network-visibility:2.1.119.62203", 694 | "command-runner:2.1.119.62020", 695 | "image-management:2.1.119.62020", 696 | "device-onboarding:2.1.119.62020", 697 | "base-provision-core:2.1.119.62020", 698 | "access-control-application:2.1.119.62020", 699 | "sd-access:2.1.119.62020", 700 | "machine-reasoning:2.1.119.210019", 701 | "application-policy:2.1.118.170001", 702 | "ndp-platform:1.4.1.428", 703 | "icap-automation:2.1.119.62020", 704 | "sensor-automation:2.1.119.62020", 705 | "ndp-base-analytics:1.4.1.175", 706 | "assurance:1.4.2.477", 707 | "ssa:2.1.119.1090015", 708 | "path-trace:2.1.119.62020", 709 | "ai-network-analytics:2.1.26.0", 710 | "app-hosting:1.1.0.200914" 711 | ], 712 | "operation": "BACKUP", 713 | "_version": 2.0, 714 | "service_fqn": "maglev-system:credentialmanager", 715 | "target_node": "", 716 | "start_timestamp": 1623263438.708358, 717 | "end_timestamp": 1623263445.1960726, 718 | "last_modified": 1623263445.1967084 719 | }, 720 | "status": "SUCCESS", 721 | "start_timestamp": 1623263438.6717293, 722 | "end_timestamp": 1623263448.072923 723 | }, 724 | { 725 | "task_name": "BACKUP.maglev-system:mongodb", 726 | "task_id": "928d0a3b-e893-4e3c-b5a1-e5320191b156", 727 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 728 | "result": { 729 | "error_message": "", 730 | "backup_id": "8760ed00-c81c-4bb2-b60d-3738a27c7866", 731 | "_id": "60c108ce8d8e98b0a91f6d19", 732 | "status": "completed", 733 | "id": "56b1bb3f-2464-44d4-a783-5349c605cad0", 734 | "backup_size": 134655513, 735 | "tenantId": "SYS0", 736 | "process_time": 4.325005769729614, 737 | "description": "backup-1-3-3-9", 738 | "versions": [ 739 | "device-onboarding-ui:2.1.23.60287", 740 | "rogue-management:1.4.1.41", 741 | "wide-area-bonjour:2.4.1.10016", 742 | "sensor-assurance:1.4.2.447", 743 | "ndp-ui:1.4.1.152", 744 | "dnac-platform:1.2.1.603", 745 | "dnac-search:1.0.0.72", 746 | "system:1.3.0.167", 747 | "ncp-system:2.1.119.62020", 748 | "automation-core:2.1.119.62020", 749 | "platform-ui:1.4.1.140", 750 | "network-visibility:2.1.119.62203", 751 | "command-runner:2.1.119.62020", 752 | "image-management:2.1.119.62020", 753 | "device-onboarding:2.1.119.62020", 754 | "base-provision-core:2.1.119.62020", 755 | "access-control-application:2.1.119.62020", 756 | "sd-access:2.1.119.62020", 757 | "machine-reasoning:2.1.119.210019", 758 | "application-policy:2.1.118.170001", 759 | "ndp-platform:1.4.1.428", 760 | "icap-automation:2.1.119.62020", 761 | "sensor-automation:2.1.119.62020", 762 | "ndp-base-analytics:1.4.1.175", 763 | "assurance:1.4.2.477", 764 | "ssa:2.1.119.1090015", 765 | "path-trace:2.1.119.62020", 766 | "ai-network-analytics:2.1.26.0", 767 | "app-hosting:1.1.0.200914" 768 | ], 769 | "operation": "BACKUP", 770 | "_version": 2.0, 771 | "service_fqn": "maglev-system:mongodb", 772 | "target_node": "", 773 | "start_timestamp": 1623263438.7231293, 774 | "end_timestamp": 1623263443.048135, 775 | "last_modified": 1623263443.0488467 776 | }, 777 | "status": "SUCCESS", 778 | "start_timestamp": 1623263438.6774695, 779 | "end_timestamp": 1623263444.7809868 780 | }, 781 | { 782 | "task_name": "BACKUP.POST_BACKUP.fusion:maintenance-service", 783 | "task_id": "e2f6f8c2-c568-477f-ac0f-4b45920a833d", 784 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 785 | "result": { 786 | "task_result": {}, 787 | "start_timestamp": 1623272019.5059977, 788 | "end_timestamp": 1623272025.6550524 789 | }, 790 | "status": "SUCCESS", 791 | "start_timestamp": 1623272019.5059977, 792 | "end_timestamp": 1623272025.6550524 793 | }, 794 | { 795 | "task_name": "BACKUP.POST_BACKUP.ndp:pipelineadmin", 796 | "task_id": "379e2168-d5af-4de9-9d11-4f8ff7f84b9f", 797 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 798 | "result": { 799 | "task_result": {}, 800 | "start_timestamp": 1623272019.5094228, 801 | "end_timestamp": 1623272021.292387 802 | }, 803 | "status": "SUCCESS", 804 | "start_timestamp": 1623272019.5094228, 805 | "end_timestamp": 1623272021.292387 806 | }, 807 | { 808 | "task_name": "BACKUP.FINALIZE_BACKUP", 809 | "task_id": "937b5bfb-072d-4f1c-91b2-46358587bda3", 810 | "task_callable": "maglev_app_workflow.task.backup_tasks.finalize_backup", 811 | "result": { 812 | "task_result": {}, 813 | "start_timestamp": 1623272026.3619351, 814 | "end_timestamp": 1623272030.3171797 815 | }, 816 | "status": "SUCCESS", 817 | "start_timestamp": 1623272026.3619351, 818 | "end_timestamp": 1623272030.3171797 819 | } 820 | ], 821 | "versions": [ 822 | "device-onboarding-ui:2.1.23.60287", 823 | "rogue-management:1.4.1.41", 824 | "wide-area-bonjour:2.4.1.10016", 825 | "sensor-assurance:1.4.2.447", 826 | "ndp-ui:1.4.1.152", 827 | "dnac-platform:1.2.1.603", 828 | "dnac-search:1.0.0.72", 829 | "system:1.3.0.167", 830 | "ncp-system:2.1.119.62020", 831 | "automation-core:2.1.119.62020", 832 | "platform-ui:1.4.1.140", 833 | "network-visibility:2.1.119.62203", 834 | "command-runner:2.1.119.62020", 835 | "image-management:2.1.119.62020", 836 | "device-onboarding:2.1.119.62020", 837 | "base-provision-core:2.1.119.62020", 838 | "access-control-application:2.1.119.62020", 839 | "sd-access:2.1.119.62020", 840 | "machine-reasoning:2.1.119.210019", 841 | "application-policy:2.1.118.170001", 842 | "ndp-platform:1.4.1.428", 843 | "icap-automation:2.1.119.62020", 844 | "sensor-automation:2.1.119.62020", 845 | "ndp-base-analytics:1.4.1.175", 846 | "assurance:1.4.2.477", 847 | "ssa:2.1.119.1090015", 848 | "path-trace:2.1.119.62020", 849 | "ai-network-analytics:2.1.26.0", 850 | "app-hosting:1.1.0.200914" 851 | ], 852 | "workflow_id": "596273c9-9fff-42f5-af96-5645a92ddf52" 853 | } 854 | ] 855 | } -------------------------------------------------------------------------------- /tests/mockup_data/progress.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.4.0", 3 | "response": [ 4 | { 5 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 6 | "backup_size": 134003436, 7 | "description": "testcli1", 8 | "group_name": "BACKUP 378cc8e5-7d16-4eff-a747-7b5609219f42", 9 | "id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 10 | "labels": { 11 | "versions": [ 12 | "device-onboarding-ui:2.1.23.60287", 13 | "rogue-management:1.4.1.41", 14 | "wide-area-bonjour:2.4.1.10016", 15 | "sensor-assurance:1.4.2.447", 16 | "ndp-ui:1.4.1.152", 17 | "dnac-platform:1.2.1.603", 18 | "dnac-search:1.0.0.72", 19 | "system:1.3.0.167", 20 | "ncp-system:2.1.119.62020", 21 | "automation-core:2.1.119.62020", 22 | "platform-ui:1.4.1.140", 23 | "network-visibility:2.1.119.62203", 24 | "command-runner:2.1.119.62020", 25 | "image-management:2.1.119.62020", 26 | "device-onboarding:2.1.119.62020", 27 | "base-provision-core:2.1.119.62020", 28 | "access-control-application:2.1.119.62020", 29 | "sd-access:2.1.119.62020", 30 | "machine-reasoning:2.1.119.210019", 31 | "application-policy:2.1.118.170001", 32 | "ndp-platform:1.4.1.428", 33 | "icap-automation:2.1.119.62020", 34 | "sensor-automation:2.1.119.62020", 35 | "ndp-base-analytics:1.4.1.175", 36 | "assurance:1.4.2.477", 37 | "ssa:2.1.119.1090015", 38 | "path-trace:2.1.119.62020", 39 | "ai-network-analytics:2.1.26.0", 40 | "app-hosting:1.1.0.200914" 41 | ], 42 | "description": "testcli1", 43 | "action": "BACKUP", 44 | "taskId": "378cc8e5-7d16-4eff-a747-7b5609219f42" 45 | }, 46 | "operation": "BACKUP", 47 | "progress_completed_counts": 5, 48 | "progress_in_percentage": 50.0, 49 | "progress_total_counts": 10, 50 | "start_timestamp": 1624029832.14639, 51 | "status": "PENDING", 52 | "tasks": [ 53 | { 54 | "task_name": "BACKUP.PREPARE_BACKUP", 55 | "end_timestamp": 1624029834.8224006, 56 | "result": { 57 | "end_timestamp": 1624029834.8224006, 58 | "start_timestamp": 1624029832.14639, 59 | "task_result": {} 60 | }, 61 | "start_timestamp": 1624029832.14639, 62 | "task_callable": "maglev_app_workflow.task.backup_tasks.prepare_backup", 63 | "task_id": "1b246986-f857-441e-a9f7-ca3807032e25", 64 | "status": "SUCCESS" 65 | }, 66 | { 67 | "task_name": "BACKUP.ndp:redis", 68 | "end_timestamp": 1624029837.9917593, 69 | "result": { 70 | "_id": "60ccba8a8d8e98b0a9d83d87", 71 | "start_timestamp": 1624029834.883174, 72 | "backup_size": 0, 73 | "description": "testcli1", 74 | "operation": "BACKUP", 75 | "last_modified": 1624029837.5420487, 76 | "target_node": "", 77 | "status": "completed", 78 | "_version": 2.0, 79 | "end_timestamp": 1624029837.5413108, 80 | "id": "68d379b6-435c-4e9b-8cde-459fe6ee2a9a", 81 | "versions": [ 82 | "device-onboarding-ui:2.1.23.60287", 83 | "rogue-management:1.4.1.41", 84 | "wide-area-bonjour:2.4.1.10016", 85 | "sensor-assurance:1.4.2.447", 86 | "ndp-ui:1.4.1.152", 87 | "dnac-platform:1.2.1.603", 88 | "dnac-search:1.0.0.72", 89 | "system:1.3.0.167", 90 | "ncp-system:2.1.119.62020", 91 | "automation-core:2.1.119.62020", 92 | "platform-ui:1.4.1.140", 93 | "network-visibility:2.1.119.62203", 94 | "command-runner:2.1.119.62020", 95 | "image-management:2.1.119.62020", 96 | "device-onboarding:2.1.119.62020", 97 | "base-provision-core:2.1.119.62020", 98 | "access-control-application:2.1.119.62020", 99 | "sd-access:2.1.119.62020", 100 | "machine-reasoning:2.1.119.210019", 101 | "application-policy:2.1.118.170001", 102 | "ndp-platform:1.4.1.428", 103 | "icap-automation:2.1.119.62020", 104 | "sensor-automation:2.1.119.62020", 105 | "ndp-base-analytics:1.4.1.175", 106 | "assurance:1.4.2.477", 107 | "ssa:2.1.119.1090015", 108 | "path-trace:2.1.119.62020", 109 | "ai-network-analytics:2.1.26.0", 110 | "app-hosting:1.1.0.200914" 111 | ], 112 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 113 | "tenantId": "SYS0", 114 | "service_fqn": "ndp:redis", 115 | "process_time": 2.6581368446350098, 116 | "error_message": "" 117 | }, 118 | "start_timestamp": 1624029834.8564713, 119 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 120 | "task_id": "f9c66634-31fe-4325-bcc3-d2ca04a47ee7", 121 | "status": "SUCCESS" 122 | }, 123 | { 124 | "task_name": "BACKUP.fusion:postgres", 125 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 126 | "start_timestamp": 1624029835.369695, 127 | "result": { 128 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 129 | "start_timestamp": 1624029835.369695, 130 | "backup_size": 0, 131 | "description": "testcli1", 132 | "operation": "BACKUP", 133 | "last_modified": 1624029836.4573648, 134 | "_id": "60ccba8c8d8e98b0a9d840cf", 135 | "status": "in-progress", 136 | "_version": 2.0, 137 | "id": "6f016063-0d77-4815-bb96-3cc4f34f7c53", 138 | "target_node": "", 139 | "tenantId": "SYS0", 140 | "versions": [ 141 | "device-onboarding-ui:2.1.23.60287", 142 | "rogue-management:1.4.1.41", 143 | "wide-area-bonjour:2.4.1.10016", 144 | "sensor-assurance:1.4.2.447", 145 | "ndp-ui:1.4.1.152", 146 | "dnac-platform:1.2.1.603", 147 | "dnac-search:1.0.0.72", 148 | "system:1.3.0.167", 149 | "ncp-system:2.1.119.62020", 150 | "automation-core:2.1.119.62020", 151 | "platform-ui:1.4.1.140", 152 | "network-visibility:2.1.119.62203", 153 | "command-runner:2.1.119.62020", 154 | "image-management:2.1.119.62020", 155 | "device-onboarding:2.1.119.62020", 156 | "base-provision-core:2.1.119.62020", 157 | "access-control-application:2.1.119.62020", 158 | "sd-access:2.1.119.62020", 159 | "machine-reasoning:2.1.119.210019", 160 | "application-policy:2.1.118.170001", 161 | "ndp-platform:1.4.1.428", 162 | "icap-automation:2.1.119.62020", 163 | "sensor-automation:2.1.119.62020", 164 | "ndp-base-analytics:1.4.1.175", 165 | "assurance:1.4.2.477", 166 | "ssa:2.1.119.1090015", 167 | "path-trace:2.1.119.62020", 168 | "ai-network-analytics:2.1.26.0", 169 | "app-hosting:1.1.0.200914" 170 | ], 171 | "service_fqn": "fusion:postgres" 172 | }, 173 | "task_id": "19f15e17-a956-4301-9d31-597fb869ac70", 174 | "status": "IN-PROGRESS" 175 | }, 176 | { 177 | "task_name": "BACKUP.app-hosting:postgres", 178 | "end_timestamp": 1624029841.0798519, 179 | "result": { 180 | "versions": [ 181 | "device-onboarding-ui:2.1.23.60287", 182 | "rogue-management:1.4.1.41", 183 | "wide-area-bonjour:2.4.1.10016", 184 | "sensor-assurance:1.4.2.447", 185 | "ndp-ui:1.4.1.152", 186 | "dnac-platform:1.2.1.603", 187 | "dnac-search:1.0.0.72", 188 | "system:1.3.0.167", 189 | "ncp-system:2.1.119.62020", 190 | "automation-core:2.1.119.62020", 191 | "platform-ui:1.4.1.140", 192 | "network-visibility:2.1.119.62203", 193 | "command-runner:2.1.119.62020", 194 | "image-management:2.1.119.62020", 195 | "device-onboarding:2.1.119.62020", 196 | "base-provision-core:2.1.119.62020", 197 | "access-control-application:2.1.119.62020", 198 | "sd-access:2.1.119.62020", 199 | "machine-reasoning:2.1.119.210019", 200 | "application-policy:2.1.118.170001", 201 | "ndp-platform:1.4.1.428", 202 | "icap-automation:2.1.119.62020", 203 | "sensor-automation:2.1.119.62020", 204 | "ndp-base-analytics:1.4.1.175", 205 | "assurance:1.4.2.477", 206 | "ssa:2.1.119.1090015", 207 | "path-trace:2.1.119.62020", 208 | "ai-network-analytics:2.1.26.0", 209 | "app-hosting:1.1.0.200914" 210 | ], 211 | "service_fqn": "app-hosting:postgres", 212 | "_id": "60ccba8a8d8e98b0a9d83d8c", 213 | "id": "f6be5bb1-f103-41a6-bda3-fa6ad863d8d0", 214 | "start_timestamp": 1624029834.901632, 215 | "backup_size": 188174, 216 | "description": "testcli1", 217 | "target_node": "", 218 | "last_modified": 1624029838.786672, 219 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 220 | "_version": 2.0, 221 | "end_timestamp": 1624029838.785944, 222 | "operation": "BACKUP", 223 | "tenantId": "SYS0", 224 | "status": "completed", 225 | "process_time": 3.8843119144439697, 226 | "error_message": "" 227 | }, 228 | "start_timestamp": 1624029834.860224, 229 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 230 | "task_id": "7248b569-d8f2-4ed5-9cb7-decfc69d7416", 231 | "status": "SUCCESS" 232 | }, 233 | { 234 | "task_name": "BACKUP.maglev-system:glusterfs-server", 235 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 236 | "start_timestamp": 1624029835.2797556, 237 | "result": { 238 | "_id": "60ccba8c8d8e98b0a9d840ce", 239 | "start_timestamp": 1624029835.2797556, 240 | "tenantId": "SYS0", 241 | "backup_size": 0, 242 | "service_fqn": "maglev-system:glusterfs", 243 | "target_node": "100.65.2.10", 244 | "last_modified": 1624029837.2919283, 245 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 246 | "status": "in-progress (calculating size)", 247 | "_version": 2.0, 248 | "id": "80486ae9-b7fa-4136-81bb-924088596c07", 249 | "operation": "BACKUP", 250 | "versions": [ 251 | "device-onboarding-ui:2.1.23.60287", 252 | "rogue-management:1.4.1.41", 253 | "wide-area-bonjour:2.4.1.10016", 254 | "sensor-assurance:1.4.2.447", 255 | "ndp-ui:1.4.1.152", 256 | "dnac-platform:1.2.1.603", 257 | "dnac-search:1.0.0.72", 258 | "system:1.3.0.167", 259 | "ncp-system:2.1.119.62020", 260 | "automation-core:2.1.119.62020", 261 | "platform-ui:1.4.1.140", 262 | "network-visibility:2.1.119.62203", 263 | "command-runner:2.1.119.62020", 264 | "image-management:2.1.119.62020", 265 | "device-onboarding:2.1.119.62020", 266 | "base-provision-core:2.1.119.62020", 267 | "access-control-application:2.1.119.62020", 268 | "sd-access:2.1.119.62020", 269 | "machine-reasoning:2.1.119.210019", 270 | "application-policy:2.1.118.170001", 271 | "ndp-platform:1.4.1.428", 272 | "icap-automation:2.1.119.62020", 273 | "sensor-automation:2.1.119.62020", 274 | "ndp-base-analytics:1.4.1.175", 275 | "assurance:1.4.2.477", 276 | "ssa:2.1.119.1090015", 277 | "path-trace:2.1.119.62020", 278 | "ai-network-analytics:2.1.26.0", 279 | "app-hosting:1.1.0.200914" 280 | ], 281 | "description": "testcli1" 282 | }, 283 | "task_id": "21c6bcac-9e29-4f29-a26d-42b8093e76cb", 284 | "status": "IN-PROGRESS" 285 | }, 286 | { 287 | "task_name": "BACKUP.maglev-system:credentialmanager", 288 | "end_timestamp": 1624029841.452968, 289 | "result": { 290 | "versions": [ 291 | "device-onboarding-ui:2.1.23.60287", 292 | "rogue-management:1.4.1.41", 293 | "wide-area-bonjour:2.4.1.10016", 294 | "sensor-assurance:1.4.2.447", 295 | "ndp-ui:1.4.1.152", 296 | "dnac-platform:1.2.1.603", 297 | "dnac-search:1.0.0.72", 298 | "system:1.3.0.167", 299 | "ncp-system:2.1.119.62020", 300 | "automation-core:2.1.119.62020", 301 | "platform-ui:1.4.1.140", 302 | "network-visibility:2.1.119.62203", 303 | "command-runner:2.1.119.62020", 304 | "image-management:2.1.119.62020", 305 | "device-onboarding:2.1.119.62020", 306 | "base-provision-core:2.1.119.62020", 307 | "access-control-application:2.1.119.62020", 308 | "sd-access:2.1.119.62020", 309 | "machine-reasoning:2.1.119.210019", 310 | "application-policy:2.1.118.170001", 311 | "ndp-platform:1.4.1.428", 312 | "icap-automation:2.1.119.62020", 313 | "sensor-automation:2.1.119.62020", 314 | "ndp-base-analytics:1.4.1.175", 315 | "assurance:1.4.2.477", 316 | "ssa:2.1.119.1090015", 317 | "path-trace:2.1.119.62020", 318 | "ai-network-analytics:2.1.26.0", 319 | "app-hosting:1.1.0.200914" 320 | ], 321 | "_id": "60ccba8a8d8e98b0a9d83d99", 322 | "start_timestamp": 1624029834.962594, 323 | "backup_size": 849208, 324 | "description": "testcli1", 325 | "target_node": "", 326 | "last_modified": 1624029841.2498572, 327 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 328 | "status": "completed", 329 | "_version": 2.0, 330 | "end_timestamp": 1624029841.200687, 331 | "id": "2dd8e00f-bd37-4697-88d9-d6ece4f9f617", 332 | "operation": "BACKUP", 333 | "tenantId": "SYS0", 334 | "service_fqn": "maglev-system:credentialmanager", 335 | "process_time": 6.23809289932251, 336 | "error_message": "" 337 | }, 338 | "start_timestamp": 1624029834.863914, 339 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 340 | "task_id": "a72d58f6-be1f-4e02-b8a6-56ce9d035bea", 341 | "status": "SUCCESS" 342 | }, 343 | { 344 | "task_name": "BACKUP.maglev-system:mongodb", 345 | "end_timestamp": 1624029841.0048943, 346 | "result": { 347 | "versions": [ 348 | "device-onboarding-ui:2.1.23.60287", 349 | "rogue-management:1.4.1.41", 350 | "wide-area-bonjour:2.4.1.10016", 351 | "sensor-assurance:1.4.2.447", 352 | "ndp-ui:1.4.1.152", 353 | "dnac-platform:1.2.1.603", 354 | "dnac-search:1.0.0.72", 355 | "system:1.3.0.167", 356 | "ncp-system:2.1.119.62020", 357 | "automation-core:2.1.119.62020", 358 | "platform-ui:1.4.1.140", 359 | "network-visibility:2.1.119.62203", 360 | "command-runner:2.1.119.62020", 361 | "image-management:2.1.119.62020", 362 | "device-onboarding:2.1.119.62020", 363 | "base-provision-core:2.1.119.62020", 364 | "access-control-application:2.1.119.62020", 365 | "sd-access:2.1.119.62020", 366 | "machine-reasoning:2.1.119.210019", 367 | "application-policy:2.1.118.170001", 368 | "ndp-platform:1.4.1.428", 369 | "icap-automation:2.1.119.62020", 370 | "sensor-automation:2.1.119.62020", 371 | "ndp-base-analytics:1.4.1.175", 372 | "assurance:1.4.2.477", 373 | "ssa:2.1.119.1090015", 374 | "path-trace:2.1.119.62020", 375 | "ai-network-analytics:2.1.26.0", 376 | "app-hosting:1.1.0.200914" 377 | ], 378 | "_id": "60ccba8a8d8e98b0a9d83d89", 379 | "start_timestamp": 1624029834.8950803, 380 | "backup_size": 132966054, 381 | "description": "testcli1", 382 | "target_node": "", 383 | "last_modified": 1624029839.353568, 384 | "backup_id": "378cc8e5-7d16-4eff-a747-7b5609219f42", 385 | "status": "completed", 386 | "_version": 2.0, 387 | "end_timestamp": 1624029839.3529274, 388 | "id": "05f2b560-3cdc-47f8-8f89-ad11b733f875", 389 | "operation": "BACKUP", 390 | "tenantId": "SYS0", 391 | "service_fqn": "maglev-system:mongodb", 392 | "process_time": 4.4578471183776855, 393 | "error_message": "" 394 | }, 395 | "start_timestamp": 1624029834.8663228, 396 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_backup", 397 | "task_id": "54409041-3508-4e3c-a6af-02f6c6565689", 398 | "status": "SUCCESS" 399 | }, 400 | { 401 | "task_name": "BACKUP.POST_BACKUP.fusion:maintenance-service", 402 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 403 | "task_id": "3fb26bca-e21a-4fce-b867-6afe5db91820", 404 | "status": "PENDING", 405 | "result": {} 406 | }, 407 | { 408 | "task_name": "BACKUP.POST_BACKUP.ndp:pipelineadmin", 409 | "task_callable": "maglev_app_workflow.task.backup_tasks.do_post_backup_per_appstack", 410 | "task_id": "bc902c85-c9f3-46df-9295-cb46c0e08be0", 411 | "status": "PENDING", 412 | "result": {} 413 | }, 414 | { 415 | "task_name": "BACKUP.FINALIZE_BACKUP", 416 | "task_callable": "maglev_app_workflow.task.backup_tasks.finalize_backup", 417 | "task_id": "92266b1b-8091-44c1-bdf5-40714d4db32e", 418 | "status": "PENDING", 419 | "result": {} 420 | } 421 | ], 422 | "versions": [ 423 | "device-onboarding-ui:2.1.23.60287", 424 | "rogue-management:1.4.1.41", 425 | "wide-area-bonjour:2.4.1.10016", 426 | "sensor-assurance:1.4.2.447", 427 | "ndp-ui:1.4.1.152", 428 | "dnac-platform:1.2.1.603", 429 | "dnac-search:1.0.0.72", 430 | "system:1.3.0.167", 431 | "ncp-system:2.1.119.62020", 432 | "automation-core:2.1.119.62020", 433 | "platform-ui:1.4.1.140", 434 | "network-visibility:2.1.119.62203", 435 | "command-runner:2.1.119.62020", 436 | "image-management:2.1.119.62020", 437 | "device-onboarding:2.1.119.62020", 438 | "base-provision-core:2.1.119.62020", 439 | "access-control-application:2.1.119.62020", 440 | "sd-access:2.1.119.62020", 441 | "machine-reasoning:2.1.119.210019", 442 | "application-policy:2.1.118.170001", 443 | "ndp-platform:1.4.1.428", 444 | "icap-automation:2.1.119.62020", 445 | "sensor-automation:2.1.119.62020", 446 | "ndp-base-analytics:1.4.1.175", 447 | "assurance:1.4.2.477", 448 | "ssa:2.1.119.1090015", 449 | "path-trace:2.1.119.62020", 450 | "ai-network-analytics:2.1.26.0", 451 | "app-hosting:1.1.0.200914" 452 | ], 453 | "workflow_id": "4d997e9c-ae04-490f-9c5c-e839b39a3796" 454 | } 455 | ] 456 | } --------------------------------------------------------------------------------