Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckIndexes
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 13
1406
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
90
 checkMetastore
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 checkIndex
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 in
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 out
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 check
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 err
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 printErrorRecursive
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getIndexMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIndexRoutingTable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 ensureClusterStateFetched
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 ensureCirrusInfoFetched
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace CirrusSearch\Maintenance;
4
5use CirrusSearch\MetaStore\MetaStoreIndex;
6
7/**
8 * Check that all Cirrus indexes report OK.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License along
21 * with this program; if not, write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
23 * http://www.gnu.org/copyleft/gpl.html
24 */
25
26$IP = getenv( 'MW_INSTALL_PATH' );
27if ( $IP === false ) {
28    $IP = __DIR__ . '/../../..';
29}
30require_once "$IP/maintenance/Maintenance.php";
31require_once __DIR__ . '/../includes/Maintenance/Maintenance.php';
32
33class CheckIndexes extends Maintenance {
34    /**
35     * @var array[] Nested array of arrays containing error strings. Individual
36     *  errors are nested based on the keys in self::$path at the time the error
37     *  occurred.
38     */
39    private $errors = [];
40    /**
41     * @var string[] Represents each step of current indentation level
42     */
43    private $path;
44    /**
45     * @var array Result of querying elasticsearch _cluster/state api endpoint
46     */
47    private $clusterState;
48    /**
49     * @var array[] Version info stored in elasticsearch /mw_cirrus_versions/version
50     */
51    private $cirrusInfo;
52
53    public function __construct() {
54        parent::__construct();
55        $this->addDescription( 'Check that all Cirrus indexes report OK. This always operates on ' .
56            'a single cluster.' );
57
58        $this->addOption( 'nagios', 'Output in nagios format' );
59    }
60
61    public function execute() {
62        if ( $this->hasOption( 'nagios' ) ) {
63            // Force silent running mode so we can match Nagios expected output.
64            $this->mQuiet = true;
65        }
66        $this->requireCirrusReady();
67        $this->ensureClusterStateFetched();
68        $this->ensureCirrusInfoFetched();
69        // @todo: use MetaStoreIndex
70        $aliases = [];
71        foreach ( $this->clusterState[ 'metadata' ][ 'indices' ] as $indexName => $data ) {
72            foreach ( $data[ 'aliases' ] as $alias ) {
73                $aliases[ $alias ][] = $indexName;
74            }
75        }
76        $this->checkMetastore( $aliases );
77        foreach ( $this->cirrusInfo as $alias => $data ) {
78            foreach ( $aliases[ $alias ] as $indexName ) {
79                $this->checkIndex( $indexName, $data[ 'shard_count'] );
80            }
81        }
82        $indexCount = count( $this->cirrusInfo );
83        $errCount = count( $this->errors );
84        if ( $this->hasOption( 'nagios' ) ) {
85            // Exit silent running mode so we can log Nagios style output
86            $this->mQuiet = false;
87            if ( $errCount > 0 ) {
88                $this->output( "CIRRUSSEARCH CRITICAL - $indexCount indexes report $errCount errors\n" );
89            } else {
90                $this->output( "CIRRUSSEARCH OK - $indexCount indexes report 0 errors\n" );
91            }
92        }
93        $this->printErrorRecursive( '', $this->errors );
94        // If there are error use the nagios error codes to signal them
95        if ( $errCount > 0 ) {
96            die( 2 );
97        }
98
99        return true;
100    }
101
102    private function checkMetastore( array $aliases ) {
103        $this->in( MetaStoreIndex::INDEX_NAME );
104        if ( isset( $aliases[ MetaStoreIndex::INDEX_NAME ] ) ) {
105            $this->check( 'alias count', 1, count( $aliases[ MetaStoreIndex::INDEX_NAME ] ) );
106            foreach ( $aliases[ MetaStoreIndex::INDEX_NAME ] as $indexName ) {
107                $this->checkIndex( $indexName, 1 );
108            }
109        } else {
110            $this->err( 'does not exist' );
111        }
112        $this->out();
113    }
114
115    /**
116     * @param string $indexName
117     * @param int $expectedShardCount
118     */
119    private function checkIndex( $indexName, $expectedShardCount ) {
120        $metadata = $this->getIndexMetadata( $indexName );
121        $this->in( $indexName );
122        if ( $metadata === null ) {
123            $this->err( 'does not exist' );
124            $this->out();
125            return;
126        }
127        $this->check( 'state', 'open', $metadata[ 'state' ] );
128        // TODO check aliases
129
130        $routingTable = $this->getIndexRoutingTable( $indexName );
131        $this->check( 'shard count', $expectedShardCount, count( $routingTable[ 'shards' ] ) );
132        foreach ( $routingTable[ 'shards' ] as $shardIndex => $shardRoutingTable ) {
133            $this->in( "shard $shardIndex" );
134            foreach ( $shardRoutingTable as $replicaIndex => $replica ) {
135                $this->in( "replica $replicaIndex" );
136                $this->check( 'state', [ 'STARTED', 'RELOCATING' ], $replica[ 'state' ] );
137                $this->out();
138            }
139            $this->out();
140        }
141        $this->out();
142    }
143
144    /**
145     * @param string $header
146     */
147    private function in( $header ) {
148        $this->path[] = $header;
149        $this->output( str_repeat( "\t", count( $this->path ) - 1 ) );
150        $this->output( "$header...\n" );
151    }
152
153    private function out() {
154        array_pop( $this->path );
155    }
156
157    /**
158     * @param string $name
159     * @param mixed $expected
160     * @param mixed $actual
161     */
162    private function check( $name, $expected, $actual ) {
163        $this->output( str_repeat( "\t", count( $this->path ) ) );
164        $this->output( "$name..." );
165        if ( is_array( $expected ) ) {
166            if ( in_array( $actual, $expected ) ) {
167                $this->output( "ok\n" );
168            } else {
169                $expectedStr = implode( ', ', $expected );
170                $this->output( "$actual not in [$expectedStr]\n" );
171                $this->err( "expected $name to be in [$expectedStr] but was $actual" );
172            }
173        } else {
174            if ( $expected === $actual ) {
175                $this->output( "ok\n" );
176            } else {
177                $this->output( "$expected != $actual\n" );
178                $this->err( "expected $name to be '$expected' but was '$actual'" );
179            }
180        }
181    }
182
183    /**
184     * @param string $explanation
185     */
186    private function err( $explanation ) {
187        $e = &$this->errors;
188        foreach ( $this->path as $element ) {
189            $e = &$e[ $element ];
190        }
191        $e[] = $explanation;
192    }
193
194    /**
195     * @param string $indent Prefix to attach before each line of output
196     * @param array $array
197     */
198    private function printErrorRecursive( $indent, array $array ) {
199        foreach ( $array as $key => $value ) {
200            $line = $indent;
201            if ( !is_numeric( $key ) ) {
202                $line .= "$key...";
203            }
204            if ( is_array( $value ) ) {
205                $this->error( $line );
206                $this->printErrorRecursive( "$indent\t", $value );
207            } else {
208                $line .= $value;
209                if ( $this->hasOption( 'nagios' ) ) {
210                    $this->output( "$line\n" );
211                } else {
212                    $this->error( $line );
213                }
214            }
215        }
216    }
217
218    /**
219     * @param string $indexName fully qualified name of elasticsearch index
220     * @return array|null Index metadata from elasticsearch cluster state
221     */
222    private function getIndexMetadata( $indexName ) {
223        return $this->clusterState['metadata']['indices'][$indexName] ?? null;
224    }
225
226    /**
227     * @param string $indexName fully qualified name of elasticsearch index
228     * @return array
229     */
230    private function getIndexRoutingTable( $indexName ) {
231        return $this->clusterState[ 'routing_table' ][ 'indices' ][ $indexName ];
232    }
233
234    private function ensureClusterStateFetched() {
235        if ( $this->clusterState === null ) {
236            $this->clusterState = $this->getConnection()->getClient()
237                ->request( '_cluster/state' )->getData();
238        }
239    }
240
241    private function ensureCirrusInfoFetched() {
242        if ( $this->cirrusInfo === null ) {
243            $store = $this->getMetaStore()->versionStore();
244            $this->cirrusInfo = [];
245            foreach ( $store->findAll() as $r ) {
246                $data = $r->getData();
247                $this->cirrusInfo[ $data['index_name'] ] = [
248                    'shard_count' => $data[ 'shard_count' ],
249                ];
250            }
251        }
252    }
253}
254
255$maintClass = CheckIndexes::class;
256require_once RUN_MAINTENANCE_IF_MAIN;