/
a1upgrade_compat.go
657 lines (552 loc) · 19.2 KB
/
a1upgrade_compat.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
// Copyright © 2017 Chef Software
package a1upgrade
import (
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
"github.com/pkg/errors"
"github.com/chef/automate/lib/platform/pg"
)
const fipsDetectedMsg = `
* FAILURE: Detected unsupported configuration settings in your Chef Automate v1 installation:
fips['enable']=true - Chef Automate v2 does not yet support FIPS operating mode
You may ignore this warning by passing the --skip-fips-check flag
to the upgrade command and running again.
`
const samlDetectedMsg = `
* WARNING: SAML config in your Chef Automate v1 installation detected:
Chef Automate v2 cannot migrate existing SAML settings due to incompatibilities
between v1 and v2 SAML functionality.
You may stop this upgrade and run:
chef-automate migrate-from-v1 gen-config --skip-saml-check
This upgrade command will generate a configuration for your Chef Automate v2
installation and offer you the opportunity to edit it. The SAML section of
"Configuring Chef Automate" explains how to configure SAML for Chef Automate v2.
https://automate.chef.io/docs/configuration/#saml
Alternatively, you may ignore this warning and continue this upgrade by entering 'y' below.
You may set up SAML for Chef Automate v2 anytime after upgrading.`
const noBackupConfiguredMsg = `
* FAILURE: No backup settings are configured for your Chef Automate v1 installation.
We recommend that you backup all production installations of Chef Automate v1
before upgrading to v2. The upgrade process will apply irreversible
data transformations, so a backup will be the only way to revert to Chef
Automate v1 in some cases.
If this is a non-production system or if you use a different method of backing
up your Chef Automate installation, you may add the --skip-backup-check flag to the
upgrade command and try again.
`
const externalESEnabledMsg = `
* FAILURE: Your Chef Automate v1 installation is configured to use an external Elasticsearch
cluster.
Automated upgrade for this configuration is in development. To upgrade an external
Elasticsearch cluster, please reach out to a Customer Success or Customer Support
representative for assistance.
`
const externalESMayBeEnabledMsg = `
* FAILURE: Your Chef Automate v1 installation may be configured to use an external Elasticsearch
cluster.
Automated upgrade for this configuration is in development. To upgrade an external
Elasticsearch cluster, please reach out to a Customer Success or Customer Support
representative for assistance.
If you are **NOT** using an external Elasticsearch cluster, you may restart your
upgrade with the --skip-external-es-check flag to proceed.
`
const proxyEnabledMsg = `
* WARNING: Your Chef Automate v1 configuration includes proxy settings (delivery[proxy]) in your v1 config).
Later in the upgrade process, we will generate a configuration for your Chef Automate v2
installation and offer you the opportunity to edit it. You will need to add the following
settings to your configuration and follow the directions to continue:
[global.v1.proxy]
host = "<your proxy host>"
port = <your proxy port>
# Uncomment the following lines and set the parameters as necessary.
# user = "<your proxy user>"
# password = "<your proxy password>"
# no_proxy = ["whitelist", "should", "include", "packages.chef.io","raw.githubusercontent.com", "bldr.habitat.sh"]
`
const disasterRecoveryEnabledMsg = `
* FAILURE: Your Chef Automate v1 configuration sets delivery['primary_ip'] and/or
delivery['standby_ip'] for disaster recovery.
Chef Automate v2 does not support disaster recovery. If you still wish to upgrade to Chef Automate v2,
restart your upgrade with the --skip-disaster-recovery-check flag.
`
const workflowInUseMsg = `
* FAILURE: The Workflow directory in your Chef Automate v1 installation
(delivery['git_repos'] in your v1 config) is not empty. Chef Automate v2 does not support
Workflow. If you still wish to upgrade to Chef Automate v2, restart your upgrade with the
--skip-workflow-check flag.
`
const workflowDirMissingMsg = `
* FAILURE: The Workflow directory in your Chef Automate v1 installation
(delivery['git_repos'] in your v1 config) does not exist.
Please create or configure an existing directory and try again.
`
const marketplaceImageMsg = `
* FAILURE: This system appears to be a cloud marketplace image.
Chef Automate v2 does not support cloud marketplace images.
`
const invalidChefServerPostgresConfigMsg = `
* FAILURE: You have enabled Chef Server upgrade, but your Chef Server
configuration is not supported for the integrated upgrade. The integrated
upgrade only supports the all-in-one topology but your Chef Server
Postgresql configuration does not match this topology. The integrated
upgrade requires the following postres configuration:
* Postgresql is not disabled
* Chef Server is configured for external Postgresql
* Chef Server shares Chef Automate v1's Postgresql (identical vip and port)
If you wish to migrate to the Chef Automate v2 integrated Chef Server
configuration, first run the upgrade from v1 without Chef Server, then
enable Chef Server in Automate v2, and then migrate your Chef Server data
separately.
`
const invalidChefServerSearchConfigMsg = `
* FAILURE: You have enabled Chef Server upgrade, but your Chef Server
configuration is not supported for the integrated upgrade. The integrated
upgrade only supports the all-in-one topology but your Chef Server
opscode-solr4 configuration does not match this topology. The integrated
upgrade requires the following search configuration:
* opscode_erchef search_provider is set to "elasticsearch"
* opscode_erchef search_queue_mode is set to "batch"
* 'opscode-solr4' is not disabled
* 'opscode-solr4' is set to external
* The 'opscode-solr4' external URL matches Chef Automate v1's nginx_proxy_url
If you wish to migrate to the Chef Automate v2 integrated Chef Server
configuration, first run the upgrade from v1 without Chef Server, then
enable Chef Server in Automate v2, and then migrate your Chef Server data
separately.
`
const invalidChefServerBookshelfConfigMsg = `
* FAILURE: You have enabled Chef Server upgrade, but your Chef Server
configuration is not supported for the integrated upgrade. The integrated
upgrade only supports the all-in-one topology but your Chef Server
bookshelf configuration does not match this topology. The integrated
upgrade requires the following search configuration:
* bookshelf is enabled
* bookshelf uses storage type "sql"
If you wish to migrate to the Chef Automate v2 integrated Chef Server
configuration, first run the upgrade from v1 without Chef Server, then
enable Chef Server in Automate v2, and then migrate your Chef Server data
separately.
`
const chefServerAddOnInUseFmt = `
* FAILURE: You have the Chef Server add-on %s installed.
The Chef Automate v2 integrated Chef Server does not support this add-on.
If you are not using the add-on, you may uninstall the add-on and retry
the upgrade command to continue.
`
const ocIDInUseMsg = `
* FAILURE: You have enabled chef server upgrade, but your chef server
has one or more applications configured for oc_id authentication. The
Chef Automate v2 integrated Chef Server does not include the oc_id
application. If you wish to continue, remove the oc_id applications from
your Chef Server configuration and retry the upgrade.
`
var chefServerAddOns = []string{
"chef-manage",
"opscode-push-jobs-server",
"opscode-reporting",
"opscode-analytics",
}
const OmnibusInstallRoot = "/opt"
type CompatChecker struct {
Warnings int
Failures int
Msgs strings.Builder
}
func NewCompatChecker() CompatChecker {
var b strings.Builder
return CompatChecker{
Warnings: 0,
Failures: 0,
Msgs: b,
}
}
type CompatCheckerSkips struct {
BackupCheck bool
DisasterRecoveryCheck bool
ExternalESCheck bool
FIPSCheck bool
SAMLCheck bool
WorkflowCheck bool
}
// @afiune delete me when workflow feature is completed, as well as the skip flags
func (s *CompatCheckerSkips) SkipWorkflowCheck() {
s.WorkflowCheck = true
}
func (c *CompatChecker) RunAutomateChecks(a1Config *A1Config, skip CompatCheckerSkips) error {
fips := a1Config.DeliveryRunning.Delivery.FIPS.Enabled
workflow := a1Config.DeliveryRunning.Delivery.Delivery.GitRepos
esClusterUrls := a1Config.DeliveryRunning.Delivery.Opensearch.ClusterURLS
proxyHost := a1Config.DeliveryRunning.Delivery.Delivery.Proxy.Host
backupRetention := a1Config.DeliveryRunning.Delivery.Backup.Retention.Enabled
backupType := a1Config.DeliveryRunning.Delivery.Backup.Type
backupLocation := a1Config.DeliveryRunning.Delivery.Backup.Location
backupS3Bucket := a1Config.DeliveryRunning.Delivery.Backup.S3Bucket
primaryIP := a1Config.DeliveryRunning.Delivery.Delivery.PrimaryIp
standbyIP := a1Config.DeliveryRunning.Delivery.Delivery.StandbyIp
if !skip.BackupCheck {
err := c.BackupConfigured(backupType, backupRetention, backupLocation, backupS3Bucket)
if err != nil {
return err
}
}
if !skip.DisasterRecoveryCheck {
err := c.DisasterRecoveryConfigured(primaryIP, standbyIP)
if err != nil {
return err
}
}
if !skip.ExternalESCheck {
err := c.ExternalElasticsearchConfigured(esClusterUrls)
if err != nil {
return err
}
}
if !skip.FIPSCheck {
err := c.FipsConfigured(fips)
if err != nil {
return err
}
}
if !skip.SAMLCheck {
// connect to database to check for SAML config in A1
port, err := a1Config.DeliveryRunning.Delivery.PostgreSQL.Port.Int64()
if err != nil {
return errors.Wrap(err, "Could not parse PostgreSQL port from Automate 1 configuration\n")
}
connInfo := &pg.A1ConnInfo{
User: a1Config.DeliveryRunning.Delivery.PostgreSQL.SuperuserUsername,
Pass: a1Config.DeliverySecrets.Postgresql.SuperuserPassword,
Host: a1Config.DeliveryRunning.Delivery.PostgreSQL.Vip,
Port: uint64(port),
}
saml, err := CheckSAMLConfig(connInfo)
if err != nil {
return errors.Wrap(err, "Could not check for existing SAML config. Retry the upgrade or pass --skip-saml-check to the upgrade command to skip this step.")
}
err = c.SAMLConfigured(saml)
if err != nil {
return err
}
}
c.ProxyConfigured(proxyHost)
if !skip.WorkflowCheck {
err := c.WorkflowInUse(workflow)
if err != nil {
return err
}
}
// We do not provide a `--skip-*` flag for this check because it's unlikely
// to provide any value: we do not have marketplace support for A2; in
// particular we do not have any implementation of the billing/licensing
// systems used in marketplace images for A2. So any upgrade would just leave
// the customer broken from a licensing standpoint (and possibly in other
// ways as well).
err := c.RunningMarketplaceImage(OmnibusInstallRoot)
if err != nil {
return err
}
return nil
}
func (c *CompatChecker) RunChefServerChecks(a1Config *A1Config) error {
deliveryRunning := a1Config.DeliveryRunning.Delivery
a1EsConfig := &deliveryRunning.Opensearch
a1PgConfig := &deliveryRunning.PostgreSQL
chefServerConfig := a1Config.ChefServerRunning.PrivateChef
erchefConfig := &chefServerConfig.OpscodeErchef
csPostgresConfig := &chefServerConfig.Postgresql
csSearchConfig := &chefServerConfig.OpscodeSolr4
bookshelfConfig := &chefServerConfig.Bookshelf
ocIDConfig := &chefServerConfig.OcID
err := c.ChefServerElasticsearchConfigValid(csSearchConfig, a1EsConfig, erchefConfig)
if err != nil {
return err
}
err = c.ChefServerPostgresConfigValid(csPostgresConfig, a1PgConfig)
if err != nil {
return err
}
err = c.ChefServerBookshelfConfigValid(bookshelfConfig)
if err != nil {
return err
}
err = c.UnsupportedCSAddOnsNotUsed(OmnibusInstallRoot)
if err != nil {
return err
}
err = c.OcIDNotUsed(ocIDConfig)
if err != nil {
return err
}
return nil
}
func (c *CompatChecker) FipsConfigured(fips bool) error {
if !fips {
return nil
}
_, err := c.Msgs.WriteString(fipsDetectedMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) SAMLConfigured(saml bool) error {
if Failure(ConfigConflictSAML) {
fmt.Fprintln(os.Stderr, samlDetectedMsg)
return errors.New("simulated SAML conflict")
}
if !saml {
return nil
}
_, err := c.Msgs.WriteString(samlDetectedMsg)
if err != nil {
return err
}
c.Warnings++
return nil
}
func (c *CompatChecker) ExternalElasticsearchConfigured(clusterURLS []string) error {
if len(clusterURLS) > 1 {
// If clusterURLS contains > 1 elements, elasticsearch is external.
_, err := c.Msgs.WriteString(externalESEnabledMsg)
if err != nil {
return err
}
c.Failures++
return nil
} else {
// TODO: verify elasticsearch_urls in the A1 config is always set to something.
// This assumes it is.
thing, err := url.Parse(clusterURLS[0])
if err != nil {
return err
}
hostname := thing.Hostname()
if hostname == "127.0.0.1" || hostname == "localhost" {
return nil
}
_, err = c.Msgs.WriteString(externalESMayBeEnabledMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
}
func (c *CompatChecker) ProxyConfigured(proxy string) {
if proxy == "" {
return
}
c.Msgs.WriteString(proxyEnabledMsg)
c.Warnings++
}
func (c *CompatChecker) BackupConfigured(backupType string, retention bool, backupLocation string, backupS3Bucket string) error {
// A1 configures local backups by default, so the presence of a backup configuration
// is not sufficient. We think people are probably doing backups if one of the following is true:
// 1. They have local backups configured and their local backup directory backupLocation exists and is non-empty.
// 2. They have s3 backups configured and an backupS3Bucket configured.
// 3. They have a retention (backup cron) configured.
// local backup case
_, err := os.Stat(backupLocation)
if err != nil {
// If the error is that the file does not exist, keep going.
if !os.IsNotExist(err) {
return err
}
} else {
dircheck, derr := directoryEmpty(backupLocation)
if derr != nil {
return derr
}
if backupType == "fs" && !dircheck {
return nil
}
}
// s3 backup case
if backupType == "s3" && backupS3Bucket != "" {
return nil
}
// retention case
if retention {
return nil
}
_, err = c.Msgs.WriteString(noBackupConfiguredMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) DisasterRecoveryConfigured(primary string, standby string) error {
if primary == "" && standby == "" {
return nil
}
_, err := c.Msgs.WriteString(disasterRecoveryEnabledMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
// RunWorkflowChecks run all the workflow checks to verify compatibility with A2
//
// TODO: @afiune here we can verify everything wee need to verify,
// correct config, existing git-repos, valid ssh-keys, etc.
func (c *CompatChecker) RunWorkflowChecks(a1Config *A1Config) error {
workflowGitDir := a1Config.DeliveryRunning.Delivery.Delivery.GitRepos
err := c.WorkflowGitReposValid(workflowGitDir)
if err != nil {
return err
}
return nil
}
// WorkflowGitReposValid
func (c *CompatChecker) WorkflowGitReposValid(gitReposDir string) error {
_, err := os.Stat(gitReposDir)
if os.IsNotExist(err) {
_, err = c.Msgs.WriteString(workflowDirMissingMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
// @afiune Do we care if the git_repos dir is empty?
return nil
}
// WorkflowInUse
func (c *CompatChecker) WorkflowInUse(workflowDir string) error {
// The workflow dir `git_repos` is always set in the A1 config.
// We look to see whether the configured directory contains anything
// to decide if workflow is in use.
_, err := os.Stat(workflowDir)
if os.IsNotExist(err) {
return nil
}
dircheck, err := directoryEmpty(workflowDir)
if err != nil {
return err
}
if dircheck {
return nil
}
_, err = c.Msgs.WriteString(workflowInUseMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) RunningMarketplaceImage(omnibusRoot string) error {
marketplacePkgPath := path.Join(omnibusRoot, "chef-marketplace")
_, err := os.Stat(marketplacePkgPath)
if os.IsNotExist(err) {
return nil
}
_, err = c.Msgs.WriteString(marketplaceImageMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) ChefServerElasticsearchConfigValid(csEsConfig *OpscodeSolr4,
a1EsConfig *DeliveryRunningOpensearch,
csErchefConfig *OpscodeErchef) error {
esEnabled := csEsConfig.Enable
esExternal := csEsConfig.External
csURL, err := url.Parse(csEsConfig.ExternalURL)
if err != nil {
return err
}
a1URL, err := url.Parse(a1EsConfig.NginxProxyURL)
if err != nil {
return err
}
URLsMatch := ((csURL.Host == a1URL.Host) &&
(csURL.Port() == a1URL.Port()) &&
(path.Clean(csURL.EscapedPath()) == path.Clean(a1URL.EscapedPath())))
erchefUsesElastic := (csErchefConfig.SearchProvider == "elasticsearch")
searchQueueModeIsBatch := (csErchefConfig.SearchQueueMode == "batch")
if esEnabled && esExternal && URLsMatch && erchefUsesElastic && searchQueueModeIsBatch {
return nil
}
_, err = c.Msgs.WriteString(invalidChefServerSearchConfigMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) ChefServerPostgresConfigValid(csPgConfig *CSRPostgreSQL, a1PgConfig *DeliveryRunningPostgreSQL) error {
pgEnabled := csPgConfig.Enable
pgExternal := csPgConfig.External
VIPsMatch := (csPgConfig.Vip == a1PgConfig.Vip)
PortsMatch := (csPgConfig.Port == a1PgConfig.Port)
if pgEnabled && pgExternal && VIPsMatch && PortsMatch {
return nil
}
_, err := c.Msgs.WriteString(invalidChefServerPostgresConfigMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) ChefServerBookshelfConfigValid(csBookshelfConfig *Bookshelf) error {
bookshelfEnabled := csBookshelfConfig.Enable
bookshelfUsesSQL := (csBookshelfConfig.StorageType == "sql")
if bookshelfEnabled && bookshelfUsesSQL {
return nil
}
_, err := c.Msgs.WriteString(invalidChefServerBookshelfConfigMsg)
if err != nil {
return err
}
c.Failures++
return nil
}
func (c *CompatChecker) UnsupportedCSAddOnsNotUsed(omnibusRoot string) error {
for _, addOn := range chefServerAddOns {
addOnPath := path.Join(omnibusRoot, addOn)
_, err := os.Stat(addOnPath)
if os.IsNotExist(err) {
continue
}
msg := fmt.Sprintf(chefServerAddOnInUseFmt, addOn)
_, err = c.Msgs.WriteString(msg)
if err != nil {
return err
}
c.Failures++
}
return nil
}
func (c *CompatChecker) OcIDNotUsed(ocIDConfig *OcID) error {
ocIDApps := ocIDConfig.Applications
if len(ocIDApps) != 0 {
_, err := c.Msgs.WriteString(ocIDInUseMsg)
if err != nil {
return err
}
c.Failures++
}
return nil
}
func directoryEmpty(dirName string) (bool, error) {
// Assumes directory exists
f, err := os.Open(dirName)
if err != nil {
return false, err
}
defer f.Close()
_, err = f.Readdirnames(1)
if err == io.EOF {
return true, nil
}
return false, err
}