├── .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 | [](https://app.wercker.com/project/bykey/d0a50d426b77a9f73da0fe4f383ad624) [](http://godoc.org/github.com/pivotalservices/cfops) [](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 |
--------------------------------------------------------------------------------