├── .gitignore ├── .testrunner_env_defaults ├── Dockerfile ├── LICENSE.md ├── NOTICE.md ├── README.md ├── ci ├── pipeline.yml ├── scripts │ ├── integration │ ├── system │ └── unit ├── set-pipeline.sh └── tasks │ ├── integration.yml │ ├── system.yml │ └── unit.yml ├── cmd └── cfops │ ├── createCliCommand.go │ ├── createCliCommand_test.go │ ├── help.go │ ├── main.go │ └── suite_test.go ├── glide.lock ├── glide.yaml ├── integration ├── cfopsintegration_suite_test.go ├── cmd_test.go └── fixtures │ └── nfs_blobstore_test_installation_settings.json ├── logos ├── cfops_logo.zip ├── dm_design.pdf ├── original-logos-2016-Feb-8505-11461245.jpg └── original-logos-2016-Feb-8505-11461245.png ├── plugin ├── cfopsplugin │ ├── backuprestore_plugin.go │ ├── backuprestore_rpc.go │ ├── backuprestore_rpc_server.go │ ├── cfopsplugin.go │ ├── const.go │ ├── default_pivotalcf.go │ ├── default_pivotalcf_test.go │ ├── fixtures │ │ ├── installation-settings-1-6-aws.json │ │ ├── installation-settings-1-6-default.json │ │ └── installation-settings-with-rabbit.json │ ├── init.go │ ├── plugin_tile_builder.go │ ├── plugin_tile_builder_test.go │ ├── sample │ │ └── main.go │ ├── suite_test.go │ └── types.go ├── fake │ ├── init.go │ ├── pivotalcf.go │ └── plugin.go ├── fixtures │ └── installation-settings-1-6-default.json └── load │ ├── busted_plugins │ └── busted-plugin │ ├── const.go │ ├── fixture_plugins │ ├── darwin │ │ └── sample │ ├── linux │ │ └── sample │ └── windows │ │ ├── sample │ │ └── sample.exe │ ├── init.go │ ├── init_test.go │ └── suite_test.go ├── scripts ├── backup.cron ├── build ├── build_cfops_centos7.sh ├── ctl.sh ├── generate_sample_plugins ├── run └── run_system_test ├── system ├── assets │ └── test-app │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── app.rb │ │ └── config.ru ├── cfopssystem_suite_test.go ├── ert_system_test.go ├── http_client.go ├── opsman_system_test.go ├── opsmanager_client.go └── system_test_helpers.go ├── testCoverage ├── testrunner ├── tile ├── .gitignore ├── cfops.yml ├── resources │ └── icon.png └── tile.yml └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | # Folders 6 | _obj 7 | _test 8 | # Architecture specific extensions/prefixes 9 | *.[568vq] 10 | [568vq].out 11 | *.cgo1.go 12 | *.cgo2.c 13 | _cgo_defun.c 14 | _cgo_gotypes.go 15 | _cgo_export.* 16 | _testmain.go 17 | *.test 18 | *.prof 19 | *.coverprofile 20 | *.log 21 | cfdeploy.iml 22 | cfdeploy 23 | test/deployments/ 24 | main 25 | .DS_Store 26 | out/ 27 | coverage.out 28 | cmd/cfops/cfops 29 | cmd/cfops/v2/v2 30 | _builds 31 | _projects 32 | _steps 33 | 34 | plugins/ 35 | 36 | .envrc 37 | !plugin/load/fixture_plugins/windows/sample.exe 38 | plugin/cfopsplugin/sample/sample 39 | /vendor 40 | .wercker/ 41 | /tmp 42 | secrets.* 43 | /system_test_workspace 44 | -------------------------------------------------------------------------------- /.testrunner_env_defaults: -------------------------------------------------------------------------------- 1 | X_COVERAGE_WATERMARK=73 2 | X_BUILD_DIR=build/ 3 | X_GO15VENDOREXPERIMENT=0 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Version: 0.0.1 2 | FROM ubuntu:latest 3 | MAINTAINER pivotalservices "https://github.com/pivotalservices" 4 | RUN \ 5 | apt-get -qq update && \ 6 | apt-get -y install --fix-missing \ 7 | build-essential \ 8 | wget \ 9 | && \ 10 | apt-get clean 11 | 12 | RUN wget https://pivotal-cfops.s3.amazonaws.com/release/linux64/v2.0.48/cfops 13 | RUN mv cfops /usr/local/bin 14 | RUN chmod +x /usr/local/bin/cfops 15 | 16 | ENV RUN_DIR '/root/pcfbackup' 17 | ENV LOG_DIR '/root/pcfbackup/log' 18 | 19 | RUN mkdir -p $RUN_DIR 20 | RUN mkdir -p $LOG_DIR 21 | 22 | ADD scripts /root/scripts 23 | 24 | CMD ["/root/scripts/ctl.sh", "start", ">>/root/pcfbackup/ctl.log 2>&1 &"] 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | cfops 2 | 3 | Copyright (c) 2014-2015 Pivotal Software, Inc. All Rights Reserved. 4 | 5 | Copyright (c) 2014-2015 EMC Corporation. All Rights Reserved. 6 | 7 | This product is licensed to you under the Apache License, Version 2.0 (the "License"). 8 | You may not use this product except in compliance with the License. 9 | 10 | This product may include a number of subcomponents with separate copyright notices 11 | and license terms. Your use of these subcomponents is subject to the terms and 12 | conditions of the subcomponent's license. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | # check us out at cfops.io 4 | [![wercker status](https://app.wercker.com/status/d0a50d426b77a9f73da0fe4f383ad624/s/master "wercker status")](https://app.wercker.com/project/bykey/d0a50d426b77a9f73da0fe4f383ad624) [![GoDoc](http://godoc.org/github.com/pivotalservices/cfops?status.png)](http://godoc.org/github.com/pivotalservices/cfops) [![Gitter](https://badges.gitter.im/pivotalservices/cfops.svg)](https://gitter.im/pivotalservices/cfops?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 5 | ====== 6 | 7 | ### Version Compatibility 8 | 9 | **IAAS** 10 | Supports PivotalCF on: 11 | - **VSphere** 12 | - ops-manager (backup/restore `verified`) 13 | - elastic-runtime 14 | - postgres datastore (backup/restore `verified`) 15 | - mysql datastore (backup/restore `verified`) 16 | - external datastore (backup/restore `not yet supported`) 17 | - **AWS** 18 | - ops-manager (backup `verified`) 19 | - elastic-runtime (`not yet supported`) 20 | - **OpenStack** 21 | - ops-manager (backup `verified`) 22 | - elastic-runtime 23 | - mysql datastore (backup/restore `verified`) 24 | 25 | 26 | 27 | ### Overview 28 | 29 | CFOPS - is a self contained binary, which has no dependencies 30 | 31 | This is simply an automation that is based on the supported way to back up Pivotal Cloud Foundry (http://docs.pivotal.io/pivotalcf/customizing/backup-restore/backup-pcf.html). 32 | 33 | It may be extended in the future to support greater breadth of functionality. 34 | 35 | **Backing up ER will take Cloud Controller offline for the duration of the backup, causing your foundation to become readonly for the duration of the backup**. App pushes etc will not work during this time. 36 | 37 | ### Install 38 | 39 | Download the latest version here: 40 | https://github.com/pivotalservices/cfops/releases/latest 41 | 42 | ### Contributing 43 | 44 | PRs welcome. To get started follow these steps: 45 | 46 | 47 | * Install [Go 1.6.x](https://golang.org) 48 | * Create a directory where you would like to store the source for Go projects and their binaries (e.g. `$HOME/go`) 49 | * Set an environment variable, `GOPATH`, pointing at the directory you created 50 | * Get the `cf` source: `go get github.com/pivotalservices/cfops` (Ignore any warnings about "no buildable Go source files") 51 | * [Fork this repository](https://help.github.com/articles/fork-a-repo/), adding your fork as a remote 52 | * Install all the required tools - [glide](https://github.com/Masterminds/glide), [wercker cli](http://wercker.com/cli/) 53 | ``` 54 | $ brew install glide 55 | $ brew tap wercker/wercker 56 | $ brew install wercker-cli 57 | ``` 58 | * Wrecker requires a local installation of docker, ensure you have docker in place with the relevant environments set. 59 | ``` 60 | $ docker ps -a # should return something meaningful 61 | ``` 62 | * Pull in glide managed dependencies: 63 | ``` 64 | $ cd $GOPATH/src/github.com/pivotalservices/cfops 65 | $ glide install 66 | ``` 67 | * Build the project: 68 | ``` 69 | $ cd cmd/cfops/ 70 | $ go build 71 | ``` 72 | * At this point you should see the cfops binary in the cmd/cfops folder 73 | * Run wercker integration tests 74 | ``` 75 | $ cd $GOPATH/src/github.com/pivotalservices/cfops 76 | $ ./testrunner 77 | ``` 78 | * At this point you have everything needed for local development, hack away and submit a [pull request](https://help.github.com/articles/using-pull-requests/) to the `develop` branch 79 | 80 | ### Version 3+ notes: 81 | - v3.1.x introduces a breaking behavior compared to prior v3.x releases. In 3.1+ 82 | in the absense of any CC-Vms or if they are unable to be shutdown, cfops will 83 | return an error and exit immediately. 84 | - Prior to 3.1 cfops will continue regardless of if it was successful in 85 | shutting down the cc-vms 86 | 87 | 88 | ### Differences between v1 and v2 89 | 90 | While the core package cfops uses to do the backup & restore has not changed 91 | the cli of cfops itself has been changed. 92 | 93 | Major differences are: 94 | - one must specify a single tile to backup/restore at a time. 95 | - one can list the tiles that are supported via the cli. 96 | - more to come... 97 | 98 | 99 | ### Usage (cfops v2.x.x+) 100 | 101 | **general commands** 102 | 103 | ``` 104 | NAME: 105 | cfops - Cloud Foundry Operations Tool 106 | 107 | USAGE: 108 | ./cfops command [command options] [arguments...] 109 | 110 | COMMANDS: 111 | version shows the application version currently in use 112 | list-tiles shows a list of available backup/restore target tiles 113 | backup creates a backup archive of the target tile 114 | restore restores from an archive to the target tile 115 | help, h Shows a list of commands or help for one command 116 | ``` 117 | 118 | **setting log levels** 119 | 120 | ``` 121 | LOG_LEVEL=(debug|info|error) ./cfops backup ... 122 | ``` 123 | 124 | **list available tiles** 125 | 126 | ``` 127 | $ cfops list-tiles 128 | Available Tiles: 129 | ops-manager 130 | elastic-runtime 131 | ``` 132 | 133 | **list version** 134 | 135 | ``` 136 | $ cfops version 137 | cfops version v2.0.0 138 | ``` 139 | 140 | ** setting S3 domain ** 141 | 142 | ``` 143 | export S3_DOMAIN= 144 | ``` 145 | 146 | **run a backup on a tile** 147 | 148 | ``` 149 | NAME: 150 | ./cfops backup - creates a backup archive of the target tile 151 | 152 | USAGE: 153 | ./cfops backup [command options] [arguments...] 154 | 155 | DESCRIPTION: 156 | backup --opsmanagerhost --adminuser --adminpass --opsmanageruser --opsmanagerpass -d --tile elastic-runtime 157 | 158 | OPTIONS: 159 | --destination, -d path of the Cloud Foundry archive [$CFOPS_DEST_PATH] 160 | --tile, -t a tile you would like to run the operation on [$CFOPS_TILE] 161 | --opsmanagerhost, --omh hostname for Ops Manager [$CFOPS_HOST] 162 | --adminuser, --du username for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_USER] 163 | --adminpass, --dp password for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_PASS] 164 | --opsmanageruser, --omu username for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_USER] 165 | --opsmanagerpass, --omp password for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_PASS] 166 | ``` 167 | 168 | 169 | **run a restore on a tile** 170 | 171 | ``` 172 | NAME: 173 | ./cfops restore - restores from an archive to the target tile 174 | 175 | USAGE: 176 | ./cfops restore [command options] [arguments...] 177 | 178 | DESCRIPTION: 179 | restore --opsmanagerhost --adminuser --adminpass --opsmanageruser --opsmanagerpass -d --tile elastic-runtime 180 | 181 | OPTIONS: 182 | --adminuser, --du username for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_USER] 183 | --adminpass, --dp password for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_PASS] 184 | --opsmanageruser, --omu username for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_USER] 185 | --opsmanagerpass, --omp password for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_PASS] 186 | --destination, -d path of the Cloud Foundry archive [$CFOPS_DEST_PATH] 187 | --tile, -t a tile you would like to run the operation on [$CFOPS_TILE] 188 | --opsmanagerhost, --omh hostname for Ops Manager [$CFOPS_HOST] 189 | ``` 190 | 191 | --- 192 | 193 | 194 | 195 | ### Usage (cfops v1.x.x) 196 | 197 | For example you can try the various commands, args and flags (and --help documentation) that are currently proposed, such as: 198 | 199 | $ ./cfops -help 200 | 201 | $ ./cfops backup 202 | 203 | $ ./cfops restore 204 | 205 | etc. 206 | 207 | 208 | Sample help output: 209 | ``` 210 | $ ./cfops help backup 211 | NAME: 212 | backup - backup --opsmanagerhost --adminuser --adminpass --opsmanageruser --opsmanagerpass -d --tl 'opsmanager, er' 213 | 214 | USAGE: 215 | command backup [command options] [arguments...] 216 | 217 | DESCRIPTION: 218 | backup a Cloud Foundry deployment, including Ops Manager configuration, databases, and blob store 219 | 220 | OPTIONS: 221 | --tilelist, --tl a csv list of the tiles you would like to run the operation on [$CFOPS_TILE_LIST] 222 | --opsmanagerhost, --omh hostname for Ops Manager [$CFOPS_HOST] 223 | --adminuser, --du username for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_USER] 224 | --adminpass, --dp password for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_PASS] 225 | --opsmanageruser, --omu username for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_USER] 226 | --opsmanagerpass, --omp password for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_PASS] 227 | --destination, -d path of the Cloud Foundry backup archive [$CFOPS_BACKUP_PATH] 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | $ ./cfops help restore 236 | NAME: 237 | restore - restore --opsmanagerhost --adminuser --adminpass --opsmanageruser --opsmanagerpass -d --tl 'opsmanager, er' 238 | 239 | USAGE: 240 | command restore [command options] [arguments...] 241 | 242 | DESCRIPTION: 243 | Restore a Cloud Foundry deployment, including Ops Manager configuration, databases, and blob store 244 | 245 | OPTIONS: 246 | --tilelist, --tl a csv list of the tiles you would like to run the operation on [$CFOPS_TILE_LIST] 247 | --opsmanagerhost, --omh hostname for Ops Manager [$CFOPS_HOST] 248 | --adminuser, --du username for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_USER] 249 | --adminpass, --dp password for Ops Mgr admin (Ops Manager WebConsole Credentials) [$CFOPS_ADMIN_PASS] 250 | --opsmanageruser, --omu username for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_USER] 251 | --opsmanagerpass, --omp password for Ops Manager VM Access (used for ssh connections) [$CFOPS_OM_PASS] 252 | --destination, -d path of the Cloud Foundry backup archive [$CFOPS_BACKUP_PATH] 253 | 254 | ``` 255 | -------------------------------------------------------------------------------- /ci/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | resources: 3 | - name: cfops 4 | type: git 5 | source: 6 | branch: develop 7 | private_key: {{git-private-key}} 8 | uri: git@github.com:pivotalservices/cfops.git 9 | - name: london-meta 10 | type: git 11 | source: 12 | branch: master 13 | private_key: {{git-private-key}} 14 | uri: git@github.com:pivotal-cf/london-meta.git 15 | - name: environment-lock-1.10 16 | type: pool 17 | source: 18 | branch: master 19 | pool: aws-1.10-envs 20 | private_key: {{git-private-key}} 21 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 22 | - name: environment-lock-1.9 23 | type: pool 24 | source: 25 | branch: master 26 | pool: aws-1.9-envs 27 | private_key: {{git-private-key}} 28 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 29 | - name: environment-lock-1.8 30 | type: pool 31 | source: 32 | branch: master 33 | pool: aws-1.8-envs 34 | private_key: {{git-private-key}} 35 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 36 | - name: environment-lock-1.7 37 | type: pool 38 | source: 39 | branch: master 40 | pool: aws-1.7-envs 41 | private_key: {{git-private-key}} 42 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 43 | - name: environment-lock-1.6 44 | type: pool 45 | source: 46 | branch: master 47 | pool: aws-1.6-envs 48 | private_key: {{git-private-key}} 49 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 50 | - name: environment-lock-vsphere-1.6 51 | type: pool 52 | source: 53 | branch: master 54 | pool: vsphere-1.6-multiaz 55 | private_key: {{git-private-key}} 56 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 57 | - name: environment-lock-vsphere-1.7 58 | type: pool 59 | source: 60 | branch: master 61 | pool: vsphere-1.7-envs 62 | private_key: {{git-private-key}} 63 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 64 | - name: environment-lock-vsphere-1.8 65 | type: pool 66 | source: 67 | branch: master 68 | pool: vsphere-1.8-envs 69 | private_key: {{git-private-key}} 70 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 71 | - name: environment-lock-vsphere-1.9 72 | type: pool 73 | source: 74 | branch: master 75 | pool: vsphere-1.9-envs 76 | private_key: {{git-private-key}} 77 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 78 | - name: environment-lock-vsphere-1.10 79 | type: pool 80 | source: 81 | branch: master 82 | pool: vsphere-1.10-envs 83 | private_key: {{git-private-key}} 84 | uri: git@github.com:pivotal-cf-experimental/london-services-locks.git 85 | 86 | 87 | jobs: 88 | - name: unit 89 | plan: 90 | - get: cfops 91 | trigger: true 92 | - task: unit 93 | file: cfops/ci/tasks/unit.yml 94 | 95 | - name: integration 96 | plan: 97 | - get: cfops 98 | trigger: true 99 | passed: [unit] 100 | - task: integration 101 | file: cfops/ci/tasks/integration.yml 102 | 103 | - name: system-aws-1.7 104 | plan: 105 | - aggregate: 106 | - put: environment-lock 107 | resource: environment-lock-1.7 108 | params: { acquire: true } 109 | - get: london-meta 110 | - get: cfops 111 | trigger: true 112 | passed: [integration] 113 | - task: system 114 | file: cfops/ci/tasks/system.yml 115 | params: 116 | AWS_ACCESS_KEY_ID: {{cfops-aws-access-key-id}} 117 | AWS_SECRET_ACCESS_KEY: {{cfops-aws-secret-access-key}} 118 | AWS_SECURITY_GROUP: {{cfops-aws-security-group}} 119 | OPSMAN_AMI: ami-f9e3ae8a # 1.7.16 120 | IAAS: aws 121 | ensure: 122 | put: environment-lock 123 | resource: environment-lock-1.7 124 | params: { release: environment-lock } 125 | 126 | - name: system-aws-1.8 127 | plan: 128 | - aggregate: 129 | - put: environment-lock 130 | resource: environment-lock-1.8 131 | params: { acquire: true } 132 | - get: london-meta 133 | - get: cfops 134 | trigger: true 135 | passed: [integration] 136 | - task: system 137 | file: cfops/ci/tasks/system.yml 138 | params: 139 | AWS_ACCESS_KEY_ID: {{cfops-aws-access-key-id}} 140 | AWS_SECRET_ACCESS_KEY: {{cfops-aws-secret-access-key}} 141 | AWS_SECURITY_GROUP: {{cfops-aws-security-group}} 142 | OPSMAN_AMI: ami-841b70f7 # OpsMan pivotal-ops-manager-v1.8-RC2 143 | IAAS: aws 144 | ensure: 145 | put: environment-lock 146 | resource: environment-lock-1.8 147 | params: { release: environment-lock } 148 | 149 | - name: system-aws-1.9 150 | plan: 151 | - aggregate: 152 | - put: environment-lock 153 | resource: environment-lock-1.9 154 | params: { acquire: true } 155 | - get: london-meta 156 | - get: cfops 157 | trigger: true 158 | passed: [integration] 159 | - task: system 160 | file: cfops/ci/tasks/system.yml 161 | params: 162 | AWS_ACCESS_KEY_ID: {{cfops-aws-access-key-id}} 163 | AWS_SECRET_ACCESS_KEY: {{cfops-aws-secret-access-key}} 164 | AWS_SECURITY_GROUP: {{cfops-aws-security-group}} 165 | OPSMAN_AMI: ami-4ce5d02a # OM 1.9.7 166 | IAAS: aws 167 | ensure: 168 | put: environment-lock 169 | resource: environment-lock-1.9 170 | params: { release: environment-lock } 171 | 172 | - name: system-aws-1.10 173 | plan: 174 | - aggregate: 175 | - put: environment-lock 176 | resource: environment-lock-1.10 177 | params: { acquire: true } 178 | - get: london-meta 179 | - get: cfops 180 | trigger: true 181 | passed: [integration] 182 | - task: system 183 | file: cfops/ci/tasks/system.yml 184 | params: 185 | AWS_ACCESS_KEY_ID: {{cfops-aws-access-key-id}} 186 | AWS_SECRET_ACCESS_KEY: {{cfops-aws-secret-access-key}} 187 | AWS_SECURITY_GROUP: {{cfops-aws-security-group}} 188 | OPSMAN_AMI: ami-4ff3ca29 # 1.10.3 189 | IAAS: aws 190 | ensure: 191 | put: environment-lock 192 | resource: environment-lock-1.10 193 | params: { release: environment-lock } 194 | 195 | - name: system-aws-1.6 196 | plan: 197 | - aggregate: 198 | - put: environment-lock 199 | resource: environment-lock-1.6 200 | params: { acquire: true } 201 | - get: london-meta 202 | - get: cfops 203 | trigger: true 204 | passed: [integration] 205 | - task: system 206 | file: cfops/ci/tasks/system.yml 207 | params: 208 | AWS_ACCESS_KEY_ID: {{cfops-aws-access-key-id}} 209 | AWS_SECRET_ACCESS_KEY: {{cfops-aws-secret-access-key}} 210 | AWS_SECURITY_GROUP: {{cfops-aws-security-group}} 211 | OPSMAN_AMI: ami-97bff1e4 # 1.6.25 212 | IAAS: aws 213 | OM_VERSION: 1.6 214 | ensure: 215 | put: environment-lock 216 | resource: environment-lock-1.6 217 | params: { release: environment-lock } 218 | 219 | - name: system-vsphere-1.6 220 | plan: 221 | - aggregate: 222 | - put: environment-lock 223 | resource: environment-lock-vsphere-1.6 224 | params: { acquire: true } 225 | - get: london-meta 226 | - get: cfops 227 | trigger: true 228 | passed: [integration] 229 | - task: system 230 | tags: [vsphere] 231 | file: cfops/ci/tasks/system.yml 232 | params: 233 | ONLY_ERT: true 234 | IAAS: vsphere 235 | OM_VERSION: 1.6 236 | ensure: 237 | put: environment-lock 238 | resource: environment-lock-vsphere-1.6 239 | params: { release: environment-lock } 240 | 241 | - name: system-vsphere-1.7 242 | plan: 243 | - aggregate: 244 | - put: environment-lock 245 | resource: environment-lock-vsphere-1.7 246 | params: { acquire: true } 247 | - get: london-meta 248 | - get: cfops 249 | trigger: true 250 | passed: [integration] 251 | - task: system 252 | tags: [vsphere] 253 | file: cfops/ci/tasks/system.yml 254 | params: 255 | ONLY_ERT: true 256 | IAAS: vsphere 257 | ensure: 258 | put: environment-lock 259 | resource: environment-lock-vsphere-1.7 260 | params: { release: environment-lock } 261 | 262 | - name: system-vsphere-1.8 263 | plan: 264 | - aggregate: 265 | - put: environment-lock 266 | resource: environment-lock-vsphere-1.8 267 | params: { acquire: true } 268 | - get: london-meta 269 | - get: cfops 270 | trigger: true 271 | passed: [integration] 272 | - task: system 273 | tags: [vsphere] 274 | file: cfops/ci/tasks/system.yml 275 | params: 276 | ONLY_ERT: true 277 | IAAS: vsphere 278 | ensure: 279 | put: environment-lock 280 | resource: environment-lock-vsphere-1.8 281 | params: { release: environment-lock } 282 | 283 | - name: system-vsphere-1.9 284 | plan: 285 | - aggregate: 286 | - put: environment-lock 287 | resource: environment-lock-vsphere-1.9 288 | params: { acquire: true } 289 | - get: london-meta 290 | - get: cfops 291 | trigger: true 292 | passed: [integration] 293 | - task: system 294 | tags: [vsphere] 295 | file: cfops/ci/tasks/system.yml 296 | params: 297 | ONLY_ERT: true 298 | IAAS: vsphere 299 | ensure: 300 | put: environment-lock 301 | resource: environment-lock-vsphere-1.9 302 | params: { release: environment-lock } 303 | 304 | - name: system-vsphere-1.10 305 | plan: 306 | - aggregate: 307 | - put: environment-lock 308 | resource: environment-lock-vsphere-1.10 309 | params: { acquire: true } 310 | - get: london-meta 311 | - get: cfops 312 | trigger: true 313 | passed: [integration] 314 | - task: system 315 | tags: [vsphere] 316 | file: cfops/ci/tasks/system.yml 317 | params: 318 | ONLY_ERT: true 319 | IAAS: vsphere 320 | ensure: 321 | put: environment-lock 322 | resource: environment-lock-vsphere-1.10 323 | params: { release: environment-lock } 324 | -------------------------------------------------------------------------------- /ci/scripts/integration: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | service ssh start 3 | 4 | export GOPATH=$PWD 5 | export PATH=$PATH:$GOPATH/bin 6 | 7 | export GO15VENDOREXPERIMENT=1 8 | 9 | mkdir -p /var/vcap/store 10 | 11 | go get github.com/onsi/ginkgo/ginkgo 12 | go get github.com/onsi/gomega 13 | 14 | go get github.com/Masterminds/glide 15 | pushd src/github.com/Masterminds/glide 16 | make install 17 | popd 18 | 19 | cd src/github.com/pivotalservices/cfops 20 | glide install 21 | LOG_LEVEL=debug ginkgo integration/ 22 | -------------------------------------------------------------------------------- /ci/scripts/system: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | export GOPATH=$PWD 4 | export PATH=$PATH:$GOPATH/bin 5 | 6 | export ENV_NAME 7 | export ENV_METADATA 8 | export OM_SSH_KEY 9 | export OM_USER 10 | export OM_PASSWORD 11 | export OM_HOSTNAME 12 | export CF_API_URL 13 | export OM_PROXY_INFO 14 | export OM_VERSION 15 | 16 | # if [ -z "$AWS_ACCESS_KEY_ID" ]; then 17 | # echo "Need to set AWS_ACCESS_KEY_ID" 18 | # exit 1 19 | # fi 20 | # 21 | # if [ -z "$AWS_SECRET_ACCESS_KEY" ]; then 22 | # echo "Need to set AWS_SECRET_ACCESS_KEY" 23 | # exit 1 24 | # fi 25 | 26 | if [ -z "$IAAS" ]; then 27 | echo "Need to set IAAS" 28 | exit 1 29 | fi 30 | 31 | ENV_NAME=$(cat environment-lock/name) 32 | ENV_METADATA=$(cat environment-lock/metadata) 33 | OM_PROXY_INFO=$(echo "$ENV_METADATA" | jq -r .proxy) 34 | OM_SSH_KEY=$(cat london-meta/"$IAAS"-environments/"$ENV_NAME"/"$ENV_NAME"-pcf.pem) 35 | OM_USER=$(echo "$ENV_METADATA" | jq -r .tempest.username) 36 | OM_PASSWORD=$(echo "$ENV_METADATA" | jq -r .tempest.password) 37 | OM_HOSTNAME=$(echo "$ENV_METADATA" | jq -r .tempest.url | cut -d '/' -f 3) 38 | CF_API_URL=$(echo "$ENV_METADATA" | jq -r .tempest.url | cut -d '/' -f 3 | sed 's/pcf/api/') 39 | 40 | export GO15VENDOREXPERIMENT=1 41 | 42 | go get github.com/onsi/ginkgo/ginkgo 43 | go get github.com/onsi/gomega 44 | 45 | go get github.com/Masterminds/glide 46 | pushd src/github.com/Masterminds/glide 47 | make install 48 | popd 49 | 50 | cd src/github.com/pivotalservices/cfops 51 | glide install 52 | LOG_LEVEL=debug ginkgo -v system/ 53 | -------------------------------------------------------------------------------- /ci/scripts/unit: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eux 2 | 3 | export GOPATH=$PWD 4 | export PATH=$PATH:$GOPATH/bin 5 | export GO15VENDOREXPERIMENT=1 6 | 7 | go get github.com/Masterminds/glide 8 | go get github.com/onsi/ginkgo/ginkgo 9 | go get github.com/onsi/gomega 10 | 11 | pushd src/github.com/Masterminds/glide 12 | make install 13 | popd 14 | 15 | cd src/github.com/pivotalservices/cfops 16 | glide install 17 | 18 | go test $(glide novendor | grep 'cmd\|plugin') -v race 19 | ginkgo -dryRun system/ 20 | -------------------------------------------------------------------------------- /ci/set-pipeline.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | pushd $(dirname $0) 4 | 5 | SSH_KEY=$(lpass show "Shared-London Services"/london-ci/git-ssh-key --notes) 6 | 7 | lpass show Shared-PCF-Backup-and-Restore/concourse-secrets --notes > \ 8 | secrets.yml 9 | 10 | fly -t london set-pipeline \ 11 | --pipeline cfops \ 12 | --config pipeline.yml \ 13 | --load-vars-from secrets.yml \ 14 | --var git-private-key="${SSH_KEY}" 15 | 16 | popd 17 | -------------------------------------------------------------------------------- /ci/tasks/integration.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: cloudfoundrylondon/cfops 7 | inputs: 8 | - name: cfops 9 | path: src/github.com/pivotalservices/cfops 10 | run: 11 | path: src/github.com/pivotalservices/cfops/ci/scripts/integration 12 | -------------------------------------------------------------------------------- /ci/tasks/system.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: cloudfoundrylondon/cfops 7 | inputs: 8 | - name: environment-lock 9 | - name: london-meta 10 | - name: cfops 11 | path: src/github.com/pivotalservices/cfops 12 | params: 13 | IAAS: 14 | run: 15 | path: src/github.com/pivotalservices/cfops/ci/scripts/system 16 | -------------------------------------------------------------------------------- /ci/tasks/unit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: cloudfoundrylondon/cfops 7 | inputs: 8 | - name: cfops 9 | path: src/github.com/pivotalservices/cfops 10 | run: 11 | path: src/github.com/pivotalservices/cfops/ci/scripts/unit 12 | -------------------------------------------------------------------------------- /cmd/cfops/createCliCommand.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/codegangsta/cli" 9 | "github.com/pivotalservices/cfbackup" 10 | "github.com/pivotalservices/cfbackup/tileregistry" 11 | 12 | "github.com/xchapter7x/lo" 13 | ) 14 | 15 | //CreateBURACliCommand - this will create a cli command object for backup / restore 16 | func CreateBURACliCommand(name string, usage string, eh *ErrorHandler) (command cli.Command) { 17 | desc := fmt.Sprintf("%s --opsmanagerhost --adminuser --adminpass --opsmanageruser --opsmanagerpass --omr -d --tile elastic-runtime", name) 18 | command = cli.Command{ 19 | Name: name, 20 | Usage: usage, 21 | Description: desc, 22 | Flags: buraFlags, 23 | Action: buraAction(name, eh), 24 | } 25 | return 26 | } 27 | 28 | func buraAction(commandName string, eh *ErrorHandler) (action func(*cli.Context) error) { 29 | action = func(c *cli.Context) error { 30 | var ( 31 | fs = &flagSet{ 32 | host: c.String(flagList[opsManagerHost].Flag[0]), 33 | adminToken: c.String(flagList[adminToken].Flag[0]), 34 | adminUser: c.String(flagList[adminUser].Flag[0]), 35 | adminPass: c.String(flagList[adminPass].Flag[0]), 36 | opsManagerUser: c.String(flagList[opsManagerUser].Flag[0]), 37 | opsManagerPass: c.String(flagList[opsManagerPass].Flag[0]), 38 | opsManagerPassphrase: c.String(flagList[opsManagerPassphrase].Flag[0]), 39 | clientID: c.String(flagList[clientID].Flag[0]), 40 | clientSecret: c.String(flagList[clientSecret].Flag[0]), 41 | dest: c.String(flagList[dest].Flag[0]), 42 | tile: c.String(flagList[tile].Flag[0]), 43 | encryptionKey: c.String(flagList[encryptionKey].Flag[0]), 44 | clearBoshManifest: c.Bool(flagList[clearBoshManifest].Flag[0]), 45 | pluginArgs: c.String(flagList[pluginArgs].Flag[0]), 46 | nfs: c.String(flagList[nfs].Flag[0]), 47 | } 48 | ) 49 | if tileCloser, err := getTileFromRegistry(fs, commandName); err == nil { 50 | defer tileCloser.Close() 51 | if err = runTileAction(commandName, tileCloser); err != nil { 52 | lo.G.Errorf("there was an error: %s running %s on %s tile:%v", err.Error(), commandName, fs.Tile(), tile) 53 | exitOnError(eh, c, commandName, err) 54 | return err 55 | } 56 | } else { 57 | lo.G.Errorf("there was an error getting tile from registry: %s", err.Error()) 58 | exitOnError(eh, c, commandName, err) 59 | return err 60 | } 61 | lo.G.Debug("Tile action completed successfully") 62 | return nil 63 | } 64 | return 65 | } 66 | 67 | func exitOnError(eh *ErrorHandler, c *cli.Context, commandName string, err error) { 68 | cli.ShowCommandHelp(c, commandName) 69 | eh.ExitCode = helpExitCode 70 | eh.Error = err 71 | } 72 | 73 | func runTileAction(commandName string, tile tileregistry.Tile) (err error) { 74 | lo.G.Debugf("Running %s for tile: %#v", commandName, tile) 75 | switch commandName { 76 | case "backup": 77 | err = tile.Backup() 78 | case "restore": 79 | err = tile.Restore() 80 | } 81 | return 82 | } 83 | 84 | func getTileFromRegistry(fs *flagSet, commandName string) (tileCloser tileregistry.TileCloser, err error) { 85 | lo.G.Debugf("checking registry for '%s' tile", fs.Tile()) 86 | 87 | if tileBuilder, ok := tileregistry.GetRegistry()[fs.Tile()]; ok { 88 | lo.G.Debug("found tile in registry") 89 | 90 | if hasValidBackupRestoreFlags(fs) { 91 | lo.G.Debug("we have all required flags and a proper builder") 92 | ts := tileregistry.TileSpec{ 93 | OpsManagerHost: fs.Host(), 94 | AdminUser: fs.AdminUser(), 95 | AdminPass: fs.AdminPass(), 96 | AdminToken: fs.AdminToken(), 97 | OpsManagerUser: fs.OpsManagerUser(), 98 | OpsManagerPass: fs.OpsManagerPass(), 99 | OpsManagerPassphrase: fs.OpsManagerPassphrase(), 100 | ClientID: fs.ClientID(), 101 | ClientSecret: fs.ClientSecret(), 102 | ArchiveDirectory: fs.Dest(), 103 | CryptKey: fs.EncryptionKey(), 104 | ClearBoshManifest: fs.ClearBoshManifest(), 105 | PluginArgs: fs.PluginArgs(), 106 | NFS: fs.NFS(), 107 | } 108 | tileCloser, err = tileBuilder.New(ts) 109 | if err != nil { 110 | return nil, fmt.Errorf("failure to connect to ops manager host: %s", err.Error()) 111 | } 112 | 113 | } else { 114 | err = ErrInvalidFlagArgs 115 | } 116 | 117 | } else { 118 | lo.G.Errorf("tile '%s' not found", fs.Tile()) 119 | err = ErrInvalidTileSelection 120 | } 121 | return 122 | } 123 | 124 | var buraFlags = func() (flags []cli.Flag) { 125 | for _, v := range flagList { 126 | flags = append(flags, cli.StringFlag{ 127 | Name: strings.Join(v.Flag, ", "), 128 | Value: v.DefaultValue, 129 | Usage: v.Desc, 130 | EnvVar: v.EnvVar, 131 | }) 132 | } 133 | return 134 | }() 135 | 136 | const ( 137 | errExitCode = 1 138 | helpExitCode = 2 139 | cleanExitCode = 0 140 | opsManagerHost string = "opsmanagerHost" 141 | adminToken string = "adminToken" 142 | adminUser string = "adminUser" 143 | adminPass string = "adminPass" 144 | opsManagerUser string = "opsManagerUser" 145 | opsManagerPass string = "opsManagerPass" 146 | opsManagerPassphrase string = "opsManagerPassphrase" 147 | clientID string = "clientID" 148 | clientSecret string = "clientSecret" 149 | dest string = "destination" 150 | tile string = "tile" 151 | encryptionKey string = "encryptionKey" 152 | clearBoshManifest string = "clearboshmanifest" 153 | pluginArgs string = "pluginArgs" 154 | nfs string = "nfs" 155 | ) 156 | 157 | var ( 158 | //ErrInvalidFlagArgs - error for invalid flags 159 | ErrInvalidFlagArgs = errors.New("invalid cli flag args") 160 | //ErrInvalidTileSelection - error for invalid tile 161 | ErrInvalidTileSelection = errors.New("invalid tile selected. try the 'list-tiles' option to see a list of available tiles") 162 | flagList = map[string]flagBucket{ 163 | nfs: flagBucket{ 164 | Flag: []string{"nfs"}, 165 | Desc: "options are 'lite' (skips optional parts of blobstore), 'full' (backs up whole blobstore) or 'bp' (only backs up buildpacks). This will only apply to elastic-runtime. Defaults to 'full'", 166 | DefaultValue: "full", 167 | EnvVar: "NFS_BACKUP", 168 | }, 169 | opsManagerHost: flagBucket{ 170 | Flag: []string{"opsmanagerhost", "omh"}, 171 | Desc: "hostname for Ops Manager", 172 | EnvVar: "CFOPS_HOST", 173 | }, 174 | adminToken: flagBucket{ 175 | Flag: []string{"admintoken", "dt"}, 176 | Desc: "Ops Mgr OAuth admin token (not required if adminuser and adminpass are provided)", 177 | EnvVar: "CFOPS_ADMIN_TOKEN", 178 | }, 179 | adminUser: flagBucket{ 180 | Flag: []string{"adminuser", "du"}, 181 | Desc: "username for Ops Mgr admin (Ops Manager WebConsole Credentials)", 182 | EnvVar: "CFOPS_ADMIN_USER", 183 | }, 184 | adminPass: flagBucket{ 185 | Flag: []string{"adminpass", "dp"}, 186 | Desc: "password for Ops Mgr admin (Ops Manager WebConsole Credentials)", 187 | EnvVar: "CFOPS_ADMIN_PASS", 188 | }, 189 | opsManagerUser: flagBucket{ 190 | Flag: []string{"opsmanageruser", "omu"}, 191 | Desc: "username for Ops Manager VM Access (used for ssh connections)", 192 | EnvVar: "CFOPS_OM_USER", 193 | }, 194 | opsManagerPass: flagBucket{ 195 | Flag: []string{"opsmanagerpass", "omp"}, 196 | Desc: "password for Ops Manager VM Access (used for ssh connections)", 197 | EnvVar: "CFOPS_OM_PASS", 198 | }, 199 | opsManagerPassphrase: flagBucket{ 200 | Flag: []string{"opsmanagerpassphrase", "omr"}, 201 | Desc: "passphrase is used by Ops Manager 1.7 and above to decrypt the installation files during restore", 202 | EnvVar: "CFOPS_OM_PASSPHRASE", 203 | }, 204 | clientID: flagBucket{ 205 | Flag: []string{"clientid", "cid"}, 206 | Desc: "client ID if using a UAA client instead of a UAA user", 207 | EnvVar: "CFOPS_CLIENT_ID", 208 | }, 209 | clientSecret: flagBucket{ 210 | Flag: []string{"clientsecret", "cis"}, 211 | Desc: "client secret if using a UAA client instead of a UAA user", 212 | EnvVar: "CFOPS_CLIENT_SECRET", 213 | }, 214 | dest: flagBucket{ 215 | Flag: []string{"destination", "d"}, 216 | Desc: "path of the Cloud Foundry archive", 217 | EnvVar: "CFOPS_DEST_PATH", 218 | }, 219 | tile: flagBucket{ 220 | Flag: []string{"tile", "t"}, 221 | Desc: "a tile you would like to run the operation on", 222 | EnvVar: "CFOPS_TILE", 223 | }, 224 | encryptionKey: flagBucket{ 225 | Flag: []string{"encryptionkey", "k"}, 226 | Desc: "encryption key to encrypt/decrypt your archive (key lengths supported are 16, 24, 32 for AES-128, AES-192, or AES-256)", 227 | EnvVar: "CFOPS_ENCRYPTION_KEY", 228 | }, 229 | clearBoshManifest: flagBucket{ 230 | Flag: []string{"clear-bosh-manifest"}, 231 | Desc: "set this flag if you would like to clear the bosh-deployments.yml (this should only affect a restore of Ops-Manager)", 232 | EnvVar: "CFOPS_CLEAR_BOSH_MANIFEST", 233 | }, 234 | pluginArgs: flagBucket{ 235 | Flag: []string{"pluginargs", "p"}, 236 | Desc: "Arguments for plugin to execute", 237 | EnvVar: "CFOPS_PLUGIN_ARGS", 238 | }, 239 | } 240 | ) 241 | 242 | type ( 243 | flagSet struct { 244 | host string 245 | adminToken string 246 | adminUser string 247 | adminPass string 248 | opsManagerUser string 249 | opsManagerPass string 250 | opsManagerPassphrase string 251 | clientID string 252 | clientSecret string 253 | dest string 254 | tile string 255 | encryptionKey string 256 | clearBoshManifest bool 257 | pluginArgs string 258 | nfs string 259 | } 260 | 261 | flagBucket struct { 262 | Flag []string 263 | Desc string 264 | EnvVar string 265 | DefaultValue string 266 | } 267 | ) 268 | 269 | func (s *flagSet) NFS() string { 270 | return s.nfs 271 | } 272 | 273 | func (s *flagSet) Host() string { 274 | return s.host 275 | } 276 | 277 | func (s *flagSet) AdminUser() string { 278 | return s.adminUser 279 | } 280 | 281 | func (s *flagSet) AdminToken() string { 282 | return s.adminToken 283 | } 284 | 285 | func (s *flagSet) AdminPass() string { 286 | return s.adminPass 287 | } 288 | 289 | func (s *flagSet) OpsManagerUser() string { 290 | return s.opsManagerUser 291 | } 292 | 293 | func (s *flagSet) OpsManagerPass() string { 294 | return s.opsManagerPass 295 | } 296 | 297 | func (s *flagSet) OpsManagerPassphrase() string { 298 | return s.opsManagerPassphrase 299 | } 300 | 301 | func (s *flagSet) ClientID() string { 302 | return s.clientID 303 | } 304 | 305 | func (s *flagSet) ClientSecret() string { 306 | return s.clientSecret 307 | } 308 | 309 | func (s *flagSet) Dest() string { 310 | return s.dest 311 | } 312 | 313 | func (s *flagSet) Tile() string { 314 | return s.tile 315 | } 316 | 317 | func (s *flagSet) EncryptionKey() string { 318 | return s.encryptionKey 319 | } 320 | 321 | func (s *flagSet) ClearBoshManifest() bool { 322 | return s.clearBoshManifest 323 | } 324 | 325 | func (s *flagSet) PluginArgs() string { 326 | return s.pluginArgs 327 | } 328 | 329 | func hasValidBackupRestoreFlags(fs *flagSet) bool { 330 | res := (fs.Host() != "" && 331 | fs.OpsManagerUser() != "" && 332 | fs.Dest() != "" && 333 | fs.Tile() != "" && 334 | validateAuth(fs) && 335 | validateNfsType(fs.NFS())) 336 | 337 | if res == false { 338 | lo.G.Debug("OpsManagerHost: ", fs.Host()) 339 | lo.G.Debug("AdminUser: ", fs.AdminUser()) 340 | lo.G.Debug("AdminPass: ", fs.AdminPass()) 341 | lo.G.Debug("OpsManagerUser: ", fs.OpsManagerUser()) 342 | lo.G.Debug("OpsManagerPass: ", fs.OpsManagerPass()) 343 | lo.G.Debug("Destination: ", fs.Dest()) 344 | } 345 | return res 346 | } 347 | 348 | func validateAuth(fs *flagSet) bool { 349 | return onlyUserAndPassIsSet(fs) || 350 | onlyTokenIsSet(fs) || 351 | onlyClientIDAndSecretIsSet(fs) 352 | } 353 | 354 | func onlyUserAndPassIsSet(fs *flagSet) bool { 355 | return userAndPassIsSet(fs) && !tokenIsSet(fs) && !clientIDAndSecretIsSet(fs) 356 | } 357 | 358 | func onlyTokenIsSet(fs *flagSet) bool { 359 | return !userAndPassIsSet(fs) && tokenIsSet(fs) && !clientIDAndSecretIsSet(fs) 360 | } 361 | 362 | func onlyClientIDAndSecretIsSet(fs *flagSet) bool { 363 | return !userAndPassIsSet(fs) && !tokenIsSet(fs) && clientIDAndSecretIsSet(fs) 364 | } 365 | 366 | func userAndPassIsSet(fs *flagSet) bool { 367 | return fs.AdminPass() != "" && fs.AdminUser() != "" 368 | } 369 | 370 | func tokenIsSet(fs *flagSet) bool { 371 | return fs.AdminToken() != "" 372 | } 373 | 374 | func clientIDAndSecretIsSet(fs *flagSet) bool { 375 | return fs.ClientID() != "" && fs.ClientSecret() != "" 376 | } 377 | 378 | func validateNfsType(nfsflag string) bool { 379 | return nfsflag == cfbackup.NFSBackupTypeBP || nfsflag == cfbackup.NFSBackupTypeLite || nfsflag == cfbackup.NFSBackupTypeFull 380 | } 381 | -------------------------------------------------------------------------------- /cmd/cfops/createCliCommand_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | 7 | "github.com/codegangsta/cli" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | "github.com/pivotalservices/cfbackup/tileregistry" 12 | "github.com/pivotalservices/cfbackup/tileregistry/fake" 13 | . "github.com/pivotalservices/cfops/cmd/cfops" 14 | ) 15 | 16 | var _ = Describe("given a CreateBURACliCommand func", func() { 17 | testTileAction("backup") 18 | testTileAction("restore") 19 | }) 20 | 21 | func testTileAction(actionName string) { 22 | var ( 23 | controlErrorHandler = new(ErrorHandler) 24 | ) 25 | 26 | Context(fmt.Sprintf("when called with the %s command name", actionName), func() { 27 | controlName := actionName 28 | controlUsage := "something about the usage here" 29 | 30 | It("Then it should return a cli.Command with required values", func() { 31 | cmd := CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 32 | Ω(cmd.Name).Should(Equal(controlName)) 33 | Ω(cmd.Usage).Should(Equal(controlUsage)) 34 | Ω(cmd.Action).ShouldNot(BeNil()) 35 | }) 36 | 37 | Describe(fmt.Sprintf("given a %s command with an Action value", actionName), func() { 38 | var ( 39 | controlExit = 0 40 | controlCliContext *cli.Context 41 | controlCmd cli.Command 42 | ) 43 | BeforeEach(func() { 44 | controlErrorHandler = new(ErrorHandler) 45 | controlErrorHandler.ExitCode = controlExit 46 | }) 47 | 48 | Context("when the action is called with all proper flags on a registered tile", func() { 49 | var ( 50 | controlTileName = "fake-tile" 51 | controlTileGenerator *fake.TileGenerator 52 | controlTile *fake.Tile 53 | controlCloser *fake.Closer 54 | ) 55 | BeforeEach(func() { 56 | controlTile = new(fake.Tile) 57 | controlCloser = &fake.Closer{Executions: 0} 58 | controlTileGenerator = new(fake.TileGenerator) 59 | controlTileGenerator.TileSpy = controlTile 60 | controlTileGenerator.Closer = controlCloser 61 | tileregistry.Register(controlTileName, controlTileGenerator) 62 | set := flag.NewFlagSet("", 0) 63 | set.String("tile", controlTileName, "") 64 | set.String("opsmanagerhost", "*****", "") 65 | set.String("adminuser", "*****", "") 66 | set.String("adminpass", "*****", "") 67 | set.String("opsmanageruser", "*****", "") 68 | set.String("opsmanagerpass", "*****", "") 69 | set.String("destination", "*****", "") 70 | set.String("pluginargs", "*****", "") 71 | set.String("nfs", "full", "") 72 | controlCliContext = cli.NewContext(cli.NewApp(), set, nil) 73 | controlCmd = CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 74 | controlCliContext.Command = controlCmd 75 | }) 76 | It("then it should execute a call on the tiles Action func()", func() { 77 | controlCmd.Action.(func(*cli.Context) error)(controlCliContext) 78 | Ω(controlErrorHandler.ExitCode).Should(Equal(controlExit)) 79 | Ω(controlErrorHandler.Error).ShouldNot(HaveOccurred()) 80 | Ω(controlCloser.Executions).Should(Equal(1)) 81 | switch controlName { 82 | case "backup": 83 | Ω(controlTile.BackupCallCount).ShouldNot(Equal(0)) 84 | case "restore": 85 | Ω(controlTile.RestoreCallCount).ShouldNot(Equal(0)) 86 | default: 87 | panic("this should never happen") 88 | } 89 | }) 90 | }) 91 | 92 | Context("when the action is called w/o a matching registered tile", func() { 93 | BeforeEach(func() { 94 | set := flag.NewFlagSet(controlName, 0) 95 | set.Bool("myflag", false, "doc") 96 | set.String("otherflag", "hello world", "doc") 97 | controlCliContext = cli.NewContext(cli.NewApp(), set, nil) 98 | controlCmd = CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 99 | controlCliContext.Command = controlCmd 100 | }) 101 | 102 | It("then it should set an error and failure exit code", func() { 103 | controlCmd.Action.(func(*cli.Context) error)(controlCliContext) 104 | Ω(controlErrorHandler.ExitCode).ShouldNot(Equal(controlExit)) 105 | Ω(controlErrorHandler.Error).Should(HaveOccurred()) 106 | Ω(controlErrorHandler.Error).Should(Equal(ErrInvalidTileSelection)) 107 | }) 108 | }) 109 | 110 | Context("when running a tile action returns an error", func() { 111 | var ( 112 | controlTileName = "fake-tile" 113 | controlTileGenerator *fake.TileGenerator 114 | controlTile *fake.Tile 115 | controlCloser *fake.Closer 116 | ) 117 | BeforeEach(func() { 118 | controlTile = new(fake.Tile) 119 | controlCloser = &fake.Closer{Executions: 0} 120 | controlTile.ErrFake = fmt.Errorf("operation failed") 121 | controlTileGenerator = new(fake.TileGenerator) 122 | controlTileGenerator.TileSpy = controlTile 123 | controlTileGenerator.Closer = controlCloser 124 | tileregistry.Register(controlTileName, controlTileGenerator) 125 | set := flag.NewFlagSet("", 0) 126 | set.String("tile", controlTileName, "") 127 | set.String("opsmanagerhost", "*****", "") 128 | set.String("adminuser", "*****", "") 129 | set.String("adminpass", "*****", "") 130 | set.String("opsmanageruser", "*****", "") 131 | set.String("opsmanagerpass", "*****", "") 132 | set.String("destination", "*****", "") 133 | controlCliContext = cli.NewContext(cli.NewApp(), set, nil) 134 | controlCmd = CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 135 | controlCliContext.Command = controlCmd 136 | }) 137 | It("then it should set an error and failure exit code", func() { 138 | controlCmd.Action.(func(*cli.Context) error)(controlCliContext) 139 | Ω(controlErrorHandler.ExitCode).ShouldNot(Equal(controlExit)) 140 | Ω(controlErrorHandler.Error).Should(HaveOccurred()) 141 | }) 142 | }) 143 | 144 | Context("when the action is called w/o proper flags but with a registered tile", func() { 145 | var controlTileName = "fake-tile" 146 | BeforeEach(func() { 147 | tileregistry.Register(controlTileName, new(fake.TileGenerator)) 148 | set := flag.NewFlagSet("", 0) 149 | set.String("tile", controlTileName, "doc") 150 | set.Bool("myflag", false, "doc") 151 | set.String("otherflag", "hello world", "doc") 152 | controlCliContext = cli.NewContext(cli.NewApp(), set, nil) 153 | controlCmd = CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 154 | controlCliContext.Command = controlCmd 155 | }) 156 | It("then it should set an error and failure exit code", func() { 157 | controlCmd.Action.(func(*cli.Context) error)(controlCliContext) 158 | Ω(controlErrorHandler.ExitCode).ShouldNot(Equal(controlExit)) 159 | Ω(controlErrorHandler.Error).Should(HaveOccurred()) 160 | Ω(controlErrorHandler.Error).Should(Equal(ErrInvalidFlagArgs)) 161 | }) 162 | }) 163 | 164 | Context("when a tile builder returns an error", func() { 165 | var ( 166 | controlTileName = "fake-tile" 167 | controlTileGenerator *fake.TileGenerator 168 | controlTile *fake.Tile 169 | ) 170 | BeforeEach(func() { 171 | controlTile = new(fake.Tile) 172 | controlTileGenerator = new(fake.TileGenerator) 173 | controlTileGenerator.TileSpy = controlTile 174 | controlTileGenerator.ErrFake = fmt.Errorf("operation timed out") 175 | tileregistry.Register(controlTileName, controlTileGenerator) 176 | set := flag.NewFlagSet("", 0) 177 | set.String("tile", controlTileName, "") 178 | set.String("opsmanagerhost", "*****", "") 179 | set.String("adminuser", "*****", "") 180 | set.String("adminpass", "*****", "") 181 | set.String("opsmanageruser", "*****", "") 182 | set.String("opsmanagerpass", "*****", "") 183 | set.String("destination", "*****", "") 184 | controlCliContext = cli.NewContext(cli.NewApp(), set, nil) 185 | controlCmd = CreateBURACliCommand(controlName, controlUsage, controlErrorHandler) 186 | controlCliContext.Command = controlCmd 187 | }) 188 | It("then it should set an error and failure exit code", func() { 189 | controlCmd.Action.(func(*cli.Context) error)(controlCliContext) 190 | Ω(controlErrorHandler.ExitCode).ShouldNot(Equal(controlExit)) 191 | Ω(controlErrorHandler.Error).Should(HaveOccurred()) 192 | }) 193 | }) 194 | }) 195 | }) 196 | } 197 | -------------------------------------------------------------------------------- /cmd/cfops/help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | CfopsHelpTemplate = ` 5 | NAME: 6 | {{.Name}} - {{.Usage}} 7 | USAGE: 8 | {{.HelpName}} {{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} 9 | {{if .Version}} 10 | VERSION: 11 | {{.Version}} 12 | {{end}}{{if len .Authors}} 13 | AUTHOR(S): 14 | {{range .Authors}}{{ . }}{{end}} 15 | {{end}}{{if .Commands}} 16 | COMMANDS: 17 | {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} 18 | {{end}}{{end}}{{if .Flags}} 19 | {{end}} 20 | ` 21 | ) 22 | -------------------------------------------------------------------------------- /cmd/cfops/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/codegangsta/cli" 8 | "github.com/pivotalservices/cfbackup/tileregistry" 9 | _ "github.com/pivotalservices/cfbackup/tiles" 10 | _ "github.com/pivotalservices/cfops/plugin/load" 11 | ) 12 | 13 | var ( 14 | VERSION string 15 | ) 16 | 17 | type ErrorHandler struct { 18 | ExitCode int 19 | Error error 20 | } 21 | 22 | func main() { 23 | eh := new(ErrorHandler) 24 | eh.ExitCode = 0 25 | app := NewApp(eh) 26 | app.Run(os.Args) 27 | os.Exit(eh.ExitCode) 28 | } 29 | 30 | // NewApp creates a new cli app 31 | func NewApp(eh *ErrorHandler) *cli.App { 32 | cli.AppHelpTemplate = CfopsHelpTemplate 33 | app := cli.NewApp() 34 | app.Version = VERSION 35 | app.Name = "cfops" 36 | app.Usage = "Cloud Foundry Operations Tool" 37 | app.Commands = []cli.Command{ 38 | cli.Command{ 39 | Name: "version", 40 | Usage: "shows the application version currently in use", 41 | Action: func(c *cli.Context) error { 42 | cli.ShowVersion(c) 43 | return nil 44 | }, 45 | }, 46 | cli.Command{ 47 | Name: "list-tiles", 48 | Usage: "shows a list of available backup/restore target tiles", 49 | Action: func(c *cli.Context) error { 50 | fmt.Println("Available Tiles:") 51 | for n, _ := range tileregistry.GetRegistry() { 52 | fmt.Println(n) 53 | } 54 | return nil 55 | }, 56 | }, 57 | CreateBURACliCommand("backup", "creates a backup archive of the target tile", eh), 58 | CreateBURACliCommand("restore", "restores from an archive to the target tile", eh), 59 | } 60 | return app 61 | } 62 | -------------------------------------------------------------------------------- /cmd/cfops/suite_test.go: -------------------------------------------------------------------------------- 1 | package main_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "test suite") 13 | } 14 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: c090cf39c725222667100156d17a60b27375e8b63b31bba59990e9fea0325f2e 2 | updated: 2017-08-31T10:25:03.126941534+01:00 3 | imports: 4 | - name: code.cloudfoundry.org/lager 5 | version: 0bfa98e49e7a976af91e918d47978f07c00b081f 6 | - name: github.com/andybalholm/cascadia 7 | version: 65919c611220063037b1db8eb334acaf17a6d8ea 8 | - name: github.com/apcera/libretto 9 | version: 715e8fbc15612e82f4e7b082fc6aaeacc461be52 10 | subpackages: 11 | - ssh 12 | - util 13 | - virtualmachine 14 | - virtualmachine/aws 15 | - name: github.com/apcera/util 16 | version: 346d8c10c6f817df60ff768b05ced4170d747180 17 | subpackages: 18 | - uuid 19 | - name: github.com/aws/aws-sdk-go 20 | version: 2063d937ea693f6f5c8b213906785ff9e36ba0b6 21 | subpackages: 22 | - aws 23 | - aws/awserr 24 | - aws/awsutil 25 | - aws/client 26 | - aws/client/metadata 27 | - aws/corehandlers 28 | - aws/credentials 29 | - aws/credentials/ec2rolecreds 30 | - aws/credentials/endpointcreds 31 | - aws/credentials/stscreds 32 | - aws/defaults 33 | - aws/ec2metadata 34 | - aws/endpoints 35 | - aws/request 36 | - aws/session 37 | - aws/signer/v4 38 | - internal/shareddefaults 39 | - private/protocol 40 | - private/protocol/ec2query 41 | - private/protocol/query 42 | - private/protocol/query/queryutil 43 | - private/protocol/rest 44 | - private/protocol/xml/xmlutil 45 | - service/ec2 46 | - service/sts 47 | - name: github.com/bmizerany/pat 48 | version: 6226ea591a40176dd3ff9cd8eff81ed6ca721a00 49 | - name: github.com/cloudfoundry-community/go-cfenv 50 | version: f920e9562d5f951cbf11785728f67258c38a10d0 51 | - name: github.com/cloudfoundry-incubator/cf-test-helpers 52 | version: 2c7deda19c83f1c2cfde730712b1f36aefc6bf33 53 | subpackages: 54 | - cf 55 | - commandreporter 56 | - commandstarter 57 | - internal 58 | - name: github.com/codegangsta/cli 59 | version: f017f86fccc5a039a98f23311f34fdf78b014f78 60 | - name: github.com/concourse/atc 61 | version: bee86f317e5acef80b153a1084372b464eef8307 62 | - name: github.com/enaml-ops/enaml 63 | version: 4f847ee10b41afca41fe09fa839cb2f6ade06fb5 64 | subpackages: 65 | - enamlbosh 66 | - name: github.com/enaml-ops/omg-cli 67 | version: 7a0299e2051cf8f1461d25260771826c15d08a48 68 | - name: github.com/go-ini/ini 69 | version: c787282c39ac1fc618827141a1f762240def08a3 70 | - name: github.com/golang/protobuf 71 | version: 83cd65fc365ace80eb6b6ecfc45203e43edfbc70 72 | subpackages: 73 | - proto 74 | - ptypes 75 | - ptypes/any 76 | - ptypes/duration 77 | - ptypes/timestamp 78 | - name: github.com/hashicorp/errwrap 79 | version: 7554cd9344cec97297fa6649b055a8c98c2a1e55 80 | - name: github.com/hashicorp/go-hclog 81 | version: b4e5765d1e5f00a0550911084f45f8214b5b83b9 82 | - name: github.com/hashicorp/go-multierror 83 | version: 83588e72410abfbe4df460eeb6f30841ae47d4c4 84 | - name: github.com/hashicorp/go-plugin 85 | version: a5174f84d7f8ff00fb07ab4ef1f380d32eee0e63 86 | - name: github.com/hashicorp/yamux 87 | version: d1caa6c97c9fc1cc9e83bbe34d0603f9ff0ce8bd 88 | - name: github.com/jessevdk/go-flags 89 | version: 6cf8f02b4ae8ba723ddc64dcfd403e530c06d927 90 | - name: github.com/jmespath/go-jmespath 91 | version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d 92 | - name: github.com/kr/fs 93 | version: 2788f0dbd16903de03cb8186e5c7d97b69ad387b 94 | - name: github.com/mitchellh/go-testing-interface 95 | version: 9a441910b16872f7b8283682619b3761a9aa2222 96 | - name: github.com/mitchellh/mapstructure 97 | version: d0303fe809921458f417bcf828397a65db30a7e4 98 | - name: github.com/nu7hatch/gouuid 99 | version: 179d4d0c4d8d407a32af483c2354df1d2c91e6c3 100 | - name: github.com/onsi/ginkgo 101 | version: 6adbc2648389a46e8ff0e96d07a07a9a9eb432de 102 | subpackages: 103 | - config 104 | - internal/codelocation 105 | - internal/containernode 106 | - internal/failer 107 | - internal/leafnodes 108 | - internal/remote 109 | - internal/spec 110 | - internal/spec_iterator 111 | - internal/specrunner 112 | - internal/suite 113 | - internal/testingtproxy 114 | - internal/writer 115 | - reporters 116 | - reporters/stenographer 117 | - reporters/stenographer/support/go-colorable 118 | - reporters/stenographer/support/go-isatty 119 | - types 120 | - name: github.com/onsi/gomega 121 | version: 9b8c753e8dfb382618ba8fa19b4197b5dcb0434c 122 | subpackages: 123 | - format 124 | - gbytes 125 | - gexec 126 | - ghttp 127 | - internal/assertion 128 | - internal/asyncassertion 129 | - internal/oraclematcher 130 | - internal/testingtsupport 131 | - matchers 132 | - matchers/support/goraph/bipartitegraph 133 | - matchers/support/goraph/edge 134 | - matchers/support/goraph/node 135 | - matchers/support/goraph/util 136 | - types 137 | - name: github.com/op/go-logging 138 | version: 970db520ece77730c7e4724c61121037378659d9 139 | - name: github.com/pivotal-golang/lager 140 | version: 95b2ada4cdf781bdcf07e5207879e034ac8c5314 141 | - name: github.com/pivotalservices/cfbackup 142 | version: db53f4188a97a8c7436ee5e98f31dd32d6d05458 143 | subpackages: 144 | - tileregistry 145 | - tileregistry/fake 146 | - tiles 147 | - tiles/elasticruntime 148 | - tiles/opsmanager 149 | - tiles/opsmanager/fakes 150 | - name: github.com/pivotalservices/gtils 151 | version: 92fbb15a9e8347eb7cd60fdb1fd80847ae989f34 152 | subpackages: 153 | - command 154 | - http 155 | - log 156 | - osutils 157 | - persistence 158 | - storage 159 | - uaa 160 | - name: github.com/pkg/errors 161 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 162 | - name: github.com/pkg/sftp 163 | version: 8a7343c6ed99a2934aea9c08c923c11df775b6db 164 | - name: github.com/PuerkitoBio/goquery 165 | version: e1271ee34c6a305e38566ecd27ae374944907ee9 166 | - name: github.com/rlmcpherson/s3gof3r 167 | version: b574ee38528c51c2c8652b79e71245817c59bd61 168 | - name: github.com/technoweenie/multipartstreamer 169 | version: a90a01d73ae432e2611d178c18367fbaa13e0154 170 | - name: github.com/tedsuo/rata 171 | version: 88dadd5b7d95869a307d31691591e2e7aea32971 172 | - name: github.com/tmc/scp 173 | version: 8734282bd23b160cc07954a157106fe59e0f3993 174 | - name: github.com/xchapter7x/goutil 175 | version: 046742e03686eda64c6ac938594120d7fb5b3ecc 176 | subpackages: 177 | - itertools 178 | - name: github.com/xchapter7x/lo 179 | version: e33b245fc7a8186582208abc2458c2691bff681c 180 | - name: golang.org/x/crypto 181 | version: 7e9105388ebff089b3f99f0ef676ea55a6da3a7e 182 | subpackages: 183 | - curve25519 184 | - ed25519 185 | - ed25519/internal/edwards25519 186 | - ssh 187 | - name: golang.org/x/net 188 | version: 3da985ce5951d99de868be4385f21ea6c2b22f24 189 | subpackages: 190 | - context 191 | - context/ctxhttp 192 | - html 193 | - html/atom 194 | - http2 195 | - http2/hpack 196 | - idna 197 | - internal/timeseries 198 | - lex/httplex 199 | - trace 200 | - name: golang.org/x/oauth2 201 | version: 9a379c6b3e95a790ffc43293c2a78dee0d7b6e20 202 | subpackages: 203 | - internal 204 | - name: golang.org/x/sys 205 | version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594 206 | subpackages: 207 | - unix 208 | - name: golang.org/x/text 209 | version: 19e51611da83d6be54ddafce4a4af510cb3e9ea4 210 | subpackages: 211 | - secure/bidirule 212 | - transform 213 | - unicode/bidi 214 | - unicode/norm 215 | - name: google.golang.org/appengine 216 | version: d9a072cfa7b9736e44311ef77b3e09d804bfa599 217 | subpackages: 218 | - internal 219 | - internal/base 220 | - internal/datastore 221 | - internal/log 222 | - internal/remote_api 223 | - internal/urlfetch 224 | - urlfetch 225 | - name: google.golang.org/genproto 226 | version: ee236bd376b077c7a89f260c026c4735b195e459 227 | subpackages: 228 | - googleapis/rpc/status 229 | - name: google.golang.org/grpc 230 | version: 25b4a426b40c26c07c80af674b03db90b5bd4a60 231 | subpackages: 232 | - codes 233 | - connectivity 234 | - credentials 235 | - grpclb/messages_only/grpc_lb_v1 236 | - grpclog 237 | - health 238 | - health/grpc_health_v1 239 | - internal 240 | - keepalive 241 | - metadata 242 | - naming 243 | - peer 244 | - stats 245 | - status 246 | - tap 247 | - transport 248 | - name: gopkg.in/yaml.v1 249 | version: 9f9df34309c04878acc86042b16630b0f696e1de 250 | - name: gopkg.in/yaml.v2 251 | version: eb3733d160e74a9c7e442f435eb3bea458e1d19f 252 | testImports: 253 | - name: github.com/pborman/uuid 254 | version: a97ce2ca70fa5a848076093f05e639a89ca34d06 255 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/pivotalservices/cfops 2 | import: 3 | - package: github.com/cloudfoundry-community/go-cfenv 4 | - package: github.com/codegangsta/cli 5 | - package: github.com/hashicorp/go-plugin 6 | - package: github.com/pivotalservices/cfbackup 7 | version: v0.1.158 8 | subpackages: 9 | - tiles 10 | - tiles/opsmanager 11 | - tileregistry 12 | - package: github.com/xchapter7x/lo 13 | - package: github.com/onsi/ginkgo 14 | version: 6adbc2648389a46e8ff0e96d07a07a9a9eb432de 15 | - package: github.com/onsi/gomega 16 | version: 9b8c753e8dfb382618ba8fa19b4197b5dcb0434c 17 | - package: github.com/nu7hatch/gouuid 18 | - package: github.com/golang/protobuf 19 | version: master 20 | subpackages: 21 | - proto 22 | - package: gopkg.in/yaml.v2 23 | - package: github.com/cloudfoundry-incubator/cf-test-helpers 24 | - package: github.com/tmc/scp 25 | - package: github.com/apcera/libretto 26 | version: master 27 | - package: github.com/enaml-ops/omg-cli 28 | version: ~0.0.10 29 | - package: code.cloudfoundry.org/lager 30 | - package: golang.org/x/net 31 | subpackages: 32 | - html 33 | - package: github.com/PuerkitoBio/goquery 34 | version: ^1.0.0 35 | - package: github.com/andybalholm/cascadia 36 | -------------------------------------------------------------------------------- /integration/cfopsintegration_suite_test.go: -------------------------------------------------------------------------------- 1 | package cfopsintegration_test 2 | 3 | import ( 4 | "testing" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | func TestBrokerintegration(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "cfops Integration Suite") 13 | } 14 | -------------------------------------------------------------------------------- /integration/cmd_test.go: -------------------------------------------------------------------------------- 1 | package cfopsintegration_test 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "os/exec" 16 | "os/user" 17 | "path/filepath" 18 | "strings" 19 | "text/template" 20 | "time" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | "github.com/onsi/gomega/gbytes" 25 | "github.com/onsi/gomega/gexec" 26 | "github.com/onsi/gomega/ghttp" 27 | ) 28 | 29 | var cfopsExecutablePath string 30 | var err error 31 | 32 | var executionTimeout = 5 * time.Second 33 | 34 | const ( 35 | help string = `cfops - Cloud Foundry Operations Tool` 36 | ) 37 | 38 | var _ = BeforeSuite(func() { 39 | os.Setenv("LOG_LEVEL", "debug") 40 | cfopsExecutablePath, err = gexec.Build("github.com/pivotalservices/cfops/cmd/cfops") 41 | Expect(err).NotTo(HaveOccurred()) 42 | 43 | Expect(directoryExists("/var/vcap/store")).To(BeTrue(), "need the /var/vcap/store directory to run tests") 44 | }) 45 | 46 | var _ = AfterSuite(func() { 47 | gexec.CleanupBuildArtifacts() 48 | }) 49 | 50 | var _ = Describe("cfops cmd", func() { 51 | Context("bosh director with basic auth", func() { 52 | cfopsWithDirectorConfig(basicAuthDirectorHandlers) 53 | }) 54 | 55 | Context("bosh director with uaa auth", func() { 56 | cfopsWithDirectorConfig(uaaAuthDirectorHandlers) 57 | }) 58 | }) 59 | 60 | const ccVmsResponse = `[ 61 | { 62 | "agent_id":"d4131496-4cdf-4309-907b-e2ce327be029", 63 | "cid":"vm-8dfe3b38-6e31-4d9a-aeef-74cbf2143bd8", 64 | "job":"cloud_controller-partition-7bc61fd2fa9d654696df", 65 | "index":0 66 | } 67 | ]` 68 | 69 | func getTaskResponseOK(id string) string { 70 | return fmt.Sprintf(`{"id": %s, "state": "done", "description":"foobar","result":"send help"}`, id) 71 | } 72 | 73 | func basicAuthDirectorHandlers(directorURL string) []http.HandlerFunc { 74 | const infoResponse = `{"name":"enaml-bosh","uuid":"31631ff9-ac41-4eba-a944-04c820633e7f","version":"1.3232.2.0 (00000000)","user":null,"cpi":"aws_cpi","user_authentication":{"type":"basic","options":{}},"features":{"dns":{"status":false,"extras":{"domain_name":null}},"compiled_package_cache":{"status":false,"extras":{"provider":null}},"snapshots":{"status":false}}}` 75 | 76 | return []http.HandlerFunc{ 77 | ghttp.CombineHandlers( 78 | ghttp.VerifyRequest("GET", "/info"), 79 | ghttp.RespondWith(http.StatusOK, infoResponse), 80 | ), 81 | ghttp.CombineHandlers( 82 | ghttp.VerifyRequest("GET", "/info"), 83 | ghttp.RespondWith(http.StatusOK, infoResponse), 84 | ), 85 | ghttp.CombineHandlers( 86 | ghttp.VerifyRequest("GET", "/info"), 87 | ghttp.RespondWith(http.StatusOK, infoResponse), 88 | ), 89 | ghttp.CombineHandlers( 90 | ghttp.VerifyRequest("GET", "/deployments/cf-f21eea2dbdb8555f89fb/vms"), 91 | ghttp.RespondWith(http.StatusOK, ccVmsResponse), 92 | ), 93 | ghttp.CombineHandlers( 94 | ghttp.VerifyRequest("GET", "/info"), 95 | ghttp.RespondWith(http.StatusOK, infoResponse), 96 | ), 97 | ghttp.CombineHandlers( 98 | ghttp.VerifyRequest("PUT", "/deployments/cf-f21eea2dbdb8555f89fb/jobs/cloud_controller-partition-7bc61fd2fa9d654696df/0", "state=stopped"), 99 | ghttp.VerifyBody([]byte(``)), 100 | ghttp.RespondWith(http.StatusFound, "", http.Header{"Content-Length": []string{"0"}, "Content-Type": []string{"application/json"}, "Location": []string{"/tasks/2"}}), 101 | ), 102 | ghttp.CombineHandlers( 103 | ghttp.VerifyRequest("GET", "/tasks/2"), 104 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("2")), 105 | ), 106 | ghttp.CombineHandlers( 107 | ghttp.VerifyRequest("GET", "/tasks/2"), 108 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("2")), 109 | ), 110 | ghttp.CombineHandlers( 111 | ghttp.VerifyRequest("GET", "/info"), 112 | ghttp.RespondWith(http.StatusOK, infoResponse), 113 | ), 114 | ghttp.CombineHandlers( 115 | ghttp.VerifyRequest("PUT", "/deployments/cf-f21eea2dbdb8555f89fb/jobs/cloud_controller-partition-7bc61fd2fa9d654696df/0", "state=started"), 116 | ghttp.VerifyBody([]byte(``)), 117 | ghttp.RespondWith(http.StatusFound, "", http.Header{"Location": []string{"/tasks/3"}}), 118 | ), 119 | ghttp.CombineHandlers( 120 | ghttp.VerifyRequest("GET", "/tasks/3"), 121 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("3")), 122 | ), 123 | ghttp.CombineHandlers( 124 | ghttp.VerifyRequest("GET", "/tasks/3"), 125 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("3")), 126 | ), 127 | } 128 | } 129 | 130 | func uaaAuthDirectorHandlers(directorURL string) []http.HandlerFunc { 131 | infoResponse := fmt.Sprintf(`{"name":"enaml-bosh","uuid":"9604f9ae-70bf-4c13-8d4d-69ff7f7f091b","version":"1.3232.2.0 (00000000)","user":null,"cpi":"aws_cpi","user_authentication":{"type":"uaa","options":{"url":"%s"}},"features":{"dns":{"status":false,"extras":{"domain_name":null}},"compiled_package_cache":{"status":false,"extras":{"provider":null}},"snapshots":{"status":false}}}`, directorURL) 132 | const tokenResponse = `{ 133 | "access_token":"abcdef01234567890", 134 | "token_type":"bearer", 135 | "refresh_token":"0987654321fedcba", 136 | "expires_in":3599, 137 | "scope":"opsman.user uaa.admin scim.read opsman.admin scim.write", 138 | "jti":"foo" 139 | }` 140 | 141 | return []http.HandlerFunc{ 142 | ghttp.CombineHandlers( 143 | ghttp.VerifyRequest("GET", "/info"), 144 | ghttp.RespondWith(http.StatusOK, infoResponse), 145 | ), 146 | ghttp.CombineHandlers( 147 | ghttp.VerifyRequest("POST", "/oauth/token"), 148 | ghttp.RespondWith(http.StatusOK, tokenResponse, http.Header{ 149 | "Content-Type": []string{"application/json"}}), 150 | ), 151 | ghttp.CombineHandlers( 152 | ghttp.VerifyRequest("GET", "/info"), 153 | ghttp.RespondWith(http.StatusOK, infoResponse), 154 | ), 155 | ghttp.CombineHandlers( 156 | ghttp.VerifyRequest("GET", "/info"), 157 | ghttp.RespondWith(http.StatusOK, infoResponse), 158 | ), 159 | ghttp.CombineHandlers( 160 | ghttp.VerifyRequest("POST", "/oauth/token"), 161 | ghttp.RespondWith(http.StatusOK, tokenResponse, http.Header{ 162 | "Content-Type": []string{"application/json"}}), 163 | ), 164 | ghttp.CombineHandlers( 165 | ghttp.VerifyRequest("GET", "/deployments/cf-f21eea2dbdb8555f89fb/vms"), 166 | ghttp.RespondWith(http.StatusOK, ccVmsResponse), 167 | ), 168 | ghttp.CombineHandlers( 169 | ghttp.VerifyRequest("GET", "/info"), 170 | ghttp.RespondWith(http.StatusOK, infoResponse), 171 | ), 172 | ghttp.CombineHandlers( 173 | ghttp.VerifyRequest("POST", "/oauth/token"), 174 | ghttp.RespondWith(http.StatusOK, tokenResponse, http.Header{ 175 | "Content-Type": []string{"application/json"}}), 176 | ), 177 | ghttp.CombineHandlers( 178 | ghttp.VerifyRequest("PUT", "/deployments/cf-f21eea2dbdb8555f89fb/jobs/cloud_controller-partition-7bc61fd2fa9d654696df/0", "state=stopped"), 179 | ghttp.VerifyBody([]byte(``)), 180 | ghttp.RespondWith(http.StatusFound, "", http.Header{"Content-Length": []string{"0"}, "Content-Type": []string{"application/json"}, "Location": []string{"/tasks/2"}}), 181 | ), 182 | ghttp.CombineHandlers( 183 | ghttp.VerifyRequest("GET", "/tasks/2"), 184 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("2")), 185 | ), 186 | ghttp.CombineHandlers( 187 | ghttp.VerifyRequest("GET", "/tasks/2"), 188 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("2")), 189 | ), 190 | ghttp.CombineHandlers( 191 | ghttp.VerifyRequest("GET", "/info"), 192 | ghttp.RespondWith(http.StatusOK, infoResponse), 193 | ), 194 | ghttp.CombineHandlers( 195 | ghttp.VerifyRequest("POST", "/oauth/token"), 196 | ghttp.RespondWith(http.StatusOK, tokenResponse, http.Header{ 197 | "Content-Type": []string{"application/json"}}), 198 | ), 199 | ghttp.CombineHandlers( 200 | ghttp.VerifyRequest("PUT", "/deployments/cf-f21eea2dbdb8555f89fb/jobs/cloud_controller-partition-7bc61fd2fa9d654696df/0", "state=started"), 201 | ghttp.VerifyBody([]byte(``)), 202 | ghttp.RespondWith(http.StatusFound, "", http.Header{"Location": []string{"/tasks/3"}}), 203 | ), 204 | ghttp.CombineHandlers( 205 | ghttp.VerifyRequest("GET", "/tasks/3"), 206 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("3")), 207 | ), 208 | ghttp.CombineHandlers( 209 | ghttp.VerifyRequest("GET", "/tasks/3"), 210 | ghttp.RespondWith(http.StatusOK, getTaskResponseOK("3")), 211 | ), 212 | } 213 | } 214 | 215 | func cfopsWithDirectorConfig(generateHTTPHandlers func(string) []http.HandlerFunc) { 216 | BeforeEach(func() { 217 | os.RemoveAll("/var/vcap/store/shared") 218 | }) 219 | 220 | It("prints the help page", func() { 221 | command := exec.Command(cfopsExecutablePath) 222 | session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter) 223 | 224 | Expect(err).NotTo(HaveOccurred(), "couldn't run cfops executable") 225 | Eventually(session.Out).Should(gbytes.Say(help)) 226 | }) 227 | var currentUser *user.User 228 | var privateKey string 229 | 230 | BeforeEach(func() { 231 | currentUser, err = user.Current() 232 | Expect(err).NotTo(HaveOccurred()) 233 | var publicKey string 234 | publicKey, privateKey = createSSHKey(currentUser.Name) 235 | addToAuthorizedKeys(currentUser, publicKey) 236 | }) 237 | 238 | AfterEach(func() { 239 | removeKeyFromAuthorized(currentUser) 240 | }) 241 | Describe("authenication methods", func() { 242 | var destinationDirectory string 243 | var opsmanUri *url.URL 244 | var boshDirectorServer *ghttp.Server 245 | BeforeEach(func() { 246 | boshDirectorServer = ghttp.NewUnstartedServer() 247 | boshDirectorServer.HTTPTestServer.Listener, err = net.Listen("tcp", "127.0.0.1:25555") 248 | Expect(err).NotTo(HaveOccurred()) 249 | boshDirectorServer.HTTPTestServer.StartTLS() 250 | directorHandlers := generateHTTPHandlers(boshDirectorServer.URL()) 251 | boshDirectorServer.AppendHandlers(directorHandlers...) 252 | createTestFiles("/var/vcap/store", []string{ 253 | "shared/cc-resources/09/4d/094dc37299e8d0c68e8e22e8f72a7b1632d26cc3", 254 | }) 255 | 256 | destinationDirectory, err = ioutil.TempDir("", "cfops_destination") 257 | Expect(err).NotTo(HaveOccurred()) 258 | }) 259 | AfterEach(func() { 260 | os.RemoveAll(destinationDirectory) 261 | boshDirectorServer.Close() 262 | }) 263 | 264 | Context("with username/password", func() { 265 | var opsmanServer *ghttp.Server 266 | var session *gexec.Session 267 | 268 | BeforeEach(func() { 269 | opsmanServer = ghttp.NewTLSServer() 270 | opsmanServer.AppendHandlers( 271 | ghttp.CombineHandlers( 272 | ghttp.VerifyRequest("POST", "/uaa/oauth/token"), 273 | ghttp.VerifyFormKV("grant_type", "password"), 274 | ghttp.VerifyFormKV("username", ""), 275 | ghttp.VerifyFormKV("password", "SECRET_admin_password"), 276 | ghttp.VerifyFormKV("client_id", "opsman"), 277 | ghttp.VerifyFormKV("client_secret", ""), 278 | ghttp.RespondWith(http.StatusOK, `{"access_token": "MAGIC_TOKEN"}`), 279 | ), 280 | ghttp.CombineHandlers( 281 | ghttp.VerifyRequest("GET", "/api/installation_settings"), 282 | ghttp.VerifyHeaderKV("Authorization", "Bearer MAGIC_TOKEN"), 283 | ghttp.RespondWith(http.StatusOK, readFixture("fixtures/nfs_blobstore_test_installation_settings.json", 284 | struct { 285 | DirectorHost string 286 | NfsServerIP string 287 | NFSSshUser string 288 | NFSSshPassword string 289 | SSHPrivateKey string 290 | }{DirectorHost: "127.0.0.1", NfsServerIP: "127.0.0.1", NFSSshUser: currentUser.Name, NFSSshPassword: "SECRET_nfs_ssh_password", SSHPrivateKey: strings.Replace(privateKey, "\n", "\\n", -1)})), 291 | )) 292 | opsmanUri, err = url.Parse(opsmanServer.URL()) 293 | Expect(err).NotTo(HaveOccurred()) 294 | }) 295 | AfterEach(func() { 296 | opsmanServer.Close() 297 | }) 298 | JustBeforeEach(func() { 299 | command := exec.Command(cfopsExecutablePath, "backup", "--opsmanagerhost", opsmanUri.Host, "--adminuser", "", "--adminpass", "SECRET_admin_password", "--opsmanageruser", "", "-d", destinationDirectory, "--tile", "elastic-runtime") 300 | 301 | session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 302 | Expect(err).ShouldNot(HaveOccurred()) 303 | session.Wait(executionTimeout) 304 | }) 305 | It("should accept the flags", func() { 306 | Expect(session.Err.Contents()).ToNot(ContainSubstring("invalid cli flag args")) 307 | }) 308 | 309 | It("should succeed", func() { 310 | Eventually(session).Should(gexec.Exit(0)) 311 | }) 312 | }) 313 | 314 | Context("with auth token", func() { 315 | var opsmanServer *ghttp.Server 316 | var session *gexec.Session 317 | 318 | BeforeEach(func() { 319 | opsmanServer = ghttp.NewTLSServer() 320 | opsmanServer.AppendHandlers( 321 | ghttp.CombineHandlers( 322 | ghttp.VerifyRequest("GET", "/api/installation_settings"), 323 | ghttp.VerifyHeaderKV("Authorization", "Bearer MAGIC_TOKEN"), 324 | ghttp.RespondWith(http.StatusOK, readFixture("fixtures/nfs_blobstore_test_installation_settings.json", 325 | struct { 326 | DirectorHost string 327 | NfsServerIP string 328 | NFSSshUser string 329 | NFSSshPassword string 330 | SSHPrivateKey string 331 | }{DirectorHost: "127.0.0.1", NfsServerIP: "127.0.0.1", NFSSshUser: currentUser.Name, NFSSshPassword: "SECRET_nfs_ssh_password", SSHPrivateKey: strings.Replace(privateKey, "\n", "\\n", -1)})), 332 | )) 333 | opsmanUri, err = url.Parse(opsmanServer.URL()) 334 | Expect(err).NotTo(HaveOccurred()) 335 | }) 336 | AfterEach(func() { 337 | opsmanServer.Close() 338 | }) 339 | JustBeforeEach(func() { 340 | command := exec.Command(cfopsExecutablePath, "backup", "--opsmanagerhost", opsmanUri.Host, "--admintoken", "MAGIC_TOKEN", "--opsmanageruser", "", "-d", destinationDirectory, "--tile", "elastic-runtime") 341 | 342 | session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 343 | Expect(err).ShouldNot(HaveOccurred()) 344 | session.Wait(executionTimeout) 345 | }) 346 | It("should accept the flags", func() { 347 | Expect(session.Err.Contents()).ToNot(ContainSubstring("invalid cli flag args")) 348 | }) 349 | 350 | It("should succeed", func() { 351 | Eventually(session).Should(gexec.Exit(0)) 352 | }) 353 | }) 354 | 355 | Context("with client ID and secret", func() { 356 | var opsmanServer *ghttp.Server 357 | var session *gexec.Session 358 | 359 | BeforeEach(func() { 360 | opsmanServer = ghttp.NewTLSServer() 361 | opsmanServer.AppendHandlers( 362 | ghttp.CombineHandlers( 363 | ghttp.VerifyRequest("POST", "/uaa/oauth/token"), 364 | ghttp.VerifyFormKV("response_type", "token"), 365 | ghttp.VerifyFormKV("grant_type", "client_credentials"), 366 | ghttp.VerifyFormKV("username", ""), 367 | ghttp.VerifyFormKV("password", ""), 368 | ghttp.VerifyFormKV("client_id", ""), 369 | ghttp.VerifyFormKV("client_secret", ""), 370 | ghttp.RespondWith(http.StatusOK, `{"access_token": "MAGIC_TOKEN"}`), 371 | ), 372 | ghttp.CombineHandlers( 373 | ghttp.VerifyRequest("GET", "/api/installation_settings"), 374 | ghttp.VerifyHeaderKV("Authorization", "Bearer MAGIC_TOKEN"), 375 | ghttp.RespondWith(http.StatusOK, readFixture("fixtures/nfs_blobstore_test_installation_settings.json", 376 | struct { 377 | DirectorHost string 378 | NfsServerIP string 379 | NFSSshUser string 380 | NFSSshPassword string 381 | SSHPrivateKey string 382 | }{DirectorHost: "127.0.0.1", NfsServerIP: "127.0.0.1", NFSSshUser: currentUser.Name, NFSSshPassword: "SECRET_nfs_ssh_password", SSHPrivateKey: strings.Replace(privateKey, "\n", "\\n", -1)})), 383 | )) 384 | opsmanUri, err = url.Parse(opsmanServer.URL()) 385 | Expect(err).NotTo(HaveOccurred()) 386 | }) 387 | AfterEach(func() { 388 | opsmanServer.Close() 389 | }) 390 | JustBeforeEach(func() { 391 | command := exec.Command(cfopsExecutablePath, "backup", "--opsmanagerhost", opsmanUri.Host, "--clientid", "", "--clientsecret", "", "--opsmanageruser", "", "-d", destinationDirectory, "--tile", "elastic-runtime") 392 | 393 | session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 394 | Expect(err).ShouldNot(HaveOccurred()) 395 | session.Wait(executionTimeout) 396 | }) 397 | It("should accept the flags", func() { 398 | Expect(session.Err.Contents()).ToNot(ContainSubstring("invalid cli flag args")) 399 | }) 400 | 401 | It("should succeed", func() { 402 | Eventually(session).Should(gexec.Exit(0)) 403 | }) 404 | }) 405 | 406 | var FlagsDontWork = func(authArgs ...string) { 407 | var session *gexec.Session 408 | JustBeforeEach(func() { 409 | defaultArgs := []string{"backup", "--opsmanagerhost", opsmanUri.Host, "-d", destinationDirectory, "--tile", "elastic-runtime", "--opsmanageruser", ""} 410 | 411 | command := exec.Command(cfopsExecutablePath, append(defaultArgs, authArgs...)...) 412 | 413 | session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 414 | Expect(err).ShouldNot(HaveOccurred()) 415 | session.Wait(executionTimeout) 416 | }) 417 | It("should not accept the flags", func() { 418 | Expect(session.Err.Contents()).To(ContainSubstring("invalid cli flag args")) 419 | }) 420 | 421 | It("should succeed", func() { 422 | Eventually(session).ShouldNot(gexec.Exit(0)) 423 | }) 424 | } 425 | 426 | Context("with both username/password and token", func() { 427 | FlagsDontWork("--adminuser", "", "--adminpass", "SECRET_admin_password", "--admintoken", "token") 428 | }) 429 | Context("with both username/password and client id", func() { 430 | FlagsDontWork("--adminuser", "", "--adminpass", "pass", 431 | "--clientid", "", "--clientsecret", "") 432 | }) 433 | Context("with both token and client id", func() { 434 | FlagsDontWork("--admintoken", "token", 435 | "--clientid", "", "--clientsecret", "") 436 | }) 437 | Context("with username/password and token and clientID", func() { 438 | FlagsDontWork("--admintoken", "token", 439 | "--adminuser", "", "--adminpass", "pass", 440 | "--clientid", "", "--clientsecret", "") 441 | }) 442 | Context("without username/password nor token nor clientID", func() { 443 | FlagsDontWork() 444 | }) 445 | }) 446 | 447 | Describe("backup blobstore", func() { 448 | var destinationDirectory string 449 | var opsmanUri *url.URL 450 | var additionalFlag string 451 | var boshDirectorServer *ghttp.Server 452 | var opsmanServer *ghttp.Server 453 | var session *gexec.Session 454 | BeforeEach(func() { 455 | boshDirectorServer = ghttp.NewUnstartedServer() 456 | boshDirectorServer.HTTPTestServer.Listener, err = net.Listen("tcp", "127.0.0.1:25555") 457 | Expect(err).NotTo(HaveOccurred()) 458 | boshDirectorServer.HTTPTestServer.StartTLS() 459 | 460 | opsmanServer = ghttp.NewTLSServer() 461 | opsmanServer.AppendHandlers( 462 | ghttp.CombineHandlers( 463 | ghttp.VerifyRequest("POST", "/uaa/oauth/token"), 464 | ghttp.RespondWith(http.StatusOK, `{}`), 465 | ), 466 | ghttp.CombineHandlers( 467 | ghttp.VerifyRequest("GET", "/api/installation_settings"), 468 | ghttp.RespondWith(http.StatusOK, readFixture("fixtures/nfs_blobstore_test_installation_settings.json", 469 | struct { 470 | DirectorHost string 471 | NfsServerIP string 472 | NFSSshUser string 473 | NFSSshPassword string 474 | SSHPrivateKey string 475 | }{DirectorHost: "127.0.0.1", NfsServerIP: "127.0.0.1", NFSSshUser: currentUser.Name, NFSSshPassword: "SECRET_nfs_ssh_password", SSHPrivateKey: strings.Replace(privateKey, "\n", "\\n", -1)})), 476 | )) 477 | opsmanUri, err = url.Parse(opsmanServer.URL()) 478 | Expect(err).NotTo(HaveOccurred()) 479 | directorHandlers := generateHTTPHandlers(boshDirectorServer.URL()) 480 | boshDirectorServer.AppendHandlers(directorHandlers...) 481 | 482 | createTestFiles("/var/vcap/store", []string{ 483 | "shared/cc-resources/09/4d/094dc37299e8d0c68e8e22e8f72a7b1632d26cc3", 484 | "shared/cc-buildpacks/07/7a/077a250e-fdc4-40e5-8d0b-2b8e9cbd1aba_886bd2888127429f7f75120d98a187cbf7289c16", 485 | "shared/cc-droplets/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a/01f1938d589f3e2aa6e3dfa9d4308e215061a5b6", 486 | "shared/cc-packages/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a", 487 | }) 488 | 489 | destinationDirectory, err = ioutil.TempDir("", "cfops_destination") 490 | Expect(err).NotTo(HaveOccurred()) 491 | }) 492 | AfterEach(func() { 493 | os.RemoveAll(destinationDirectory) 494 | opsmanServer.Close() 495 | boshDirectorServer.Close() 496 | }) 497 | JustBeforeEach(func() { 498 | command := exec.Command(cfopsExecutablePath, "backup", "--opsmanagerhost", opsmanUri.Host, "--adminuser", "", "--adminpass", "SECRET_admin_password", "--opsmanageruser", "", "-d", destinationDirectory, "--tile", "elastic-runtime", additionalFlag) 499 | 500 | session, err = gexec.Start(command, GinkgoWriter, GinkgoWriter) 501 | Expect(err).ShouldNot(HaveOccurred()) 502 | session.Wait(executionTimeout) 503 | }) 504 | Context("dosen't log secrets", func() { 505 | It("should Succeed", func() { 506 | 507 | Consistently(session.Err.Contents()).ShouldNot(ContainSubstring("SECRET")) 508 | Consistently(session.Err.Contents()).ShouldNot(ContainSubstring("BEGIN RSA PRIVATE KEY")) 509 | Consistently(session.Out.Contents()).ShouldNot(ContainSubstring("SECRET")) 510 | Consistently(session.Out.Contents()).ShouldNot(ContainSubstring("BEGIN RSA PRIVATE KEY")) 511 | }) 512 | }) 513 | 514 | Context("without any additional flags", func() { 515 | BeforeEach(func() { 516 | additionalFlag = "" 517 | }) 518 | It("should Succeed", func() { 519 | Eventually(session).Should(gexec.Exit(0)) 520 | }) 521 | It("backups all the files", func() { 522 | nfsBackupPath := filepath.Join(destinationDirectory, "nfs_server.backup") 523 | Expect(filesInTar(nfsBackupPath)).To(ConsistOf("shared/cc-resources/09/4d/094dc37299e8d0c68e8e22e8f72a7b1632d26cc3", 524 | "shared/cc-buildpacks/07/7a/077a250e-fdc4-40e5-8d0b-2b8e9cbd1aba_886bd2888127429f7f75120d98a187cbf7289c16", 525 | "shared/cc-droplets/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a/01f1938d589f3e2aa6e3dfa9d4308e215061a5b6", 526 | "shared/cc-packages/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a")) 527 | }) 528 | }) 529 | 530 | Context("with nfs flag set to full", func() { 531 | BeforeEach(func() { 532 | additionalFlag = "-nfs=full" 533 | }) 534 | It("should succeed", func() { 535 | Eventually(session).Should(gexec.Exit(0)) 536 | }) 537 | It("backups all the files", func() { 538 | nfsBackupPath := filepath.Join(destinationDirectory, "nfs_server.backup") 539 | Expect(filesInTar(nfsBackupPath)).To(ConsistOf("shared/cc-resources/09/4d/094dc37299e8d0c68e8e22e8f72a7b1632d26cc3", 540 | "shared/cc-buildpacks/07/7a/077a250e-fdc4-40e5-8d0b-2b8e9cbd1aba_886bd2888127429f7f75120d98a187cbf7289c16", 541 | "shared/cc-droplets/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a/01f1938d589f3e2aa6e3dfa9d4308e215061a5b6", 542 | "shared/cc-packages/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a")) 543 | }) 544 | }) 545 | 546 | Context("with nfs flag set to bp", func() { 547 | BeforeEach(func() { 548 | additionalFlag = "-nfs=bp" 549 | }) 550 | It("should succeed", func() { 551 | Eventually(session).Should(gexec.Exit(0)) 552 | }) 553 | It("backups only the buildbacks", func() { 554 | nfsBackupPath := filepath.Join(destinationDirectory, "nfs_server.backup") 555 | Expect(filesInTar(nfsBackupPath)).To(ConsistOf( 556 | "shared/cc-buildpacks/07/7a/077a250e-fdc4-40e5-8d0b-2b8e9cbd1aba_886bd2888127429f7f75120d98a187cbf7289c16")) 557 | }) 558 | }) 559 | 560 | Context("with nfs flag set to a invalid valid", func() { 561 | BeforeEach(func() { 562 | additionalFlag = "-nfs=invalid" 563 | }) 564 | It("does not run the backup", func() { 565 | Expect(session.Err).Should(gbytes.Say("invalid cli flag args")) 566 | }) 567 | It("should fail", func() { 568 | Eventually(session).ShouldNot(gexec.Exit(0)) 569 | }) 570 | It("does not back up NFS", func() { 571 | nfsBackupPath := filepath.Join(destinationDirectory, "nfs_server.backup") 572 | Expect(nfsBackupPath).NotTo(BeAnExistingFile()) 573 | }) 574 | }) 575 | 576 | Context("with nfs flag set to lite", func() { 577 | BeforeEach(func() { 578 | additionalFlag = "-nfs=lite" 579 | }) 580 | 581 | It("should succeed", func() { 582 | Eventually(session).Should(gexec.Exit(0)) 583 | }) 584 | 585 | It("backs up NFS without cc-resources dir", func() { 586 | nfsBackupPath := filepath.Join(destinationDirectory, "nfs_server.backup") 587 | Expect(filesInTar(nfsBackupPath)).To(ConsistOf( 588 | "shared/cc-buildpacks/07/7a/077a250e-fdc4-40e5-8d0b-2b8e9cbd1aba_886bd2888127429f7f75120d98a187cbf7289c16", 589 | "shared/cc-droplets/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a/01f1938d589f3e2aa6e3dfa9d4308e215061a5b6", 590 | "shared/cc-packages/63/94/63945b8b-b3f4-4736-bb46-edb5a5dae80a")) 591 | }) 592 | }) 593 | }) 594 | 595 | } 596 | 597 | func filesInTar(filename string) []string { 598 | nfsBackupFile, err := os.Open(filename) 599 | defer nfsBackupFile.Close() 600 | Expect(err).ShouldNot(HaveOccurred()) 601 | 602 | var tarReader *tar.Reader 603 | gzf, err := gzip.NewReader(nfsBackupFile) 604 | Expect(err).ShouldNot(HaveOccurred()) 605 | if err == nil { 606 | tarReader = tar.NewReader(gzf) 607 | } else { 608 | tarReader = tar.NewReader(nfsBackupFile) 609 | } 610 | 611 | files := []string{} 612 | for { 613 | hdr, err := tarReader.Next() 614 | if err == io.EOF { 615 | break 616 | } 617 | if err != nil { 618 | Expect(err).NotTo(HaveOccurred()) 619 | } 620 | if hdr.Typeflag == tar.TypeReg { 621 | files = append(files, hdr.Name) 622 | } 623 | } 624 | return files 625 | } 626 | 627 | func readFixture(filename string, variables interface{}) string { 628 | contents, err := ioutil.ReadFile(filename) 629 | Expect(err).NotTo(HaveOccurred()) 630 | 631 | t := template.Must(template.New("fixture").Parse(string(contents))) 632 | 633 | buffer := bytes.NewBuffer([]byte{}) 634 | Expect(t.Execute(buffer, variables)).NotTo(HaveOccurred()) 635 | return buffer.String() 636 | } 637 | 638 | func createSSHKey(sshKeyUsername string) (string, string) { 639 | sshKeys, err := ioutil.TempDir("", "integration-ssh-keys") 640 | Expect(err).ToNot(HaveOccurred()) 641 | privateKeyPath := filepath.Join(sshKeys, "id_rsa") 642 | Expect(exec.Command("ssh-keygen", "-t", "rsa", "-b", "4096", "-C", sshKeyUsername, 643 | "-N", "", "-f", privateKeyPath).Run()).To(Succeed()) 644 | privateKeyContents, err := ioutil.ReadFile(privateKeyPath) 645 | Expect(err).ToNot(HaveOccurred()) 646 | publicKeyContents, err := ioutil.ReadFile(filepath.Join(sshKeys, "id_rsa.pub")) 647 | Expect(err).ToNot(HaveOccurred()) 648 | os.RemoveAll(sshKeys) 649 | return string(publicKeyContents), string(privateKeyContents) 650 | } 651 | 652 | func addToAuthorizedKeys(unixUser *user.User, pubKey string) { 653 | Expect(os.MkdirAll(filepath.Join(unixUser.HomeDir, ".ssh"), 0700)).To(Succeed()) 654 | authKeys, err := os.OpenFile(filepath.Join(unixUser.HomeDir, ".ssh", "authorized_keys"), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600) 655 | Expect(err).ToNot(HaveOccurred()) 656 | authKeys.WriteString(pubKey) 657 | authKeys.Close() 658 | } 659 | 660 | func removeKeyFromAuthorized(unixUser *user.User) { 661 | authKeysFilePath := filepath.Join(unixUser.HomeDir, ".ssh", "authorized_keys") 662 | authKeysContent, err := ioutil.ReadFile(authKeysFilePath) 663 | Expect(err).NotTo(HaveOccurred()) 664 | 665 | trimmedAuthKeysLines := [][]byte{} 666 | for _, line := range bytes.Split(authKeysContent, []byte("\n")) { 667 | if !bytes.Contains(line, []byte(unixUser.Name)) { 668 | trimmedAuthKeysLines = append(trimmedAuthKeysLines, line) 669 | } 670 | } 671 | 672 | trimmedAuthKeysContent := bytes.Join(trimmedAuthKeysLines, []byte("\n")) 673 | err = ioutil.WriteFile(authKeysFilePath, trimmedAuthKeysContent, 0600) 674 | Expect(err).NotTo(HaveOccurred()) 675 | } 676 | 677 | func toJson(s interface{}) string { 678 | b, err := json.Marshal(s) 679 | Expect(err).NotTo(HaveOccurred()) 680 | return string(b) 681 | } 682 | 683 | func directoryExists(dirname string) bool { 684 | _, err := os.Stat(dirname) 685 | return err == nil 686 | } 687 | 688 | func createTestFiles(dirname string, files []string) { 689 | 690 | for _, file := range files { 691 | fullFileName := filepath.Join(dirname, file) 692 | subDirname := filepath.Dir(fullFileName) 693 | Expect(os.MkdirAll(subDirname, 0777)).NotTo(HaveOccurred()) 694 | 695 | w, err := os.Create(fullFileName) 696 | if err != nil { 697 | Expect(err).NotTo(HaveOccurred()) 698 | } 699 | Expect(w.Close()).NotTo(HaveOccurred()) 700 | } 701 | } 702 | -------------------------------------------------------------------------------- /logos/cfops_logo.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/logos/cfops_logo.zip -------------------------------------------------------------------------------- /logos/dm_design.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/logos/dm_design.pdf -------------------------------------------------------------------------------- /logos/original-logos-2016-Feb-8505-11461245.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/logos/original-logos-2016-Feb-8505-11461245.jpg -------------------------------------------------------------------------------- /logos/original-logos-2016-Feb-8505-11461245.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/logos/original-logos-2016-Feb-8505-11461245.png -------------------------------------------------------------------------------- /plugin/cfopsplugin/backuprestore_plugin.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "net/rpc" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | ) 8 | 9 | //Server -- 10 | func (g BackupRestorePlugin) Server(*plugin.MuxBroker) (interface{}, error) { 11 | return &BackupRestoreRPCServer{Impl: g.P}, nil 12 | } 13 | 14 | //Client -- 15 | func (g BackupRestorePlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { 16 | return &BackupRestoreRPC{client: c}, nil 17 | } 18 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/backuprestore_rpc.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/xchapter7x/lo" 7 | ) 8 | 9 | //Backup -- 10 | func (g *BackupRestoreRPC) Backup() (err error) { 11 | lo.G.Debug("running backuprestorerpc backup: ") 12 | resp := err 13 | err = g.client.Call("Plugin.Backup", new(interface{}), &resp) 14 | lo.G.Debug("done calling plugin.backup: ", err) 15 | return 16 | } 17 | 18 | //Restore -- 19 | func (g *BackupRestoreRPC) Restore() (err error) { 20 | lo.G.Debug("running backuprestorerpc restore: ") 21 | resp := errors.New("") 22 | err = g.client.Call("Plugin.Restore", new(interface{}), &resp) 23 | lo.G.Debug("done calling plugin.restore: ", err) 24 | return 25 | } 26 | 27 | //Restore -- 28 | func (g *BackupRestoreRPC) Setup(pcf PivotalCF) error { 29 | lo.G.Debug("running backuprestorerpc setup ", pcf) 30 | resp := errors.New("") 31 | err := g.client.Call("Plugin.Setup", &pcf, &resp) 32 | lo.G.Debug("done calling plugin.setup: ", err) 33 | return err 34 | } 35 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/backuprestore_rpc_server.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import "github.com/xchapter7x/lo" 4 | 5 | //Backup -- 6 | func (s *BackupRestoreRPCServer) Backup(args interface{}, resp *error) error { 7 | lo.G.Debug("rpc server backup execution") 8 | return s.Impl.Backup() 9 | } 10 | 11 | //Restore -- 12 | func (s *BackupRestoreRPCServer) Restore(args interface{}, resp *error) error { 13 | lo.G.Debug("rpc server restore execution") 14 | return s.Impl.Restore() 15 | } 16 | 17 | //Restore -- 18 | func (s *BackupRestoreRPCServer) Setup(pcf PivotalCF, resp *error) error { 19 | lo.G.Debug("rpc server setup execution") 20 | return s.Impl.Setup(pcf) 21 | } 22 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/cfopsplugin.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "encoding/gob" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/xchapter7x/lo" 10 | ) 11 | 12 | //Start - takes a given plugin and starts it 13 | func Start(plgn Plugin) { 14 | gob.Register(plgn) 15 | RegisterPlugin(plgn.GetMeta().Name, plgn) 16 | 17 | if len(os.Args) == 2 && os.Args[1] == PluginMeta { 18 | b, _ := json.Marshal(plgn.GetMeta()) 19 | UIOutput(string(b)) 20 | 21 | } else { 22 | 23 | plugin.Serve(&plugin.ServeConfig{ 24 | HandshakeConfig: handshakeConfig, 25 | Plugins: GetPlugins(), 26 | }) 27 | } 28 | } 29 | 30 | //RegisterPlugin - register a plugin as available 31 | func RegisterPlugin(name string, plugin BackupRestorer) { 32 | lo.G.Debug("registering plugin: ", name, plugin) 33 | pluginMap[name] = &BackupRestorePlugin{ 34 | P: plugin, 35 | } 36 | } 37 | 38 | //GetPlugins - returns the list of registered plugins 39 | func GetPlugins() map[string]plugin.Plugin { 40 | return pluginMap 41 | } 42 | 43 | //GetHandshake - gets the handshake object 44 | func GetHandshake() plugin.HandshakeConfig { 45 | return handshakeConfig 46 | } 47 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/const.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/hashicorp/go-plugin" 7 | ) 8 | 9 | const ( 10 | //PluginMeta - default plugin arg to show meta data 11 | PluginMeta = "plugin-meta" 12 | ) 13 | 14 | var handshakeConfig = plugin.HandshakeConfig{ 15 | ProtocolVersion: 1, 16 | MagicCookieKey: "BASIC_PLUGIN", 17 | MagicCookieValue: "hello", 18 | } 19 | 20 | var pluginMap = make(map[string]plugin.Plugin) 21 | 22 | var ( 23 | //UIOutput - a function to control UIOutput 24 | UIOutput = fmt.Print 25 | ) 26 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/default_pivotalcf.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "io" 5 | "path" 6 | 7 | "github.com/cloudfoundry-community/go-cfenv" 8 | "github.com/pivotalservices/cfbackup" 9 | "github.com/pivotalservices/cfbackup/tileregistry" 10 | ) 11 | 12 | //GetHostDetails - return all of the host and archive details in the form of a tile spec object 13 | func (s *DefaultPivotalCF) GetHostDetails() tileregistry.TileSpec { 14 | return s.TileSpec 15 | } 16 | 17 | //GetInstallationSettings - return installation settings 18 | func (s *DefaultPivotalCF) GetInstallationSettings() cfbackup.InstallationSettings { 19 | return s.InstallationSettings 20 | } 21 | 22 | //NewArchiveWriter - creates a writer to a named resource using the given name on the cfops defined target (s3, local, etc) 23 | func (s *DefaultPivotalCF) NewArchiveWriter(name string) (writer io.WriteCloser, err error) { 24 | backupContext := cfbackup.NewBackupContext(s.TileSpec.ArchiveDirectory, cfenv.CurrentEnv(), s.TileSpec.CryptKey) 25 | writer, err = backupContext.StorageProvider.Writer(path.Join(s.TileSpec.ArchiveDirectory, name)) 26 | return 27 | } 28 | 29 | //NewArchiveReader - creates a reader to a named resource using the given name on the cfops defined target (s3, local, etc) 30 | func (s *DefaultPivotalCF) NewArchiveReader(name string) (reader io.ReadCloser, err error) { 31 | backupContext := cfbackup.NewBackupContext(s.TileSpec.ArchiveDirectory, cfenv.CurrentEnv(), s.TileSpec.CryptKey) 32 | reader, err = backupContext.StorageProvider.Reader(path.Join(s.TileSpec.ArchiveDirectory, name)) 33 | return 34 | } 35 | 36 | //NewPivotalCF - creates the default pivotacf 37 | var NewPivotalCF = func(installationSettings cfbackup.InstallationSettings, ts tileregistry.TileSpec) PivotalCF { 38 | 39 | return &DefaultPivotalCF{ 40 | TileSpec: ts, 41 | InstallationSettings: installationSettings, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/default_pivotalcf_test.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin_test 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "os" 7 | "path" 8 | 9 | "github.com/nu7hatch/gouuid" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/pivotalservices/cfbackup" 13 | "github.com/pivotalservices/cfbackup/tileregistry" 14 | . "github.com/pivotalservices/cfops/plugin/cfopsplugin" 15 | "github.com/xchapter7x/lo" 16 | ) 17 | 18 | var _ = Describe("DefaultPivotalCF initialized with valid installationSettings & TileSpec", func() { 19 | var configParser *cfbackup.ConfigurationParser 20 | var pivotalCF PivotalCF 21 | var controlTmpDir, _ = ioutil.TempDir("", "unit-test") 22 | var controlTileSpec = tileregistry.TileSpec{ 23 | ArchiveDirectory: controlTmpDir, 24 | OpsManagerHost: "localhost", 25 | } 26 | BeforeEach(func() { 27 | configParser = cfbackup.NewConfigurationParser("./fixtures/installation-settings-1-6-aws.json") 28 | pivotalCF = NewPivotalCF(configParser.InstallationSettings, controlTileSpec) 29 | }) 30 | 31 | Context("when GetHostDetails is called", func() { 32 | It("then it should return its targeted host information", func() { 33 | Ω(pivotalCF.GetHostDetails()).Should(Equal(controlTileSpec)) 34 | }) 35 | }) 36 | 37 | Context("when NewArchiveWriter is called w/ a archive name", func() { 38 | var controlName string 39 | BeforeEach(func() { 40 | u, _ := uuid.NewV4() 41 | controlName = u.String() 42 | }) 43 | AfterEach(func() { 44 | os.Remove(path.Join(controlTmpDir, controlName)) 45 | }) 46 | It("then it should create a writer based on the cfops configured target and the archive name", func() { 47 | _, errStatBefore := os.Stat(path.Join(controlTmpDir, controlName)) 48 | Ω(os.IsNotExist(errStatBefore)).ShouldNot(BeFalse()) 49 | writer, err := pivotalCF.NewArchiveWriter(controlName) 50 | Ω(err).ShouldNot(HaveOccurred()) 51 | Ω(writer).ShouldNot(BeNil()) 52 | Ω(func() { 53 | func(x io.Writer) { 54 | lo.G.Debug("the returned var should be a valid writer", writer) 55 | }(writer) 56 | }).ShouldNot(Panic()) 57 | _, errStatAfter := os.Stat(path.Join(controlTmpDir, controlName)) 58 | Ω(os.IsNotExist(errStatAfter)).Should(BeFalse()) 59 | }) 60 | }) 61 | 62 | Context("when NewArchiveReader is called w/ a valid archive name", func() { 63 | var controlName string 64 | BeforeEach(func() { 65 | u, _ := uuid.NewV4() 66 | controlName := u.String() 67 | os.Create(path.Join(controlTmpDir, controlName)) 68 | }) 69 | AfterEach(func() { 70 | os.Remove(path.Join(controlTmpDir, controlName)) 71 | }) 72 | 73 | It("then it should return a reader based on the cfops configured target and the archive name", func() { 74 | reader, err := pivotalCF.NewArchiveReader(controlName) 75 | Ω(err).ShouldNot(HaveOccurred()) 76 | Ω(reader).ShouldNot(BeNil()) 77 | Ω(func() { 78 | func(x io.Reader) { 79 | lo.G.Debug("the returned var should be a valid Reader", reader) 80 | }(reader) 81 | }).ShouldNot(Panic()) 82 | }) 83 | }) 84 | 85 | }) 86 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/init.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import "encoding/gob" 4 | 5 | func init() { 6 | gob.Register(make(map[string]interface{})) 7 | gob.Register(new(DefaultPivotalCF)) 8 | gob.Register(new(BackupRestoreRPC)) 9 | } 10 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/plugin_tile_builder.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/hashicorp/go-plugin" 12 | "github.com/pivotalservices/cfbackup" 13 | "github.com/pivotalservices/cfbackup/tileregistry" 14 | "github.com/pivotalservices/cfbackup/tiles/opsmanager" 15 | "github.com/xchapter7x/lo" 16 | ) 17 | 18 | //DefaultCmdBuilder This is to build the default Cmd to execute the plugin 19 | var DefaultCmdBuilder BuildCmd = func(filePath string, args string) *exec.Cmd { 20 | arguments := strings.Split(args, " ") 21 | arguments = append([]string{"plugin"}, arguments...) 22 | return exec.Command(filePath, arguments...) 23 | } 24 | 25 | //Close Let the client kill the method 26 | func (clientCloser *ClientCloser) Close() { 27 | clientCloser.Client.Kill() 28 | } 29 | 30 | //New - method to create a plugin tile 31 | func (s *PluginTileBuilder) New(tileSpec tileregistry.TileSpec) (tileCloser tileregistry.TileCloser, err error) { 32 | var opsManager *opsmanager.OpsManager 33 | var settingsReader io.Reader 34 | opsManager, err = opsmanager.NewOpsManager(tileSpec.OpsManagerHost, tileSpec.AdminUser, tileSpec.AdminPass, tileSpec.AdminToken, tileSpec.OpsManagerUser, tileSpec.OpsManagerPass, tileSpec.OpsManagerPassphrase, tileSpec.ClientID, tileSpec.ClientSecret, tileSpec.ArchiveDirectory, tileSpec.CryptKey) 35 | 36 | if settingsReader, err = opsManager.GetInstallationSettings(); err == nil { 37 | var brPlugin BackupRestorer 38 | installationSettings := cfbackup.NewConfigurationParserFromReader(settingsReader) 39 | pcf := NewPivotalCF(installationSettings.InstallationSettings, tileSpec) 40 | lo.G.Debug("", s.Meta.Name, s.FilePath, pcf) 41 | brPlugin, client := s.call(tileSpec) 42 | brPlugin.Setup(pcf) 43 | tileCloser = struct { 44 | tileregistry.Tile 45 | tileregistry.Closer 46 | }{ 47 | brPlugin, 48 | &ClientCloser{Client: client}, 49 | } 50 | } 51 | lo.G.Debug("error from getinstallationsettings: ", err) 52 | return 53 | } 54 | 55 | func (s *PluginTileBuilder) call(tileSpec tileregistry.TileSpec) (BackupRestorer, *plugin.Client) { 56 | RegisterPlugin(s.Meta.Name, new(BackupRestoreRPC)) 57 | log.SetOutput(ioutil.Discard) 58 | 59 | client := plugin.NewClient(&plugin.ClientConfig{ 60 | Stderr: os.Stderr, 61 | SyncStdout: os.Stdout, 62 | SyncStderr: os.Stderr, 63 | HandshakeConfig: GetHandshake(), 64 | Plugins: GetPlugins(), 65 | Cmd: s.CmdBuilder(s.FilePath, tileSpec.PluginArgs), 66 | }) 67 | rpcClient, err := client.Client() 68 | 69 | if err != nil { 70 | lo.G.Debug("rpcclient error: ", err) 71 | log.Fatal(err) 72 | } 73 | raw, err := rpcClient.Dispense(s.Meta.Name) 74 | 75 | if err != nil { 76 | lo.G.Debug("error: ", err) 77 | log.Fatal(err) 78 | } 79 | return raw.(BackupRestorer), client 80 | } 81 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/plugin_tile_builder_test.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "runtime" 7 | "strings" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | "github.com/onsi/gomega/ghttp" 12 | "github.com/pivotalservices/cfbackup/tileregistry" 13 | opsfakes "github.com/pivotalservices/cfbackup/tiles/opsmanager/fakes" 14 | . "github.com/pivotalservices/cfops/plugin/cfopsplugin" 15 | ) 16 | 17 | var _ = Describe("given a plugin tile builder", func() { 18 | testNewPluginTileBuilder("when new is called on a default PCF installation", "./fixtures/installation-settings-1-6-default.json") 19 | testNewPluginTileBuilder("when new is called on a AWS PCF installation", "./fixtures/installation-settings-1-6-aws.json") 20 | testNewPluginTileBuilder("when new is called on a PCF installation w/ RabbitMQ", "./fixtures/installation-settings-with-rabbit.json") 21 | }) 22 | 23 | func testNewPluginTileBuilder(behavior string, fixtureInstallationSettingsPath string) { 24 | var pluginTileBuilder *PluginTileBuilder 25 | var pluginTile tileregistry.TileCloser 26 | var err error 27 | Context(behavior, func() { 28 | 29 | var ( 30 | server *ghttp.Server 31 | fakeUser = "fakeuser" 32 | fakePass = "fakepass" 33 | ) 34 | 35 | BeforeEach(func() { 36 | fileBytes, _ := ioutil.ReadFile(fixtureInstallationSettingsPath) 37 | server = opsfakes.NewFakeOpsManagerServer(ghttp.NewTLSServer(), http.StatusInternalServerError, "{}", http.StatusOK, string(fileBytes[:])) 38 | pluginTileBuilder = new(PluginTileBuilder) 39 | pluginTileBuilder.FilePath = "../load/fixture_plugins/" + runtime.GOOS + "/sample" 40 | pluginTileBuilder.Meta = Meta{Name: "backuprestore"} 41 | pluginTileBuilder.CmdBuilder = DefaultCmdBuilder 42 | pluginTile, err = pluginTileBuilder.New(tileregistry.TileSpec{ 43 | OpsManagerHost: strings.Replace(server.URL(), "https://", "", 1), 44 | AdminUser: fakeUser, 45 | AdminPass: fakePass, 46 | OpsManagerUser: "ubuntu", 47 | OpsManagerPass: "xxx", 48 | ArchiveDirectory: "/tmp", 49 | }) 50 | }) 51 | 52 | AfterEach(func() { 53 | server.Close() 54 | pluginTile.Close() 55 | }) 56 | It("should return a plugin tile object", func() { 57 | Ω(err).ShouldNot(HaveOccurred()) 58 | }) 59 | It("should yield results from its methods", func() { 60 | Ω(pluginTile.Backup()).Should(HaveOccurred()) 61 | }) 62 | It("should yield results from its methods", func() { 63 | Ω(pluginTile.Restore()).ShouldNot(HaveOccurred()) 64 | }) 65 | }) 66 | } 67 | 68 | var _ = Describe("given the default command builder", func() { 69 | Context("when a string with argument passed in", func() { 70 | var arguments = "--arg1 value1 --arg2 value2" 71 | var filePath = "path" 72 | It("should return a command with space delimited args", func() { 73 | cmd := DefaultCmdBuilder(filePath, arguments) 74 | Ω(cmd.Path).Should(Equal(filePath)) 75 | Ω(cmd.Args[1]).Should(Equal("plugin")) 76 | Ω(cmd.Args[2]).Should(Equal("--arg1")) 77 | Ω(cmd.Args[3]).Should(Equal("value1")) 78 | Ω(cmd.Args[4]).Should(Equal("--arg2")) 79 | Ω(cmd.Args[5]).Should(Equal("value2")) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/sample/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | cfopsplugin "github.com/pivotalservices/cfops/plugin/cfopsplugin" 8 | "github.com/xchapter7x/lo" 9 | ) 10 | 11 | func main() { 12 | runPlugin() 13 | } 14 | 15 | // Here is a real implementation of a plugin 16 | type BackupRestoreTest struct { 17 | Meta cfopsplugin.Meta 18 | } 19 | 20 | //GetMeta --- 21 | func (b BackupRestoreTest) GetMeta() cfopsplugin.Meta { 22 | return b.Meta 23 | } 24 | 25 | //Setup -- 26 | func (BackupRestoreTest) Setup(pcf cfopsplugin.PivotalCF) error { 27 | lo.G.Debug("mypcf: ", pcf) 28 | return nil 29 | } 30 | 31 | //Backup -- 32 | func (BackupRestoreTest) Backup() error { 33 | lo.G.Debug("Backup!") 34 | lo.G.Debug("again") 35 | return errors.New("somethign happened") 36 | } 37 | 38 | //Restore -- 39 | func (BackupRestoreTest) Restore() error { 40 | lo.G.Debug("restore!") 41 | lo.G.Debug("Arguments %s", os.Args[2:]) 42 | return nil 43 | } 44 | 45 | func runPlugin() { 46 | brt := new(BackupRestoreTest) 47 | brt.Meta = cfopsplugin.Meta{Name: "backuprestore", Role: "backup-restore"} 48 | cfopsplugin.Start(brt) 49 | } 50 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/suite_test.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "test suite") 13 | } 14 | -------------------------------------------------------------------------------- /plugin/cfopsplugin/types.go: -------------------------------------------------------------------------------- 1 | package cfopsplugin 2 | 3 | import ( 4 | "io" 5 | "net/rpc" 6 | "os/exec" 7 | 8 | "github.com/hashicorp/go-plugin" 9 | "github.com/pivotalservices/cfbackup" 10 | "github.com/pivotalservices/cfbackup/tileregistry" 11 | ) 12 | 13 | //Meta - plugin meta data storage object 14 | type Meta struct { 15 | Name string 16 | Role string 17 | Description string 18 | SupportedActivities map[string]bool 19 | } 20 | 21 | //BackupRestorer - is the interface that we're exposing as a plugin. 22 | type BackupRestorer interface { 23 | Backup() error 24 | Restore() error 25 | Setup(PivotalCF) error 26 | } 27 | 28 | //Plugin - is a interface plugin providers should implement 29 | type Plugin interface { 30 | BackupRestorer 31 | GetMeta() Meta 32 | } 33 | 34 | //PivotalCF - interface representing a pivotalcf 35 | type PivotalCF interface { 36 | GetHostDetails() tileregistry.TileSpec 37 | NewArchiveReader(name string) (io.ReadCloser, error) 38 | NewArchiveWriter(name string) (io.WriteCloser, error) 39 | GetInstallationSettings() cfbackup.InstallationSettings 40 | } 41 | 42 | //BackupRestorePlugin - this is an implementation of the rpc client and server wrapper 43 | type BackupRestorePlugin struct { 44 | P BackupRestorer 45 | } 46 | 47 | //BackupRestoreRPCServer - this is an implementation of the rpc server 48 | //for a backuprestorer 49 | type BackupRestoreRPCServer struct { 50 | Impl BackupRestorer 51 | } 52 | 53 | //BackupRestoreRPC - is an implementation of a client that talks over RPC 54 | type BackupRestoreRPC struct { 55 | client *rpc.Client 56 | } 57 | 58 | //DefaultPivotalCF - default implementation of PivotalCF interface 59 | type DefaultPivotalCF struct { 60 | TileSpec tileregistry.TileSpec 61 | InstallationSettings cfbackup.InstallationSettings 62 | } 63 | 64 | //PluginTileBuilder - factory for a tile wrapped plugin 65 | type PluginTileBuilder struct { 66 | FilePath string 67 | Meta Meta 68 | CmdBuilder BuildCmd 69 | } 70 | 71 | //BuildCmd Command func 72 | type BuildCmd func(filePath string, args string) *exec.Cmd 73 | 74 | //ClientCloser This is an adaptor to transform the client kill method to Close method 75 | type ClientCloser struct { 76 | Client *plugin.Client 77 | } 78 | -------------------------------------------------------------------------------- /plugin/fake/init.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import "encoding/gob" 4 | 5 | func init() { 6 | gob.Register(new(PivotalCF)) 7 | } 8 | -------------------------------------------------------------------------------- /plugin/fake/pivotalcf.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/pivotalservices/cfbackup" 7 | "github.com/pivotalservices/cfbackup/tileregistry" 8 | ) 9 | 10 | //PivotalCF -- 11 | type PivotalCF struct { 12 | FakeActivity string 13 | FakeReader io.ReadCloser 14 | FakeWriter io.WriteCloser 15 | FakeHostDetails tileregistry.TileSpec 16 | FakeInstallationSettings cfbackup.InstallationSettings 17 | } 18 | 19 | //GetHostDetails -- 20 | func (s *PivotalCF) GetHostDetails() tileregistry.TileSpec { 21 | return s.FakeHostDetails 22 | } 23 | 24 | //GetInstallationSettings -- 25 | func (s *PivotalCF) GetInstallationSettings() cfbackup.InstallationSettings { 26 | return s.FakeInstallationSettings 27 | } 28 | 29 | //SetActivity -- 30 | func (s *PivotalCF) SetActivity(activity string) { 31 | s.FakeActivity = activity 32 | } 33 | 34 | //GetActivity -- 35 | func (s *PivotalCF) GetActivity() string { 36 | return s.FakeActivity 37 | } 38 | 39 | //NewArchiveReader -- fake archive reader 40 | func (s *PivotalCF) NewArchiveReader(name string) (reader io.ReadCloser, err error) { 41 | reader = s.FakeReader 42 | return 43 | } 44 | 45 | //NewArchiveWriter -- fake archive writer 46 | func (s *PivotalCF) NewArchiveWriter(name string) (writer io.WriteCloser, err error) { 47 | writer = s.FakeWriter 48 | return 49 | } 50 | -------------------------------------------------------------------------------- /plugin/fake/plugin.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/pivotalservices/cfops/plugin/cfopsplugin" 7 | "github.com/xchapter7x/lo" 8 | ) 9 | 10 | //Plugin -- Here is a real implementation of a plugin 11 | type Plugin struct { 12 | Meta cfopsplugin.Meta 13 | } 14 | 15 | //GetMeta --- 16 | func (b Plugin) GetMeta() cfopsplugin.Meta { 17 | return b.Meta 18 | } 19 | 20 | //Setup -- 21 | func (Plugin) Setup(pcf cfopsplugin.PivotalCF) error { 22 | lo.G.Debug("mypcf: ", pcf) 23 | return nil 24 | } 25 | 26 | //Backup -- 27 | func (Plugin) Backup() error { 28 | lo.G.Debug("Backup!") 29 | lo.G.Debug("again") 30 | return errors.New("somethign happened") 31 | } 32 | 33 | //Restore -- 34 | func (Plugin) Restore() error { 35 | lo.G.Debug("restore!") 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /plugin/load/busted_plugins/busted-plugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/plugin/load/busted_plugins/busted-plugin -------------------------------------------------------------------------------- /plugin/load/const.go: -------------------------------------------------------------------------------- 1 | package load 2 | 3 | import "errors" 4 | 5 | //PluginDir - default plugin directory 6 | var PluginDir = "./plugins" 7 | 8 | //ErrInvalidPluginMeta - plugin error 9 | var ErrInvalidPluginMeta = errors.New("invalid plugin meta") 10 | 11 | const ( 12 | //PluginDirEnv - env var to set a custom plugin directory 13 | PluginDirEnv = "CFOPS_PLUGIN_DIR" 14 | ) 15 | -------------------------------------------------------------------------------- /plugin/load/fixture_plugins/darwin/sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/plugin/load/fixture_plugins/darwin/sample -------------------------------------------------------------------------------- /plugin/load/fixture_plugins/linux/sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/plugin/load/fixture_plugins/linux/sample -------------------------------------------------------------------------------- /plugin/load/fixture_plugins/windows/sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/plugin/load/fixture_plugins/windows/sample -------------------------------------------------------------------------------- /plugin/load/fixture_plugins/windows/sample.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/plugin/load/fixture_plugins/windows/sample.exe -------------------------------------------------------------------------------- /plugin/load/init.go: -------------------------------------------------------------------------------- 1 | package load 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | 10 | "github.com/pivotalservices/cfbackup/tileregistry" 11 | "github.com/pivotalservices/cfops/plugin/cfopsplugin" 12 | "github.com/xchapter7x/lo" 13 | ) 14 | 15 | func init() { 16 | 17 | if dir := os.Getenv(PluginDirEnv); dir != "" { 18 | PluginDir = dir 19 | } 20 | Plugins(PluginDir) 21 | } 22 | 23 | //Plugins - function to register plugins residing in a given directory with cfops 24 | func Plugins(dir string) (err error) { 25 | var fileInfoArray []os.FileInfo 26 | var fileInfo os.FileInfo 27 | 28 | if fileInfoArray, err = ioutil.ReadDir(dir); err == nil && len(fileInfoArray) > 0 { 29 | for _, fileInfo = range fileInfoArray { 30 | if err = loadPlugin(dir, fileInfo); err != nil { 31 | lo.G.Debug("error loading plugin: ", err, fileInfo) 32 | break 33 | } 34 | lo.G.Debug("loading plugin from: ", fileInfo) 35 | } 36 | 37 | } else if err != nil { 38 | lo.G.Debug("not loading plugins: ", err, fileInfoArray) 39 | err = fmt.Errorf("error loading plugins: %v %v", err, fileInfoArray) 40 | } 41 | return 42 | } 43 | 44 | func loadPlugin(dir string, fileInfo os.FileInfo) (err error) { 45 | var bytes []byte 46 | filePath := dir + "/" + fileInfo.Name() 47 | 48 | if bytes, err = exec.Command(filePath, cfopsplugin.PluginMeta).Output(); err == nil { 49 | meta := cfopsplugin.Meta{} 50 | if err = json.Unmarshal(bytes, &meta); err == nil { 51 | 52 | if meta.Name == "" { 53 | lo.G.Debug("plugin meta busted", meta) 54 | err = ErrInvalidPluginMeta 55 | 56 | } else { 57 | ptb := &cfopsplugin.PluginTileBuilder{ 58 | FilePath: filePath, 59 | Meta: meta, 60 | CmdBuilder: cfopsplugin.DefaultCmdBuilder, 61 | } 62 | lo.G.Debug("registering plugin: ", ptb) 63 | tileregistry.Register(meta.Name, ptb) 64 | } 65 | 66 | } else { 67 | lo.G.Error("plugin load error: ", filePath, err) 68 | err = fmt.Errorf("plugin load error: %v %v ", filePath, err) 69 | } 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /plugin/load/init_test.go: -------------------------------------------------------------------------------- 1 | package load_test 2 | 3 | import ( 4 | "runtime" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | "github.com/pivotalservices/cfbackup/tileregistry" 9 | . "github.com/pivotalservices/cfops/plugin/load" 10 | ) 11 | 12 | var _ = Describe("given Plugins", func() { 13 | Context("when called with a valid plugin directory containing plugins", func() { 14 | It("then it should register all plugins in the plugins directory", func() { 15 | controlTileLength := len(tileregistry.GetRegistry()) 16 | err := Plugins("fixture_plugins/" + runtime.GOOS) 17 | tileCount := len(tileregistry.GetRegistry()) 18 | Ω(err).ShouldNot(HaveOccurred()) 19 | Ω(tileCount).Should(BeNumerically(">", controlTileLength)) 20 | }) 21 | }) 22 | Context("when called on a invalid or empty directory", func() { 23 | var err error 24 | BeforeEach(func() { 25 | err = Plugins("dir-does-not-exist") 26 | }) 27 | It("then it should yield an error", func() { 28 | Ω(err).Should(HaveOccurred()) 29 | }) 30 | }) 31 | Context("when a plugin is not able to be registered", func() { 32 | var err error 33 | BeforeEach(func() { 34 | err = Plugins("busted_plugins") 35 | }) 36 | It("then it should yield an error", func() { 37 | Ω(err).Should(HaveOccurred()) 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /plugin/load/suite_test.go: -------------------------------------------------------------------------------- 1 | package load_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestSuite(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "test suite") 13 | } 14 | -------------------------------------------------------------------------------- /scripts/backup.cron: -------------------------------------------------------------------------------- 1 | ${CFOPS_CRON} /root/scripts/run backup ${CFOPS_TILE} >>/root/pcfbackup/backup.log 2>&1 2 | -------------------------------------------------------------------------------- /scripts/build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo -e "\nGenerating Binary..." 6 | 7 | ROOT_DIR=$(cd $(dirname $(dirname $0)) && pwd) 8 | 9 | CLI_GOPATH=$ROOT_DIR/tmp/cli_gopath 10 | rm -rf $CLI_GOPATH 11 | mkdir -p $CLI_GOPATH/src/github.com/pivotalservices/ 12 | ln -s $ROOT_DIR $CLI_GOPATH/src/github.com/pivotalservices/cfops 13 | 14 | GODEP_GOPATH=$ROOT_DIR/Godeps/_workspace 15 | 16 | GOPATH=$CLI_GOPATH:$GODEP_GOPATH:$GOPATH go build -o $ROOT_DIR/out/cfops cmd/cfops/*.go 17 | rm -rf $CLI_GOPATH 18 | -------------------------------------------------------------------------------- /scripts/build_cfops_centos7.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ue 2 | 3 | # Install git 4 | yum install -y git 5 | 6 | #Install GO Lang 7 | cd /tmp/ 8 | curl https://storage.googleapis.com/golang/go1.8.linux-amd64.tar.gz | tar -C /usr/local -xzf - 9 | echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile.d/path.sh 10 | 11 | source /etc/profile.d/path.sh 12 | export GOPATH="/var/tmp/" 13 | 14 | #Get the source code 15 | go get github.com/pivotalservices/cfops || true 16 | 17 | #Install Glide 18 | export GOBIN="/usr/local/go/bin" 19 | curl https://glide.sh/get | sh 20 | 21 | #Pull in glide managed dependencies: 22 | cd $GOPATH/src/github.com/pivotalservices/cfops 23 | glide install 24 | 25 | #Build the project: 26 | cd cmd/cfops/ 27 | go build 28 | 29 | ls -la /var/tmp/src/github.com/pivotalservices/cfops/cmd/cfops/cfops 30 | -------------------------------------------------------------------------------- /scripts/ctl.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | RUN_DIR='/root/pcfbackup' 4 | LOG_DIR='/root/pcfbackup/log' 5 | PIDFILE="$RUN_DIR/pid" 6 | 7 | CRON_STATUS=`pgrep cron | wc -l` 8 | CRON_ENTRY=$(cat /root/scripts/backup.cron) 9 | CRONTAB_TMP="$RUN_DIR/crontab.$$.tmp" 10 | 11 | case $1 in 12 | 13 | start) 14 | 15 | mkdir -p "$RUN_DIR" "$LOG_DIR" 16 | 17 | echo $$ > $PIDFILE 18 | 19 | if [ $CRON_STATUS -eq 0 ]; then 20 | /usr/sbin/cron start 21 | fi 22 | 23 | if crontab -l | sed -e 's/^#.*//' | grep "$CRON_ENTRY" 2>&1>/dev/null; then 24 | echo 'Already running' 25 | else 26 | crontab -l | grep -v "$CRON_ENTRY" > "$CRONTAB_TMP" 27 | echo "$CRON_ENTRY" >> "$CRONTAB_TMP" 28 | crontab < "$CRONTAB_TMP" 29 | rm "$CRONTAB_TMP" 30 | fi 31 | 32 | while true; do 33 | sleep 60 34 | done 35 | 36 | ;; 37 | 38 | stop) 39 | 40 | if crontab -l | sed -e 's/^#.*//' | grep "$CRON_ENTRY" 2>&1>/dev/null; then 41 | crontab -l | grep -v "$CRON_ENTRY" > "$CRONTAB_TMP" 42 | crontab < "$CRONTAB_TMP" 43 | rm "$CRONTAB_TMP" 44 | else 45 | echo 'Not running' 46 | fi 47 | 48 | kill -TERM $(cat "$PIDFILE") 49 | 50 | rm -f $PIDFILE 51 | 52 | ;; 53 | 54 | *) 55 | echo "Usage: ctl {start|stop}" ;; 56 | 57 | esac 58 | -------------------------------------------------------------------------------- /scripts/generate_sample_plugins: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | MYOS=darwin 3 | GOOS=${MYOS} godep go build plugin/cfopsplugin/sample/main.go && chmod +x main && mv main plugin/load/fixture_plugins/${MYOS}/sample 4 | 5 | MYOS=linux 6 | GOOS=${MYOS} godep go build plugin/cfopsplugin/sample/main.go && chmod +x main && mv main plugin/load/fixture_plugins/${MYOS}/sample 7 | 8 | MYOS=windows 9 | GOOS=${MYOS} godep go build plugin/cfopsplugin/sample/main.go && chmod +x main.exe && mv main.exe plugin/load/fixture_plugins/${MYOS}/sample 10 | 11 | -------------------------------------------------------------------------------- /scripts/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ $# -lt 2 ]; then 6 | echo "Usage: run backup|restore " 7 | exit 1 8 | fi 9 | 10 | if [[ "${CFOPS_HOST}X" == "X" ]]; then 11 | echo "CFOPS_HOST environment variable not set" 12 | exit 1 13 | fi 14 | if [[ "${CFOPS_ADMIN_USER}X" == "X" ]]; then 15 | echo "CFOPS_ADMIN_USER environment variable not set" 16 | exit 1 17 | fi 18 | if [[ "${CFOPS_ADMIN_PASS}X" == "X" ]]; then 19 | echo "CFOPS_ADMIN_PASS environment variable not set" 20 | exit 1 21 | fi 22 | if [[ "${CFOPS_ADMIN_PASS}X" == "X" ]]; then 23 | echo "CFOPS_ADMIN_PASS environment variable not set" 24 | exit 1 25 | fi 26 | if [[ "${CFOPS_OM_PASS}X" == "X" ]]; then 27 | echo "CFOPS_OM_PASS environment variable not set" 28 | exit 1 29 | fi 30 | 31 | ACTION=$1 32 | TILE=$2 33 | ER_VERSION=${ER_VERSION:-1.6} 34 | LOG_LEVEL=${LOG_LEVEL:-debug} 35 | CFOPS_DEST_PATH=${CFOPS_DEST_PATH:-/tmp} 36 | S3_ACTIVE=${S3_ACTIVE:false} 37 | S3_BUCKET_NAME=${S3_BUCKET_NAME:-pcfbackup-files} 38 | 39 | if [[ $S3_ACTIVE ]]; then 40 | if [[ "${S3_SECRET_ACCESS_KEY}X" == "X" ]]; then 41 | echo "S3_SECRET_ACCESS_KEY environment variable not set" 42 | exit 1 43 | fi 44 | if [[ "${S3_ACCESS_KEY_ID}X" == "X" ]]; then 45 | echo "S3_ACCESS_KEY_ID environment variable not set" 46 | exit 1 47 | fi 48 | fi 49 | 50 | ROOT_DIR=$(cd $(dirname $(dirname $0)) && pwd) 51 | SCRIPTS_DIR=$(dirname $0) 52 | 53 | . $SCRIPTS_DIR/build 54 | 55 | echo -e "\nRunning cfops $1 $2" 56 | echo "Starting at: " $(date) 57 | echo "" 58 | 59 | #echo -e "$(env | grep CFOPS && env | grep S3)" 60 | 61 | $ROOT_DIR/out/cfops $ACTION -t $TILE --omh $CFOPS_HOST --du $CFOPS_ADMIN_USER --dp $CFOPS_ADMIN_PASS --omu ubuntu --omp $CFOPS_OM_PASS 62 | 63 | echo -e "\nEnded at:" $(date) 64 | -------------------------------------------------------------------------------- /scripts/run_system_test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | `rm -rf system_test_workspace` 4 | raise 'cant use without ENV_NAME' unless ENV['ENV_NAME'] 5 | raise 'cant use without IAAS' unless ENV['IAAS'] 6 | raise 'cant use without OM_VERSION' unless ENV['OM_VERSION'] 7 | 8 | `mkdir -p system_test_workspace` 9 | 10 | `ln -s ~/workspace/london-meta system_test_workspace/london-meta` 11 | 12 | `mkdir -p system_test_workspace/environment-lock` 13 | `echo $ENV_NAME > system_test_workspace/environment-lock/name` 14 | `cat ~/workspace/london-services-locks/$IAAS-$OM_VERSION-envs/claimed/$ENV_NAME > system_test_workspace/environment-lock/metadata` 15 | 16 | `mkdir -p system_test_workspace/src/github.com/pivotalservices/` 17 | 18 | puts `ln -s #{`pwd`.strip} system_test_workspace/src/github.com/pivotalservices/cfops` 19 | 20 | exec("cd system_test_workspace && ../ci/scripts/system") 21 | 22 | -------------------------------------------------------------------------------- /system/assets/test-app/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "sinatra" 4 | -------------------------------------------------------------------------------- /system/assets/test-app/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | rack (1.6.4) 5 | rack-protection (1.5.3) 6 | rack 7 | sinatra (1.4.7) 8 | rack (~> 1.5) 9 | rack-protection (~> 1.4) 10 | tilt (>= 1.3, < 3) 11 | tilt (2.0.5) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | sinatra 18 | 19 | BUNDLED WITH 20 | 1.12.5 21 | -------------------------------------------------------------------------------- /system/assets/test-app/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | 3 | class TestApp < Sinatra::Base 4 | get "/" do 5 | "hello world" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /system/assets/test-app/config.ru: -------------------------------------------------------------------------------- 1 | require File.expand_path('app', File.dirname(__FILE__)) 2 | 3 | run TestApp 4 | -------------------------------------------------------------------------------- /system/cfopssystem_suite_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | "testing" 7 | 8 | "code.cloudfoundry.org/lager" 9 | 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | "github.com/onsi/gomega/gexec" 13 | "github.com/pborman/uuid" 14 | ) 15 | 16 | var cfConfig Config 17 | 18 | func TestBrokerintegration(t *testing.T) { 19 | RegisterFailHandler(Fail) 20 | RunSpecs(t, "cfops System Test Suite") 21 | } 22 | 23 | var _ = BeforeSuite(func() { 24 | cfConfig.APIEndpoint = os.Getenv("CF_API_URL") 25 | cfConfig.OMAdminUser = os.Getenv("OM_USER") 26 | cfConfig.OMAdminPassword = os.Getenv("OM_PASSWORD") 27 | cfConfig.OMHostname = os.Getenv("OM_HOSTNAME") 28 | cfConfig.AmiID = os.Getenv("OPSMAN_AMI") 29 | cfConfig.SecurityGroup = os.Getenv("AWS_SECURITY_GROUP") 30 | 31 | cfConfig.AppName = uuid.NewRandom().String() 32 | cfConfig.OrgName = uuid.NewRandom().String() 33 | cfConfig.SpaceName = uuid.NewRandom().String() 34 | cfConfig.AppPath = "assets/test-app" 35 | 36 | Expect(json.Unmarshal([]byte(os.Getenv("OM_PROXY_INFO")), &cfConfig.OMHostInfo)).To(Succeed()) 37 | cfConfig.OMHostInfo.SSHKey = os.Getenv("OM_SSH_KEY") 38 | 39 | var err error 40 | 41 | logger = lager.NewLogger("Test Logs") 42 | logger.RegisterSink(lager.NewWriterSink(GinkgoWriter, lager.DEBUG)) 43 | 44 | os.Setenv("GOOS", "linux") 45 | cfopsLinuxExecutablePath, err = gexec.Build("github.com/pivotalservices/cfops/cmd/cfops") 46 | Expect(err).NotTo(HaveOccurred()) 47 | os.Unsetenv("GOOS") 48 | 49 | cfopsExecutablePath, err = gexec.Build("github.com/pivotalservices/cfops/cmd/cfops") 50 | Expect(err).NotTo(HaveOccurred()) 51 | }) 52 | 53 | var _ = AfterSuite(func() { 54 | gexec.CleanupBuildArtifacts() 55 | }) 56 | -------------------------------------------------------------------------------- /system/ert_system_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/cloudfoundry-incubator/cf-test-helpers/cf" 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | "github.com/onsi/gomega/gbytes" 10 | "github.com/pborman/uuid" 11 | ) 12 | 13 | var _ = Describe("CFOps Elastic Runtime plugin", func() { 14 | cfopsPath := "/tmp/cfops" 15 | backupPath := "/tmp/cfops-backup-" + uuid.NewRandom().String() 16 | 17 | BeforeEach(func() { 18 | opsManager, _ := NewOpsManagerClient(cfConfig.OMHostname, cfConfig.OMAdminUser, cfConfig.OMAdminPassword, logger) 19 | adminUser, adminPassword, err := opsManager.GetAdminCredentials() 20 | Expect(err).NotTo(HaveOccurred()) 21 | cfConfig.AdminUser, cfConfig.AdminPassword = adminUser, adminPassword 22 | 23 | pushTestApp(cfConfig) 24 | }) 25 | 26 | AfterEach(func() { 27 | deleteTestApp(cfConfig) 28 | 29 | if cfopsPath != "" { 30 | remoteExecute(cfConfig.OMHostInfo, "rm -rf "+cfopsPath) 31 | } 32 | 33 | if backupPath != "" { 34 | remoteExecute(cfConfig.OMHostInfo, "rm -rf "+backupPath) 35 | } 36 | }) 37 | 38 | It("backs up and restores successfully", func() { 39 | backupCmd := strings.Join([]string{ 40 | "LOG_LEVEL=debug", 41 | cfopsPath, 42 | "backup", 43 | "--opsmanagerhost=" + cfConfig.OMHostname, 44 | "--opsmanageruser=ubuntu", 45 | "--destination=" + backupPath, 46 | "--adminuser=" + cfConfig.OMAdminUser, 47 | "--adminpass=" + cfConfig.OMAdminPassword, 48 | "--tile=elastic-runtime", 49 | }, " ") 50 | 51 | restoreCmd := strings.Join([]string{ 52 | "LOG_LEVEL=debug", 53 | cfopsPath, 54 | "restore", 55 | "--opsmanagerhost=" + cfConfig.OMHostname, 56 | "--opsmanageruser=ubuntu", 57 | "--destination=" + backupPath, 58 | "--adminuser=" + cfConfig.OMAdminUser, 59 | "--adminpass=" + cfConfig.OMAdminPassword, 60 | "--tile=elastic-runtime", 61 | }, " ") 62 | 63 | scpHelper(cfConfig.OMHostInfo, cfopsLinuxExecutablePath, cfopsPath) 64 | _, err := remoteExecute(cfConfig.OMHostInfo, "chmod +x /tmp/cfops") 65 | 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | By("Backing up ERT...") 69 | output, err := remoteExecute(cfConfig.OMHostInfo, backupCmd) 70 | GinkgoWriter.Write(output) 71 | Expect(err).NotTo(HaveOccurred()) 72 | checkNoSecretsInSession(output) 73 | deleteTestApp(cfConfig) 74 | 75 | By("Restoring ERT...") 76 | output, err = remoteExecute(cfConfig.OMHostInfo, restoreCmd) 77 | GinkgoWriter.Write(output) 78 | Expect(err).NotTo(HaveOccurred()) 79 | checkNoSecretsInSession(output) 80 | 81 | cfDo("target", "-o", cfConfig.OrgName, "-s", cfConfig.SpaceName) 82 | Eventually(cf.Cf("apps")).Should(gbytes.Say(cfConfig.AppName)) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /system/http_client.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "path" 11 | 12 | "code.cloudfoundry.org/lager" 13 | ) 14 | 15 | type httpClient struct { 16 | baseURL string 17 | authorization string 18 | client *http.Client 19 | request *http.Request 20 | logger lager.Logger 21 | } 22 | 23 | //newHttpClient ... 24 | func newHttpClient(url, authorization string, logger lager.Logger) httpClient { 25 | tr := &http.Transport{ 26 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 27 | } 28 | return httpClient{ 29 | baseURL: url, 30 | authorization: authorization, 31 | client: &http.Client{Transport: tr}, 32 | logger: logger.Session("http"), 33 | } 34 | } 35 | 36 | func (client *httpClient) NewRequest(endpoint string, body io.Reader) (*http.Request, error) { 37 | parsedURL, err := url.Parse(client.baseURL) 38 | if err != nil { 39 | return nil, err 40 | } 41 | parsedURL.Path = path.Join(parsedURL.Path, endpoint) 42 | request, err := http.NewRequest("", parsedURL.String(), body) 43 | if err != nil { 44 | panic(err) 45 | } 46 | client.request = request 47 | client.addCommonHeaders() 48 | return request, nil 49 | } 50 | 51 | func (client *httpClient) Post() error { 52 | client.request.Method = "POST" 53 | _, err := client.processRequest() 54 | return err 55 | } 56 | 57 | func (client *httpClient) Delete() error { 58 | client.request.Method = "DELETE" 59 | _, err := client.processRequest() 60 | return err 61 | } 62 | 63 | func (client *httpClient) Get(responseObject interface{}) error { 64 | client.request.Method = "GET" 65 | responseBody, err := client.processRequest() 66 | if err != nil { 67 | return err 68 | } 69 | err = json.NewDecoder(responseBody).Decode(&responseObject) 70 | // TODO: Add logging again, it fails case we have a json.RawMessage in a struct 71 | // TODO: uncomment when https://github.com/cloudfoundry/lager/pull/20 is fixed 72 | // client.logger.Debug("Response body", lager.Data{"response": responseObject}) 73 | return err 74 | } 75 | 76 | func (client *httpClient) processRequest() (io.ReadCloser, error) { 77 | client.logger.Debug("making request", lager.Data{ 78 | "URL": client.request.URL.String(), 79 | "Method": client.request.Method, 80 | }) 81 | response, err := client.client.Do(client.request) 82 | if err != nil { 83 | client.logger.Error("request failed", err) 84 | panic("Catz!") 85 | } 86 | 87 | if response.StatusCode != 200 { 88 | responseBody := map[string]interface{}{} 89 | json.NewDecoder(response.Body).Decode(&responseBody) 90 | 91 | return nil, fmt.Errorf("Request failed with %d. Info: %v", response.StatusCode, responseBody) 92 | } 93 | return response.Body, err 94 | } 95 | 96 | func (client *httpClient) addCommonHeaders() { 97 | client.request.Header.Set("Content-Type", "application/json") 98 | client.request.Header.Set("Authorization", client.authorization) 99 | } 100 | -------------------------------------------------------------------------------- /system/opsman_system_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | 7 | "code.cloudfoundry.org/lager" 8 | 9 | "time" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | "github.com/onsi/gomega/gexec" 14 | ) 15 | 16 | var cfopsExecutablePath string 17 | var cfopsLinuxExecutablePath string 18 | var logger lager.Logger 19 | 20 | func checkNoSecretsInSession(session []byte) { 21 | if cfConfig.OMAdminPassword != "" { 22 | Expect(session).NotTo(ContainSubstring(cfConfig.OMAdminPassword)) 23 | } 24 | Expect(session).NotTo(ContainSubstring("RSA PRIVATE KEY")) 25 | } 26 | 27 | func checkOpsManagersIdentical(oldHost, newHost string) { 28 | opsManager, err := NewOpsManagerClient(oldHost, cfConfig.OMAdminUser, cfConfig.OMAdminPassword, logger) 29 | Expect(err).NotTo(HaveOccurred()) 30 | opsManagerProducts, _ := opsManager.GetStagedProducts() 31 | Expect(err).NotTo(HaveOccurred()) 32 | 33 | restoredOpsManager, err := NewOpsManagerClient(newHost, cfConfig.OMAdminUser, cfConfig.OMAdminPassword, logger) 34 | Expect(err).NotTo(HaveOccurred()) 35 | restoredOpsManagerProducts, _ := restoredOpsManager.GetStagedProducts() 36 | Expect(err).NotTo(HaveOccurred()) 37 | 38 | Expect(opsManagerProducts).To(ConsistOf(restoredOpsManagerProducts)) 39 | } 40 | 41 | var _ = Describe("CFOps Ops Manager plugin", func() { 42 | It("backs up and restores successfully", func() { 43 | if os.Getenv("ONLY_ERT") == "true" { 44 | return 45 | } 46 | 47 | vm := createInstance("cfops-test", cfConfig.AmiID, cfConfig.SecurityGroup) 48 | 49 | ips, err := vm.GetIPs() 50 | newVMIP := ips[0].String() 51 | Expect(err).NotTo(HaveOccurred()) 52 | 53 | backupCommand := exec.Command( 54 | cfopsExecutablePath, 55 | "backup", 56 | "--opsmanagerhost="+cfConfig.OMHostname, 57 | "--opsmanageruser=ubuntu", 58 | "--destination=../tmp/", 59 | "--adminuser="+cfConfig.OMAdminUser, 60 | "--adminpass="+cfConfig.OMAdminPassword, 61 | "--tile=ops-manager", 62 | ) 63 | 64 | restoreCommand := exec.Command( 65 | cfopsExecutablePath, 66 | "restore", 67 | "--opsmanagerhost="+newVMIP, 68 | "--opsmanageruser=ubuntu", 69 | "--destination=../tmp/", 70 | "--adminuser="+cfConfig.OMAdminUser, 71 | "--adminpass="+cfConfig.OMAdminPassword, 72 | "--opsmanagerpassphrase="+cfConfig.OMAdminPassword, 73 | "--tile=ops-manager", 74 | ) 75 | 76 | backupSession, err := gexec.Start(backupCommand, GinkgoWriter, GinkgoWriter) 77 | Expect(err).NotTo(HaveOccurred()) 78 | 79 | Eventually(backupSession, 1200).Should(gexec.Exit(0)) 80 | checkNoSecretsInSession(backupSession.Out.Contents()) 81 | checkNoSecretsInSession(backupSession.Err.Contents()) 82 | 83 | if os.Getenv("OM_VERSION") == "1.6" { 84 | createAdminUser(newVMIP, cfConfig.OMAdminUser, cfConfig.OMAdminPassword) 85 | } 86 | 87 | restoreSession, err := gexec.Start(restoreCommand, GinkgoWriter, GinkgoWriter) 88 | Expect(err).NotTo(HaveOccurred()) 89 | 90 | Eventually(restoreSession, 1800).Should(gexec.Exit(0)) 91 | checkNoSecretsInSession(restoreSession.Out.Contents()) 92 | checkNoSecretsInSession(restoreSession.Err.Contents()) 93 | 94 | time.Sleep(2 * time.Minute) // TODO make this better 95 | 96 | checkOpsManagersIdentical(cfConfig.OMHostname, newVMIP) 97 | 98 | vm.Destroy() 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /system/opsmanager_client.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/base64" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | 12 | . "github.com/onsi/ginkgo" 13 | 14 | "code.cloudfoundry.org/lager" 15 | ) 16 | 17 | type opsmanClient struct { 18 | httpClient httpClient 19 | token string 20 | opsManagerURL string 21 | logger lager.Logger 22 | } 23 | 24 | type stagedProduct struct { 25 | Type string `json:"type"` 26 | GUID string `json:"GUID"` 27 | } 28 | 29 | type credentials struct { 30 | Credential struct { 31 | Type string `json:"simple_credentials"` 32 | Value struct { 33 | Username string `json:"identity"` 34 | Password string `json:"password"` 35 | } 36 | } 37 | } 38 | 39 | //NewOpsManagerClient ... 40 | func NewOpsManagerClient(hostname, username, password string, logger lager.Logger) (*opsmanClient, error) { 41 | url := "https://" + hostname 42 | 43 | token, err := getAuthorization(url, username, password) 44 | 45 | if err != nil { 46 | return &opsmanClient{}, err 47 | } 48 | 49 | opsManClient := newHttpClient(url, token, logger) 50 | return &opsmanClient{ 51 | token: token, 52 | httpClient: opsManClient, 53 | opsManagerURL: url, 54 | logger: logger, 55 | }, nil 56 | } 57 | 58 | func (client *opsmanClient) GetStagedProducts() ([]stagedProduct, error) { 59 | client.httpClient.NewRequest("api/v0/staged/products", nil) 60 | stagedProducts := []stagedProduct{} 61 | err := client.httpClient.Get(&stagedProducts) 62 | return stagedProducts, err 63 | } 64 | 65 | type installationSettings struct { 66 | Products products `json:"products"` 67 | } 68 | type products []product 69 | 70 | func (products products) CF() product { 71 | for _, p := range products { 72 | if strings.HasPrefix(p.InstallationName, "cf-") { 73 | return p 74 | } 75 | } 76 | Fail("Cant find cf product in installation settings") 77 | return product{} 78 | } 79 | 80 | type product struct { 81 | InstallationName string `json:"installation_name"` 82 | Jobs jobs `json:"jobs"` 83 | } 84 | type jobs []job 85 | 86 | func (jobs jobs) UAA() job { 87 | for _, j := range jobs { 88 | if j.InstallationName == "uaa" { 89 | return j 90 | } 91 | } 92 | Fail("Cant find uaa job in product settings") 93 | return job{} 94 | } 95 | 96 | type job struct { 97 | InstallationName string `json:"installation_name"` 98 | Properties properties 99 | } 100 | 101 | type properties []property 102 | 103 | func (properties properties) AdminCredentials() property { 104 | for _, p := range properties { 105 | if p.Identifier == "admin_credentials" { 106 | return p 107 | } 108 | } 109 | Fail("Cant find admin_credentials in uaa job") 110 | return property{} 111 | } 112 | 113 | type property struct { 114 | Identifier string `json:"identifier"` 115 | Value json.RawMessage `json:"value"` 116 | } 117 | 118 | type credentialsValue struct { 119 | Username string `json:"identity"` 120 | Password string `json:"password"` 121 | } 122 | 123 | func (property property) Credentials() (username, password string, err error) { 124 | creds := credentialsValue{} 125 | fmt.Printf("property.Value %s\n", string(property.Value)) 126 | err = json.Unmarshal(property.Value, &creds) 127 | return creds.Username, creds.Password, err 128 | } 129 | 130 | func (client *opsmanClient) GetInstallationSettings() (installationSettings, error) { 131 | client.httpClient.NewRequest("api/installation_settings", nil) 132 | installationSetting := installationSettings{} 133 | err := client.httpClient.Get(&installationSetting) 134 | return installationSetting, err 135 | } 136 | 137 | func (client *opsmanClient) GetAdminCredentials() (username, password string, err error) { 138 | installationSetting, err := client.GetInstallationSettings() 139 | if err != nil { 140 | return "", "", err 141 | } 142 | fmt.Printf("installationSetting.Products.CF().Jobs %d\n", len(installationSetting.Products.CF().Jobs)) 143 | return installationSetting.Products.CF().Jobs.UAA().Properties.AdminCredentials().Credentials() 144 | } 145 | 146 | func getAuthorization(opsManagerURL, username, password string) (string, error) { 147 | body := url.Values{ 148 | "grant_type": {"password"}, 149 | "username": {username}, 150 | "password": {password}, 151 | } 152 | tr := &http.Transport{ 153 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 154 | } 155 | opsManClient := &http.Client{Transport: tr} 156 | request, err := http.NewRequest("POST", fmt.Sprintf("%s/uaa/oauth/token", opsManagerURL), strings.NewReader(body.Encode())) 157 | if err != nil { 158 | return "", err 159 | } 160 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8") 161 | request.SetBasicAuth("opsman", "") 162 | response, err := opsManClient.Do(request) 163 | if err != nil { 164 | return "", err 165 | } 166 | 167 | if response.StatusCode == 200 { 168 | responseToken := token{} 169 | err = json.NewDecoder(response.Body).Decode(&responseToken) 170 | if err != nil { 171 | return "", err 172 | } 173 | if responseToken.AccessToken == "" { 174 | return "", fmt.Errorf("No token returned") 175 | } 176 | return "Bearer " + responseToken.AccessToken, nil 177 | } else if response.StatusCode == 404 { 178 | // Assume basic auth 179 | auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) 180 | return "Basic " + auth, nil 181 | } else { 182 | return "", fmt.Errorf("Unexpected response code %d", response.StatusCode) 183 | } 184 | } 185 | 186 | type token struct { 187 | AccessToken string `json:"access_token,required"` 188 | } 189 | -------------------------------------------------------------------------------- /system/system_test_helpers.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | 13 | "golang.org/x/crypto/ssh" 14 | 15 | "github.com/PuerkitoBio/goquery" 16 | librssh "github.com/apcera/libretto/ssh" 17 | "github.com/apcera/libretto/virtualmachine/aws" 18 | "github.com/cloudfoundry-incubator/cf-test-helpers/cf" 19 | . "github.com/onsi/gomega" 20 | "github.com/onsi/gomega/gexec" 21 | "github.com/pivotalservices/gtils/command" 22 | "github.com/pivotalservices/gtils/osutils" 23 | errwrap "github.com/pkg/errors" 24 | ) 25 | 26 | func pushTestApp(config Config) { 27 | fmt.Println("Pushing test app...") 28 | 29 | cfDo("api", config.APIEndpoint, "--skip-ssl-validation") 30 | cfDo("auth", config.AdminUser, config.AdminPassword) 31 | cfDo("create-org", config.OrgName) 32 | cfDo("target", "-o", config.OrgName) 33 | cfDo("create-space", config.SpaceName) 34 | cfDo("target", "-s", config.SpaceName) 35 | cfDo("push", config.AppName, "-p", config.AppPath) 36 | 37 | fmt.Println("Done pushing test app.") 38 | } 39 | 40 | func getAuthMethod(pemkeycontents []byte) (authMethod []ssh.AuthMethod) { 41 | keySigner, _ := ssh.ParsePrivateKey(pemkeycontents) 42 | authMethod = []ssh.AuthMethod{ 43 | ssh.PublicKeys(keySigner), 44 | } 45 | return 46 | } 47 | 48 | func scpHelper(hostInfo HostInfo, localpath, remotepath string) { 49 | f, err := os.Open(localpath) 50 | Expect(err).ToNot(HaveOccurred()) 51 | var remoteConn *osutils.RemoteOperations 52 | if hostInfo.Password == "" { 53 | remoteConn = osutils.NewRemoteOperationsWithPath(command.SshConfig{ 54 | Username: hostInfo.Username, 55 | Host: hostInfo.Hostname, 56 | Port: 22, 57 | SSLKey: hostInfo.SSHKey, 58 | }, remotepath) 59 | } else { 60 | remoteConn = osutils.NewRemoteOperationsWithPath(command.SshConfig{ 61 | Username: hostInfo.Username, 62 | Host: hostInfo.Hostname, 63 | Password: hostInfo.Password, 64 | Port: 22, 65 | }, remotepath) 66 | } 67 | err = remoteConn.UploadFile(f) 68 | Expect(err).ToNot(HaveOccurred()) 69 | } 70 | 71 | type wrappedClientToEnableDebugging struct { 72 | innerClient command.ClientInterface 73 | session *ssh.Session 74 | outputWriter io.Writer 75 | } 76 | 77 | func (wc wrappedClientToEnableDebugging) NewSession() (command.SSHSession, error) { 78 | session, err := wc.innerClient.NewSession() 79 | if err != nil { 80 | return nil, err 81 | } 82 | sess := session.(*ssh.Session) 83 | sess.Setenv("LOG_LEVEL", "debug") 84 | wc.session = sess 85 | stderrReader, err := sess.StderrPipe() 86 | if err != nil { 87 | return nil, err 88 | } 89 | _, err = io.Copy(wc.outputWriter, stderrReader) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return sess, nil 94 | } 95 | 96 | func remoteExecute(hostInfo HostInfo, remotecommand string) ([]byte, error) { 97 | var authMethod []ssh.AuthMethod 98 | if hostInfo.Password == "" { 99 | keySigner, err := ssh.ParsePrivateKey([]byte(hostInfo.SSHKey)) 100 | if err != nil { 101 | return nil, errwrap.Wrap(err, "failed parsing private key") 102 | } 103 | 104 | authMethod = []ssh.AuthMethod{ 105 | ssh.PublicKeys(keySigner), 106 | } 107 | 108 | } else { 109 | authMethod = []ssh.AuthMethod{ 110 | ssh.Password(hostInfo.Password), 111 | } 112 | } 113 | clientconfig := &ssh.ClientConfig{ 114 | User: hostInfo.Username, 115 | Auth: authMethod, 116 | HostKeyCallback: ssh.InsecureIgnoreHostKey(), 117 | } 118 | client, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", hostInfo.Hostname, 22), clientconfig) 119 | if err != nil { 120 | return nil, errwrap.Wrap(err, "failed client dialing server") 121 | } 122 | defer client.Close() 123 | session, err := client.NewSession() 124 | if err != nil { 125 | return nil, errwrap.Wrap(err, "failed client generating new session") 126 | } 127 | defer session.Close() 128 | session.Setenv("LOG_LEVEL", "debug") 129 | 130 | resp, err := session.CombinedOutput(remotecommand) 131 | if err != nil { 132 | return resp, errwrap.Wrap(err, "combinedoutput call failed") 133 | } 134 | return resp, nil 135 | } 136 | 137 | func deleteTestApp(config Config) { 138 | fmt.Println("Deleting test app...") 139 | 140 | cfDo("api", config.APIEndpoint, "--skip-ssl-validation") 141 | cfDo("auth", config.AdminUser, config.AdminPassword) 142 | cfDo("target", "-o", config.OrgName, "-s", config.SpaceName) 143 | cfDo("delete", "-f", config.AppName) 144 | cfDo("delete-org", "-f", config.OrgName) 145 | 146 | fmt.Println("Done deleting test app.") 147 | } 148 | 149 | func createInstance(amznkeyname string, amiID string, securityGroup string) *aws.VM { 150 | fmt.Println("Creating AWS VM...") 151 | 152 | vm := &aws.VM{ 153 | Name: "cfops-test", 154 | AMI: amiID, 155 | InstanceType: "m3.large", 156 | SSHCreds: librssh.Credentials{ 157 | SSHUser: "ubuntu", 158 | SSHPrivateKey: amznkeyname, 159 | }, 160 | Volumes: []aws.EBSVolume{ 161 | { 162 | DeviceName: "/dev/sda1", 163 | VolumeSize: 100, 164 | }, 165 | }, 166 | Region: "eu-west-1", 167 | KeyPair: amznkeyname, 168 | SecurityGroups: []string{securityGroup}, 169 | } 170 | 171 | err := aws.ValidCredentials(vm.Region) 172 | Expect(err).NotTo(HaveOccurred()) 173 | 174 | err = vm.Provision() 175 | Expect(err).NotTo(HaveOccurred()) 176 | fmt.Println("AWS VM created.") 177 | 178 | return vm 179 | } 180 | 181 | func cfDo(cmd ...string) { 182 | Eventually(cf.Cf(cmd...), 300).Should(gexec.Exit(0), 183 | fmt.Sprintf("Command `cf %s` failed", cmd), 184 | ) 185 | } 186 | 187 | func createAdminUser(hostname, username, password string) { 188 | transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} 189 | client := &http.Client{Transport: transport} 190 | setupResp, err := client.Get(fmt.Sprintf("https://%s/setup", hostname)) 191 | Expect(err).NotTo(HaveOccurred()) 192 | 193 | doc, err := goquery.NewDocumentFromReader(setupResp.Body) 194 | Expect(err).NotTo(HaveOccurred()) 195 | 196 | token, exists := doc.Find(`input[name="authenticity_token"]`).First().Attr("value") 197 | Expect(exists).To(BeTrue()) 198 | 199 | data := url.Values{} 200 | data.Add("setup[user_name]", username) 201 | data.Add("setup[password]", password) 202 | data.Add("setup[password_confirmation]", password) 203 | data.Add("setup[eula_accepted]", "0") 204 | data.Add("setup[eula_accepted]", "true") 205 | data.Add("authenticity_token", token) 206 | 207 | makeUserRequest, err := http.NewRequest(http.MethodPost, "https://"+hostname+"/setup", bytes.NewBufferString(data.Encode())) 208 | Expect(err).NotTo(HaveOccurred()) 209 | for _, cookie := range setupResp.Cookies() { 210 | makeUserRequest.AddCookie(cookie) 211 | } 212 | 213 | resp, err := client.Do(makeUserRequest) 214 | body, _ := ioutil.ReadAll(resp.Body) 215 | Expect(err).NotTo(HaveOccurred(), string(body)) 216 | } 217 | 218 | //Config ... 219 | type Config struct { 220 | APIEndpoint string 221 | AdminUser string 222 | AdminPassword string 223 | AppName string 224 | OrgName string 225 | SpaceName string 226 | AppPath string 227 | OMAdminUser string 228 | OMAdminPassword string 229 | OMHostname string 230 | 231 | AmiID string 232 | SecurityGroup string 233 | OMHostInfo HostInfo 234 | } 235 | 236 | type HostInfo struct { 237 | Username string `json:"username"` 238 | Password string `json:"password"` 239 | Hostname string `json:"host"` 240 | SSHKey string 241 | } 242 | -------------------------------------------------------------------------------- /testCoverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | EXITCODE=0; 4 | WATERMARK=$2; 5 | TESTDIR=$1 6 | go test -cover ${TESTDIR} | { 7 | while read -r COVEROUTPUT;do 8 | echo ${COVEROUTPUT}; 9 | 10 | COVERLEVELS=`echo ${COVEROUTPUT} | grep -oh "[0-9]\{1,3\}\.[0-9]\{1,3\}\%"` 11 | for COVERLEVEL in ${COVERLEVELS};do 12 | echo ${COVERLEVEL} | tr "%" " "; 13 | COVERLEVEL=${COVERLEVEL/.*}; 14 | echo "Coverage - ${COVERLEVEL}%"; 15 | echo "Watermark- ${WATERMARK}%"; 16 | if [[ $COVERLEVEL -lt $WATERMARK ]]; then echo "!!!! FAIL !!!!"; EXITCODE=1;fi 17 | done 18 | EXITCODE=$EXITCODE 19 | done 20 | exit ${EXITCODE} 21 | } 22 | -------------------------------------------------------------------------------- /testrunner: -------------------------------------------------------------------------------- 1 | # Check if images are available locally 2 | DOCKER_LOCAL="" 3 | if [[ $(docker images|grep golang) ]]; then 4 | echo "Using previously cached docker images" 5 | DOCKER_LOCAL="--docker-local" 6 | fi 7 | rm -fR _builds _steps _projects _cache _temp 8 | wercker --verbose --environment ".testrunner_env_defaults" build --git-domain github.com --git-owner pivotalservices --git-repository cfops ${DOCKER_LOCAL} 9 | rm -fR _builds _steps _projects _cache _temp 10 | -------------------------------------------------------------------------------- /tile/.gitignore: -------------------------------------------------------------------------------- 1 | release/ 2 | product/ 3 | tile-history.yml 4 | -------------------------------------------------------------------------------- /tile/cfops.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: cfops 3 | label: CFOps, an automation tool for Pivotal CF 4 | description: A CLI tool for running backup and restore operations on Pivotal Cloud Foundry products 5 | icon_image: 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 6 | product_version: '0.0.1' 7 | metadata_version: '1.5' 8 | rank: 1 9 | stemcell_criteria: 10 | os: ubuntu-trusty 11 | requires_cpi: false 12 | version: '3146.5' 13 | releases: 14 | - file: cfops-0.0.1.tgz 15 | name: cfops 16 | version: '0.0.1' 17 | - file: docker-boshrelease-23.tgz 18 | name: docker 19 | version: '23' 20 | requires_product_versions: 21 | - name: cf 22 | version: "~> 1.5" 23 | provides_product_versions: 24 | - name: cfops 25 | version: '0.0.1' 26 | 27 | form_types: 28 | - name: backup 29 | label: CFOps Backup 30 | description: Properties for CFOps Backup 31 | property_inputs: 32 | - reference: .properties.backup_cron_expression 33 | label: Schedule Backup 34 | description: Schedule backups on a regular basis with a CRON expression. 35 | - reference: .properties.tile 36 | label: Tile 37 | description: Tile 38 | - reference: .properties.host 39 | label: Ops Manager Host 40 | description: Ops Manager Host 41 | - reference: .properties.opsman_user 42 | label: Ops Manager Username 43 | description: Ops Manager Username 44 | - reference: .properties.opsman_password 45 | label: Ops Manager Password 46 | description: Ops Manager Password 47 | - reference: .properties.opsman_ssh_user 48 | label: Ops Manager SSH User 49 | description: Ops Manager SSH User 50 | - reference: .properties.opsman_ssh_password 51 | label: Ops Manager SSH Password 52 | description: Ops Manager SSH Password 53 | - reference: .properties.s3_bucket_name 54 | label: S3 Bucket Name 55 | description: S3 Bucket Name 56 | - reference: .properties.s3_access_key_id 57 | label: S3 Access Key ID 58 | description: S3 Access Key ID 59 | - reference: .properties.s3_secret_access_key 60 | label: S3 Secret Access Key 61 | description: S3 Secret Access Key 62 | - reference: .properties.s3_active 63 | label: Enable S3 backup 64 | description: Enable S3 backup 65 | 66 | property_blueprints: 67 | - name: backup_cron_expression # Refer to this elsewhere as (( .properties.backup_cron_expression )) 68 | type: string 69 | default: "*/30 * * * *" 70 | configurable: true 71 | - name: tile # Refer to this elsewhere as (( .properties.tile )) 72 | type: dropdown_select 73 | configurable: true 74 | default: ops-manager 75 | options: 76 | - name: ops-manager 77 | label: Ops Manager 78 | - name: elastic-runtime 79 | label: Elastic Runtime 80 | - name: host # Refer to this elsewhere as (( .properties.host )) 81 | type: string 82 | configurable: true 83 | - name: opsman_user # Refer to this elsewhere as (( .properties.opsman_user )) 84 | type: string 85 | configurable: true 86 | - name: opsman_password # Refer to this elsewhere as (( .properties.opsman_password )) 87 | type: string 88 | configurable: true 89 | - name: opsman_ssh_user # Refer to this elsewhere as (( .properties.opsman_ssh_user )) 90 | type: string 91 | configurable: true 92 | - name: opsman_ssh_password # Refer to this elsewhere as (( .properties.opsman_ssh_password )) 93 | type: string 94 | configurable: true 95 | - name: s3_bucket_name # Refer to this elsewhere as (( .properties.s3_bucket_name )) 96 | type: string 97 | configurable: true 98 | - name: s3_access_key_id # Refer to this elsewhere as (( .properties.s3_access_key_id )) 99 | type: string 100 | configurable: true 101 | - name: s3_secret_access_key # Refer to this elsewhere as (( .properties.s3_secret_access_key )) 102 | type: string 103 | configurable: true 104 | - name: s3_active # Refer to this elsewhere as (( .properties.s3_active )) 105 | type: boolean 106 | configurable: true 107 | default: true 108 | 109 | job_types: 110 | - name: compilation 111 | resource_label: compilation 112 | resource_definitions: 113 | - name: ram 114 | type: integer 115 | configurable: true 116 | default: 2048 117 | - name: ephemeral_disk 118 | type: integer 119 | configurable: true 120 | default: 5120 121 | - name: persistent_disk 122 | type: integer 123 | configurable: true 124 | default: 0 125 | - name: cpu 126 | type: integer 127 | configurable: true 128 | default: 2 129 | static_ip: 0 130 | dynamic_ip: 1 131 | max_in_flight: 1 132 | instance_definitions: 133 | - name: instances 134 | type: integer 135 | default: 1 136 | - name: docker-image-uploader-docker_cfops 137 | resource_label: docker-image-uploader-docker_cfops 138 | errand: false 139 | templates: 140 | - name: containers 141 | release: docker 142 | - name: docker 143 | release: docker 144 | - name: docker-image-uploader-docker_cfops 145 | release: cfops 146 | resource_definitions: 147 | - name: ram 148 | type: integer 149 | configurable: false 150 | default: 4096 151 | - name: ephemeral_disk 152 | type: integer 153 | configurable: false 154 | default: 4096 155 | - name: persistent_disk 156 | type: integer 157 | configurable: false 158 | default: 2048 159 | - name: cpu 160 | type: integer 161 | configurable: false 162 | default: 1 163 | static_ip: 1 164 | dynamic_ip: 0 165 | max_in_flight: 1 166 | instance_definitions: 167 | - name: instances 168 | type: integer 169 | configurable: true 170 | default: 1 171 | property_blueprints: 172 | - name: vm_credentials 173 | type: salted_credentials 174 | default: 175 | identity: vcap 176 | - name: app_credentials 177 | resource_definitions: 178 | - name: ram 179 | type: integer 180 | configurable: false 181 | default: 4096 182 | - name: ephemeral_disk 183 | type: integer 184 | configurable: false 185 | default: 4096 186 | - name: persistent_disk 187 | type: integer 188 | configurable: false 189 | default: 2048 190 | - name: cpu 191 | type: integer 192 | configurable: false 193 | default: 1 194 | static_ip: 1 195 | dynamic_ip: 0 196 | max_in_flight: 1 197 | instance_definitions: 198 | - name: instances 199 | type: integer 200 | configurable: true 201 | default: 1 202 | property_blueprints: 203 | - name: vm_credentials 204 | type: salted_credentials 205 | default: 206 | identity: vcap 207 | - name: app_credentials 208 | type: salted_credentials 209 | manifest: | 210 | containers: 211 | - name: cfops 212 | image: malston/cfops 213 | env_vars: 214 | - CFOPS_HOST: (( .properties.host.value )) 215 | - CFOPS_ADMIN_USER: (( .properties.opsman_user.value )) 216 | - CFOPS_ADMIN_PASS: (( .properties.opsman_password.value )) 217 | - CFOPS_OM_USER: (( .properties.opsman_ssh_user.value )) 218 | - CFOPS_OM_PASS: (( .properties.opsman_ssh_password.value )) 219 | - CFOPS_TILE: (( .properties.tile.value )) 220 | - CFOPS_CRON: (( .properties.backup_cron_expression.value )) 221 | - S3_ACCESS_KEY_ID: (( .properties.s3_access_key_id.value )) 222 | - S3_SECRET_ACCESS_KEY: (( .properties.s3_secret_access_key.value )) 223 | - S3_BUCKET_NAME: (( .properties.s3_bucket_name.value )) 224 | - S3_ACTIVE: (( .properties.s3_active.value )) 225 | -------------------------------------------------------------------------------- /tile/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vmware-archive/cfops/ffce6023ccf4b0f952fe534c3c8b6d58aa16f919/tile/resources/icon.png -------------------------------------------------------------------------------- /tile/tile.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: cfops 3 | icon_file: resources/icon.png 4 | label: CFOps, an automation tool for Pivotal CF 5 | description: A CLI tool for running backup and restore operations on Pivotal Cloud Foundry products 6 | 7 | packages: 8 | - name: docker-cfops 9 | type: docker-bosh 10 | image: malston/cfops 11 | # files: 12 | # - path: resources/ubuntu_image.tgz 13 | cpu: 1 14 | memory: 4096 15 | ephemeral_disk: 4096 16 | persistent_disk: 2048 17 | instances: 1 18 | manifest: | 19 | containers: 20 | - name: cfops 21 | image: malston/cfops 22 | env_vars: 23 | - "CFOPS_HOST=(( .properties.host.value ))" 24 | - "CFOPS_ADMIN_USER=(( .properties.opsman_user.value ))" 25 | - "CFOPS_ADMIN_PASS=(( .properties.opsman_password.value ))" 26 | - "CFOPS_OM_USER=(( .properties.opsman_ssh_user.value ))" 27 | - "CFOPS_OM_PASS=(( .properties.opsman_ssh_password.value ))" 28 | - "CFOPS_TILE=(( .properties.tile.value ))" 29 | - "CFOPS_CRON=(( .properties.backup_cron_expression.value ))" 30 | - "S3_ACCESS_KEY_ID=(( .properties.s3_access_key_id.value ))" 31 | - "S3_SECRET_ACCESS_KEY=(( .properties.s3_secret_access_key.value ))" 32 | - "S3_BUCKET_NAME=(( .properties.s3_bucket_name.value ))" 33 | - "S3_ACTIVE=(( .properties.s3_active.value ))" 34 | 35 | forms: 36 | - name: backup 37 | label: CFOps Backup 38 | description: Properties for CFOps Backup 39 | properties: 40 | - name: backup_cron_expression 41 | type: string 42 | label: Schedule Backup 43 | default: "*/30 * * * *" 44 | description: Schedule backups on a regular basis with a CRON expression. 45 | - name: tile 46 | type: dropdown_select 47 | label: Tile 48 | options: 49 | - name: ops-manager 50 | label: Ops Manager 51 | default: true 52 | - name: elastic-runtime 53 | label: Elastic Runtime 54 | - name: host 55 | type: string 56 | label: Ops Manager Host 57 | - name: opsman_user 58 | type: string 59 | label: Ops Manager Username 60 | - name: opsman_password 61 | type: string 62 | label: Ops Manager Password 63 | - name: opsman_ssh_user 64 | type: string 65 | label: Ops Manager SSH User 66 | - name: opsman_ssh_password 67 | type: string 68 | label: Ops Manager SSH Password 69 | - name: s3_bucket_name 70 | type: string 71 | label: Ops Manager SSH Password 72 | - name: s3_access_key_id 73 | type: string 74 | label: Ops Manager SSH Password 75 | - name: s3_secret_access_key 76 | type: string 77 | label: Ops Manager SSH Password 78 | - name: s3_active 79 | type: boolean 80 | label: Enable S3 backup 81 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: golang 2 | build: 3 | # The steps that will be executed on build 4 | steps: 5 | # Sets the go workspace and places you package 6 | # at the right place in the workspace tree 7 | - setup-go-workspace 8 | 9 | # Get the dependencies 10 | - script: 11 | name: go get 12 | code: | 13 | export GO15VENDOREXPERIMENT=1 14 | cd $WERCKER_SOURCE_DIR 15 | go version 16 | go get github.com/Masterminds/glide 17 | export PATH=$WERCKER_SOURCE_DIR/bin:$PATH 18 | glide install 19 | 20 | - script: 21 | name: setup ssh for integration tests 22 | code: | 23 | sudo apt-get update 24 | sudo apt-get install -y openssh-server 25 | service ssh start 26 | 27 | # Test the project 28 | - script: 29 | name: go test 30 | code: | 31 | mkdir -p /var/vcap/store 32 | go test $(glide novendor | grep -v system) -v -race 33 | 34 | # Setting the coverage watermark low. 35 | # This should be raised as we gain more coverage... 36 | # Test coverage for the project 37 | - script: 38 | name: go test cover 39 | code: | 40 | ./testCoverage $(glide novendor | grep -v system) $COVERAGE_WATERMARK 41 | 42 | # lets make sure we can build 43 | # the main executable (later we can cross compile and upload) 44 | - script: 45 | name: go smoke build 46 | code: | 47 | (cd cmd/cfops && go build) 48 | 49 | - script: 50 | name: add repo to artifact 51 | code: | 52 | cp -R ./ ${WERCKER_OUTPUT_DIR} 53 | 54 | - script: 55 | name: set release id variable and version 56 | code: | 57 | go get github.com/xchapter7x/versioning 58 | export NEXT_VERSION=`versioning bump_patch` 59 | echo "next version should be: ${NEXT_VERSION}" 60 | 61 | - script: 62 | name: cross platform release 63 | code: | 64 | (cd cmd/cfops/ && GOOS=linux GOARCH=amd64 go build -ldflags "-X main.VERSION=${NEXT_VERSION}" && mkdir -p ${WERCKER_OUTPUT_DIR}/${BUILD_DIR}/linux64 && cp cfops ${WERCKER_OUTPUT_DIR}/cfops_linux64) 65 | deploy: 66 | steps: 67 | - script: 68 | name: install-packages 69 | code: | 70 | sudo apt-get install -y openssh-client wget 71 | ls -la 72 | pwd 73 | echo ${WERCKER_OUTPUT_DIR} 74 | ls -la ${WERCKER_OUTPUT_DIR} 75 | 76 | - wercker/add-ssh-key@1.0.2: 77 | keyname: PCF_GITHUB_KEY 78 | 79 | - wercker/add-to-known_hosts@1.4.0: 80 | hostname: github.com 81 | fingerprint: SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8 82 | 83 | - script: 84 | name: set release id variable for version 85 | code: | 86 | go get github.com/xchapter7x/versioning 87 | export WERCKER_GITHUB_CREATE_RELEASE_ID=`versioning bump_patch` 88 | 89 | - xchapter7x/flowy-release: 90 | action: "get-latest" 91 | tag_variable_name: "VERSION_TAG" 92 | git_name: $GITFLOW_NAME 93 | git_email: $GITFLOW_EMAIL 94 | 95 | - xchapter7x/flowy-release: 96 | action: "complete-release" 97 | active: $GITFLOW_ACTIVE 98 | git_name: $GITFLOW_NAME 99 | git_email: $GITFLOW_EMAIL 100 | 101 | #this is a workaround for the flowy-release not resetting 102 | #its branch state... 103 | - script: 104 | name: reset git to proper commit 105 | code: | 106 | git checkout -fq ${WERCKER_GIT_COMMIT} 107 | git submodule update --init --recursive 108 | 109 | - github-create-release: 110 | token: $GITHUB_TOKEN 111 | tag: $WERCKER_GITHUB_CREATE_RELEASE_ID 112 | title: CFOPS $WERCKER_GITHUB_CREATE_RELEASE_ID 113 | draft: $RELEASE_DRAFT 114 | 115 | - github-upload-asset: 116 | token: $GITHUB_TOKEN 117 | file: cfops_linux64 118 | release_id: $WERCKER_GITHUB_CREATE_RELEASE_ID 119 | content-type: application/octet-stream 120 | 121 | - s3sync: 122 | key-id: $S3_KEY 123 | key-secret: $S3_SECRET 124 | bucket-url: ${S3_BUCKET}/${S3_FOLDER}/linux64/${VERSION_TAG}/ 125 | source-dir: ./${BUILD_DIR}/linux64 126 | delete-removed: false 127 | --------------------------------------------------------------------------------