blob: 8b2f0e44c15d03e9b2d7051410b22795c44c1e5d [file] [log] [blame]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +00001/**
2 *
3 * Run openscap xccdf evaluation on given nodes
4 *
5 * Expected parametes:
6 * SALT_MASTER_URL Full Salt API address.
7 * SALT_MASTER_CREDENTIALS Credentials to the Salt API.
8 *
9 * XCCDF_BENCHMARKS_DIR The XCCDF benchmarks base directory (default /usr/share/xccdf-benchmarks/mirantis/)
10 * XCCDF_BENCHMARKS List of pairs XCCDF benchmark filename and corresponding profile separated with ','
11 * these pairs are separated with semicolon.
12 * (e.g. manila/openstack_manila-xccdf.xml,profilename;horizon/openstack_horizon-xccdf.xml,profile)
13 * XCCDF_VERSION The XCCDF version (default 1.2)
14 * XCCDF_TAILORING_ID The tailoring id (default None)
15 *
16 * TARGET_SERVERS The target Salt nodes (default *)
17 *
18 * ARTIFACTORY_URL The artifactory URL
19 * ARTIFACTORY_NAMESPACE The artifactory namespace (default 'mirantis/openscap')
20 * ARTIFACTORY_REPO The artifactory repo (default 'binary-dev-local')
21 *
22 * UPLOAD_TO_DASHBOARD Boolean. Upload results to the WORP or not
23 * DASHBOARD_API_URL The WORP api base url. Mandatory if UPLOAD_TO_DASHBOARD is true
24 */
25
26
27
28/**
29 * Upload results to the `WORP` dashboard
30 *
31 * @param apiUrl The base dashboard api url
32 * @param cloudName The cloud name (mostly, the given node's domain name)
33 * @param nodeName The node name
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030034 * @param reportType Type of the report to create/use, either 'openscap' or 'cve'
35 * @param reportId Report Id to re-use, if empty report will be created
36 * @param results The scanning results as a json file content (string)
37 * @return reportId The Id of the report created if incoming reportId was empty, otherwise incoming reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000038 */
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030039def uploadResultToDashboard(apiUrl, cloudName, nodeName, reportType, reportId, results) {
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +030040 def common = new com.mirantis.mk.Common()
41 def http = new com.mirantis.mk.Http()
42
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000043 // Yes, we do not care of performance and will create at least 4 requests per each result
44 def requestData = [:]
45
46 def cloudId
47 def nodeId
48
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030049 def worpApi = [:]
50 worpApi["url"] = apiUrl
51
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000052 // Let's take a look, may be our minion is already presented on the dashboard
53 // Get available environments
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030054 common.infoMsg("Making GET to ${worpApi.url}/environment/")
55 environments = http.restGet(worpApi, "/environment/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000056 for (environment in environments) {
57 if (environment['name'] == cloudName) {
58 cloudId = environment['uuid']
59 break
60 }
61 }
62 // Cloud wasn't presented, let's create it
63 if (! cloudId ) {
64 // Create cloud
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030065 requestData = [:]
66 requestData['name'] = cloudName
67 common.infoMsg("Making POST to ${worpApi.url}/environment/ with ${requestData}")
68 cloudId = http.restPost(worpApi, "/environment/", requestData)['env']['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000069
70 // And the node
71 // It was done here to reduce count of requests to the api.
72 // Because if there was not cloud presented on the dashboard, then the node was not presented as well.
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030073 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000074 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030075 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
76 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000077 }
78
79 if (! nodeId ) {
80 // Get available nodes in our environment
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030081 common.infoMsg("Making GET to ${worpApi.url}/environment/${cloudId}/nodes/")
82 nodes = http.restGet(worpApi, "/environment/${cloudId}/nodes/")
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000083 for (node in nodes) {
84 if (node['name'] == nodeName) {
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030085 nodeId = node['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000086 break
87 }
88 }
89 }
90
91 // Node wasn't presented, let's create it
92 if (! nodeId ) {
93 // Create node
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030094 requestData = [:]
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000095 requestData['nodes'] = [nodeName]
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +030096 common.infoMsg("Making PUT to ${worpApi.url}/environment/${cloudId}/nodes/ with ${requestData}")
97 nodeId = http.restCall(worpApi, "/environment/${cloudId}/nodes/", "PUT", requestData)['uuid']
Vasyl Saienko4e8ec642018-09-17 10:08:08 +000098 }
99
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300100 // Create report if needed
101 if (! reportId ) {
102 requestData = [:]
103 requestData['env_uuid'] = cloudId
104 common.infoMsg("Making POST to ${worpApi.url}/reports/${reportType}/ with ${requestData}")
105 reportId = http.restPost(worpApi, "/reports/${reportType}/", requestData)['report']['uuid']
106 }
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000107
108 // Upload results
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300109 // NOTE(pas-ha) results should already be a dict with 'results' key
110 requestData = common.parseJSON(results)
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000111 requestData['node_name'] = nodeName
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300112 common.infoMsg("First result in results to PUT is ${requestData['results'][0]}")
113 // NOTE(pas-ha) not logging whole results to be sent, is too large and just spams the logs
114 common.infoMsg("Making PUT to ${worpApi.url}/reports/${reportType}/${reportId}/ with node name ${requestData['node_name']} and results")
115 http.restCall(worpApi, "/reports/${reportType}/${reportId}/", "PUT", requestData)
116 return reportId
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000117}
118
119
120node('python') {
121 def pepperEnv = 'pepperEnv'
122
123 // XCCDF related variables
124 def benchmarksAndProfilesArray = XCCDF_BENCHMARKS.tokenize(';')
125 def benchmarksDir = XCCDF_BENCHMARKS_DIR ?: '/usr/share/xccdf-benchmarks/mirantis/'
126 def xccdfVersion = XCCDF_VERSION ?: '1.2'
127 def xccdfTailoringId = XCCDF_TAILORING_ID ?: 'None'
128 def targetServers = TARGET_SERVERS ?: '*'
129
130 def salt = new com.mirantis.mk.Salt()
131 def python = new com.mirantis.mk.Python()
132 def common = new com.mirantis.mk.Common()
133 def http = new com.mirantis.mk.Http()
134
135 // To have an ability to work in heavy concurrency conditions
136 def scanUUID = UUID.randomUUID().toString()
137
138 def artifactsArchiveName = "openscap-${scanUUID}.zip"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300139 def resultsBaseDir = "/var/log/openscap/${scanUUID}"
140 def artifactsDir = "openscap"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000141
142 def liveMinions
143
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300144
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000145 stage ('Setup virtualenv for Pepper') {
146 python.setupPepperVirtualenv(pepperEnv, SALT_MASTER_URL, SALT_MASTER_CREDENTIALS)
147 }
148
149 stage ('Run openscap xccdf evaluation and attempt to upload the results to a dashboard') {
150 liveMinions = salt.getMinions(pepperEnv, targetServers)
151
152 if (liveMinions.isEmpty()) {
153 throw new Exception('There are no alive minions')
154 }
155
156 common.infoMsg("Scan UUID: ${scanUUID}")
157
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300158 // Clean all results before proceeding with results from every minion
159 dir(artifactsDir) {
160 deleteDir()
161 }
162
Pavlo Shchelokovskyyf1b99b02018-10-02 20:31:17 +0300163 def reportId
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300164 // Iterate oscap evaluation over the benchmarks
165 for (benchmark in benchmarksAndProfilesArray) {
166 def (benchmarkFilePath, profile) = benchmark.tokenize(',').collect({it.trim()})
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000167
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300168 // Remove extension from the benchmark name
169 def benchmarkPathWithoutExtension = benchmarkFilePath.replaceFirst('[.][^.]+$', '')
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000170
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300171 // Get benchmark name
172 def benchmarkName = benchmarkPathWithoutExtension.tokenize('/')[-1]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300173
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300174 // And build resultsDir based on this path
175 def resultsDir = "${resultsBaseDir}/${benchmarkPathWithoutExtension}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300176
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300177 def benchmarkFile = "${benchmarksDir}${benchmarkFilePath}"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000178
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300179 // Evaluate the benchmark on all minions at once
180 salt.runSaltProcessStep(pepperEnv, targetServers, 'oscap.eval', [
181 'xccdf', benchmarkFile, "results_dir=${resultsDir}",
182 "profile=${profile}", "xccdf_version=${xccdfVersion}",
183 "tailoring_id=${xccdfTailoringId}"
184 ])
185
186 // fetch, store and publish results one by one
187 for (minion in liveMinions) {
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000188
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300189 def nodeShortName = minion.tokenize('.')[0]
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300190 def archiveName = "${scanUUID}_${nodeShortName}_${benchmarkName}.tar"
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300191 def localResultsDir = "${artifactsDir}/${scanUUID}/${nodeShortName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300192
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300193 // TODO(pas-ha) when using Salt >= 2017.7.0, use file.read(path) module
194 // also investigate compressing to gz/xz and reading with binary=True (for less traffic)
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300195 salt.cmdRun(pepperEnv, minion, "tar -cf /tmp/${archiveName} -C ${resultsBaseDir} .")
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300196 // NOTE(pas-ha) salt.getFileContent does not pass extra args to cmdRun that we need
197 fileContents = salt.getFileContent(pepperEnv, minion, "/tmp/${archiveName}", true, null, false)
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300198
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300199 sh "mkdir -p ${localResultsDir}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300200 writeFile file: "${archiveName}", text: fileContents
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300201 sh "tar --strip-components 1 -xf ${archiveName} --directory ${localResultsDir}; rm -f ${archiveName}"
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300202
203 // Remove archive which is not needed anymore
204 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', "/tmp/${archiveName}")
205
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000206 // Attempt to upload the scanning results to the dashboard
207 if (UPLOAD_TO_DASHBOARD.toBoolean()) {
208 if (common.validInputParam('DASHBOARD_API_URL')) {
209 def cloudName = salt.getGrain(pepperEnv, minion, 'domain')['return'][0].values()[0].values()[0]
Pavlo Shchelokovskyy82117e42018-10-03 19:48:56 +0300210 def nodeResults = readFile "${localResultsDir}/${benchmarkPathWithoutExtension}/results.json"
211 reportId = uploadResultToDashboard(DASHBOARD_API_URL, cloudName, minion, "openscap", reportId, nodeResults)
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000212 } else {
213 throw new Exception('Uploading to the dashboard is enabled but the DASHBOARD_API_URL was not set')
214 }
215 }
216 }
217 }
Ivan Udovichenkod1bd28c2018-10-02 12:41:04 +0300218
219 // Prepare archive
220 sh "tar -cJf ${artifactsDir}.tar.xz ${artifactsDir}"
221
222 // Archive the build output artifacts
223 archiveArtifacts artifacts: "*.xz"
Vasyl Saienko4e8ec642018-09-17 10:08:08 +0000224 }
225
226/* // Will be implemented later
227 stage ('Attempt to upload results to an artifactory') {
228 if (common.validInputParam('ARTIFACTORY_URL')) {
229 for (minion in liveMinions) {
230 def destDir = "${artifactsDir}/${minion}"
231 def archiveName = "openscap-${scanUUID}.tar.gz"
232 def tempArchive = "/tmp/${archiveName}"
233 def destination = "${destDir}/${archiveName}"
234
235 dir(destDir) {
236 // Archive scanning results on the remote target
237 salt.runSaltProcessStep(pepperEnv, minion, 'archive.tar', ['czf', tempArchive, resultsBaseDir])
238
239 // Get it content and save it
240 writeFile file: destination, text: salt.getFileContent(pepperEnv, minion, tempArchive)
241
242 // Remove scanning results and the temp archive on the remote target
243 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', resultsBaseDir)
244 salt.runSaltProcessStep(pepperEnv, minion, 'file.remove', tempArchive)
245 }
246 }
247
248 def artifactory = new com.mirantis.mcp.MCPArtifactory()
249 def artifactoryName = 'mcp-ci'
250 def artifactoryRepo = ARTIFACTORY_REPO ?: 'binary-dev-local'
251 def artifactoryNamespace = ARTIFACTORY_NAMESPACE ?: 'mirantis/openscap'
252 def artifactoryServer = Artifactory.server(artifactoryName)
253 def publishInfo = true
254 def buildInfo = Artifactory.newBuildInfo()
255 def zipName = "${env.WORKSPACE}/openscap/${scanUUID}/results.zip"
256
257 // Zip scan results
258 zip zipFile: zipName, archive: false, dir: artifactsDir
259
260 // Mandatory and additional properties
261 def properties = artifactory.getBinaryBuildProperties([
262 "scanUuid=${scanUUID}",
263 "project=openscap"
264 ])
265
266 // Build Artifactory spec object
267 def uploadSpec = """{
268 "files":
269 [
270 {
271 "pattern": "${zipName}",
272 "target": "${artifactoryRepo}/${artifactoryNamespace}/openscap",
273 "props": "${properties}"
274 }
275 ]
276 }"""
277
278 // Upload artifacts to the given Artifactory
279 artifactory.uploadBinariesToArtifactory(artifactoryServer, buildInfo, uploadSpec, publishInfo)
280
281 } else {
282 common.warningMsg('ARTIFACTORY_URL was not given, skip uploading to artifactory')
283 }
284 }
285*/
286
287}