glusterfs

Форк
0
/
peer_eventsapi.py 
669 строк · 20.2 Кб
1
#!/usr/bin/python3
2
# -*- coding: utf-8 -*-
3
#
4
#  Copyright (c) 2016 Red Hat, Inc. <http://www.redhat.com>
5
#  This file is part of GlusterFS.
6
#
7
#  This file is licensed to you under your choice of the GNU Lesser
8
#  General Public License, version 3 or any later version (LGPLv3 or
9
#  later), or the GNU General Public License, version 2 (GPLv2), in all
10
#  cases as published by the Free Software Foundation.
11
#
12

13
from __future__ import print_function
14
import os
15
import json
16
from errno import EEXIST
17
import fcntl
18
from errno import EACCES, EAGAIN
19
import signal
20
import sys
21
import time
22

23
import requests
24
from prettytable import PrettyTable
25

26
from gluster.cliutils import (Cmd, node_output_ok, node_output_notok,
27
                              sync_file_to_peers, GlusterCmdException,
28
                              output_error, execute_in_peers, runcli,
29
                              set_common_args_func)
30
from gfevents.utils import LockedOpen, get_jwt_token, save_https_cert, NamedTempOpen
31

32
from gfevents.eventsapiconf import (WEBHOOKS_FILE_TO_SYNC,
33
                                    WEBHOOKS_FILE,
34
                                    DEFAULT_CONFIG_FILE,
35
                                    CUSTOM_CONFIG_FILE,
36
                                    CUSTOM_CONFIG_FILE_TO_SYNC,
37
                                    EVENTSD,
38
                                    CONFIG_KEYS,
39
                                    BOOL_CONFIGS,
40
                                    INT_CONFIGS,
41
                                    PID_FILE,
42
                                    RESTART_CONFIGS,
43
                                    ERROR_INVALID_CONFIG,
44
                                    ERROR_WEBHOOK_NOT_EXISTS,
45
                                    ERROR_CONFIG_SYNC_FAILED,
46
                                    ERROR_WEBHOOK_ALREADY_EXISTS,
47
                                    ERROR_PARTIAL_SUCCESS,
48
                                    ERROR_ALL_NODES_STATUS_NOT_OK,
49
                                    ERROR_SAME_CONFIG,
50
                                    ERROR_WEBHOOK_SYNC_FAILED,
51
                                    CERTS_DIR)
52

53

54
def handle_output_error(err, errcode=1, json_output=False):
55
    if json_output:
56
        print (json.dumps({
57
            "output": "",
58
            "error": err
59
            }))
60
        sys.exit(errcode)
61
    else:
62
        output_error(err, errcode)
63

64

65
def file_content_overwrite(fname, data):
66
    with open(fname + ".tmp", "w") as f:
67
        f.write(json.dumps(data))
68

69
    os.rename(fname + ".tmp", fname)
70

71

72
def create_custom_config_file_if_not_exists(args):
73
    try:
74
        config_dir = os.path.dirname(CUSTOM_CONFIG_FILE)
75
        mkdirp(config_dir)
76
    except OSError as e:
77
        handle_output_error("Failed to create dir %s: %s" % (config_dir, e),
78
                            json_output=args.json)
79

80
    if not os.path.exists(CUSTOM_CONFIG_FILE):
81
        with NamedTempOpen(CUSTOM_CONFIG_FILE, "w") as f:
82
            f.write(json.dumps({}))
83

84

85
def create_webhooks_file_if_not_exists(args):
86
    try:
87
        webhooks_dir = os.path.dirname(WEBHOOKS_FILE)
88
        mkdirp(webhooks_dir)
89
    except OSError as e:
90
        handle_output_error("Failed to create dir %s: %s" % (webhooks_dir, e),
91
                            json_output=args.json)
92

93
    if not os.path.exists(WEBHOOKS_FILE):
94
        with NamedTempOpen(WEBHOOKS_FILE, "w") as f:
95
           f.write(json.dumps({}))
96

97

98
def boolify(value):
99
    val = False
100
    if value.lower() in ["enabled", "true", "on", "yes"]:
101
        val = True
102
    return val
103

104

105
def mkdirp(path, exit_on_err=False, logger=None):
106
    """
107
    Try creating required directory structure
108
    ignore EEXIST and raise exception for rest of the errors.
109
    Print error in stderr and exit
110
    """
111
    try:
112
        os.makedirs(path)
113
    except OSError as e:
114
        if e.errno != EEXIST or not os.path.isdir(path):
115
            raise
116

117

118
def is_active():
119
    state = False
120
    try:
121
        with open(PID_FILE, "a+") as f:
122
            fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
123
            state = False
124
    except (IOError, OSError) as e:
125
        if e.errno in (EACCES, EAGAIN):
126
            # cannot grab. so, process still running..move on
127
            state = True
128
        else:
129
            state = False
130
    return state
131

132

133
def reload_service():
134
    pid = None
135
    if is_active():
136
        with open(PID_FILE) as f:
137
            try:
138
                pid = int(f.read().strip())
139
            except ValueError:
140
                pid = None
141
        if pid is not None:
142
            os.kill(pid, signal.SIGUSR2)
143

144
    return (0, "", "")
145

146

147
def rows_to_json(json_out, column_name, rows):
148
    num_ok_rows = 0
149
    for row in rows:
150
        num_ok_rows += 1 if row.ok else 0
151
        json_out.append({
152
            "node": row.hostname,
153
            "node_status": "UP" if row.node_up else "DOWN",
154
            column_name: "OK" if row.ok else "NOT OK",
155
            "error": row.error
156
        })
157
    return num_ok_rows
158

159

160
def rows_to_table(table, rows):
161
    num_ok_rows = 0
162
    for row in rows:
163
        num_ok_rows += 1 if row.ok else 0
164
        table.add_row([row.hostname,
165
                       "UP" if row.node_up else "DOWN",
166
                       "OK" if row.ok else "NOT OK: {0}".format(
167
                           row.error)])
168
    return num_ok_rows
169

170

171
def sync_to_peers(args):
172
    if os.path.exists(WEBHOOKS_FILE):
173
        try:
174
            sync_file_to_peers(WEBHOOKS_FILE_TO_SYNC)
175
        except GlusterCmdException as e:
176
            # Print stdout if stderr is empty
177
            errmsg = e.args[0][2] if e.args[0][2] else e.args[0][1]
178
            handle_output_error("Failed to sync Webhooks file: [Error: {0}]"
179
                                "{1}".format(e.args[0][0], errmsg),
180
                                errcode=ERROR_WEBHOOK_SYNC_FAILED,
181
                                json_output=args.json)
182

183
    if os.path.exists(CUSTOM_CONFIG_FILE):
184
        try:
185
            sync_file_to_peers(CUSTOM_CONFIG_FILE_TO_SYNC)
186
        except GlusterCmdException as e:
187
            # Print stdout if stderr is empty
188
            errmsg = e.args[0][2] if e.args[0][2] else e.args[0][1]
189
            handle_output_error("Failed to sync Config file: [Error: {0}]"
190
                                "{1}".format(e.args[0][0], errmsg),
191
                                errcode=ERROR_CONFIG_SYNC_FAILED,
192
                                json_output=args.json)
193

194
    out = execute_in_peers("node-reload")
195
    if not args.json:
196
        table = PrettyTable(["NODE", "NODE STATUS", "SYNC STATUS"])
197
        table.align["NODE STATUS"] = "r"
198
        table.align["SYNC STATUS"] = "r"
199

200
    json_out = []
201
    if args.json:
202
        num_ok_rows = rows_to_json(json_out, "sync_status", out)
203
    else:
204
        num_ok_rows = rows_to_table(table, out)
205

206
    ret = 0
207
    if num_ok_rows == 0:
208
        ret = ERROR_ALL_NODES_STATUS_NOT_OK
209
    elif num_ok_rows != len(out):
210
        ret = ERROR_PARTIAL_SUCCESS
211

212
    if args.json:
213
        print (json.dumps({
214
            "output": json_out,
215
            "error": ""
216
        }))
217
    else:
218
        print (table)
219

220
    # If sync status is not ok for any node set error code as partial success
221
    sys.exit(ret)
222

223

224
def node_output_handle(resp):
225
    rc, out, err = resp
226
    if rc == 0:
227
        node_output_ok(out)
228
    else:
229
        node_output_notok(err)
230

231

232
def action_handle(action, json_output=False):
233
    out = execute_in_peers("node-" + action)
234
    column_name = action.upper()
235
    if action == "status":
236
        column_name = EVENTSD.upper()
237

238
    if not json_output:
239
        table = PrettyTable(["NODE", "NODE STATUS", column_name + " STATUS"])
240
        table.align["NODE STATUS"] = "r"
241
        table.align[column_name + " STATUS"] = "r"
242

243
    json_out = []
244
    if json_output:
245
        rows_to_json(json_out, column_name.lower() + "_status", out)
246
    else:
247
        rows_to_table(table, out)
248

249
    return json_out if json_output else table
250

251

252
class NodeReload(Cmd):
253
    name = "node-reload"
254

255
    def run(self, args):
256
        node_output_handle(reload_service())
257

258

259
class ReloadCmd(Cmd):
260
    name = "reload"
261

262
    def run(self, args):
263
        out = action_handle("reload", args.json)
264
        if args.json:
265
            print (json.dumps({
266
                "output": out,
267
                "error": ""
268
            }))
269
        else:
270
            print (out)
271

272

273
class NodeStatus(Cmd):
274
    name = "node-status"
275

276
    def run(self, args):
277
        node_output_ok("UP" if is_active() else "DOWN")
278

279

280
class StatusCmd(Cmd):
281
    name = "status"
282

283
    def run(self, args):
284
        webhooks = {}
285
        if os.path.exists(WEBHOOKS_FILE):
286
            webhooks = json.load(open(WEBHOOKS_FILE))
287

288
        json_out = {"webhooks": [], "data": []}
289
        if args.json:
290
            json_out["webhooks"] = webhooks.keys()
291
        else:
292
            print ("Webhooks: " + ("" if webhooks else "None"))
293
            for w in webhooks:
294
                print (w)
295

296
            print ()
297

298
        out = action_handle("status", args.json)
299
        if args.json:
300
            json_out["data"] = out
301
            print (json.dumps({
302
                "output": json_out,
303
                "error": ""
304
            }))
305
        else:
306
            print (out)
307

308

309
class WebhookAddCmd(Cmd):
310
    name = "webhook-add"
311

312
    def args(self, parser):
313
        parser.add_argument("url", help="URL of Webhook")
314
        parser.add_argument("--bearer_token", "-t", help="Bearer Token",
315
                            default="")
316
        parser.add_argument("--secret", "-s",
317
                            help="Secret to add JWT Bearer Token", default="")
318

319
    def run(self, args):
320
        create_webhooks_file_if_not_exists(args)
321

322
        with LockedOpen(WEBHOOKS_FILE, 'r+'):
323
            data = json.load(open(WEBHOOKS_FILE))
324
            if data.get(args.url, None) is not None:
325
                handle_output_error("Webhook already exists",
326
                                    errcode=ERROR_WEBHOOK_ALREADY_EXISTS,
327
                                    json_output=args.json)
328

329
            data[args.url] = {"token": args.bearer_token,
330
                              "secret": args.secret}
331
            file_content_overwrite(WEBHOOKS_FILE, data)
332

333
        sync_to_peers(args)
334

335

336
class WebhookModCmd(Cmd):
337
    name = "webhook-mod"
338

339
    def args(self, parser):
340
        parser.add_argument("url", help="URL of Webhook")
341
        parser.add_argument("--bearer_token", "-t", help="Bearer Token",
342
                            default="")
343
        parser.add_argument("--secret", "-s",
344
                            help="Secret to add JWT Bearer Token", default="")
345

346
    def run(self, args):
347
        create_webhooks_file_if_not_exists(args)
348

349
        with LockedOpen(WEBHOOKS_FILE, 'r+'):
350
            data = json.load(open(WEBHOOKS_FILE))
351
            if data.get(args.url, None) is None:
352
                handle_output_error("Webhook does not exists",
353
                                    errcode=ERROR_WEBHOOK_NOT_EXISTS,
354
                                    json_output=args.json)
355

356
            if isinstance(data[args.url], str):
357
                data[args.url]["token"] = data[args.url]
358

359
            if args.bearer_token != "":
360
                data[args.url]["token"] = args.bearer_token
361

362
            if args.secret != "":
363
                data[args.url]["secret"] = args.secret
364

365
            file_content_overwrite(WEBHOOKS_FILE, data)
366

367
        sync_to_peers(args)
368

369

370
class WebhookDelCmd(Cmd):
371
    name = "webhook-del"
372

373
    def args(self, parser):
374
        parser.add_argument("url", help="URL of Webhook")
375

376
    def run(self, args):
377
        create_webhooks_file_if_not_exists(args)
378

379
        with LockedOpen(WEBHOOKS_FILE, 'r+'):
380
            data = json.load(open(WEBHOOKS_FILE))
381
            if data.get(args.url, None) is None:
382
                handle_output_error("Webhook does not exists",
383
                                    errcode=ERROR_WEBHOOK_NOT_EXISTS,
384
                                    json_output=args.json)
385

386
            del data[args.url]
387
            file_content_overwrite(WEBHOOKS_FILE, data)
388

389
        sync_to_peers(args)
390

391

392
class NodeWebhookTestCmd(Cmd):
393
    name = "node-webhook-test"
394

395
    def args(self, parser):
396
        parser.add_argument("url")
397
        parser.add_argument("bearer_token")
398
        parser.add_argument("secret")
399

400
    def run(self, args):
401
        http_headers = {}
402
        hashval = ""
403
        if args.bearer_token != ".":
404
            hashval = args.bearer_token
405

406
        if args.secret != ".":
407
            hashval = get_jwt_token(args.secret, "TEST", int(time.time()))
408

409
        if hashval:
410
            http_headers["Authorization"] = "Bearer " + hashval
411

412
        urldata = requests.utils.urlparse(args.url)
413
        parts = urldata.netloc.split(":")
414
        domain = parts[0]
415
        # Default https port if not specified
416
        port = 443
417
        if len(parts) == 2:
418
            port = int(parts[1])
419

420
        cert_path = os.path.join(CERTS_DIR, args.url.replace("/", "_").strip())
421
        verify = True
422
        while True:
423
            try:
424
                resp = requests.post(args.url, headers=http_headers,
425
                                     verify=verify)
426
                # Successful webhook push
427
                break
428
            except requests.exceptions.SSLError as e:
429
                # If verify is equal to cert path, but still failed with
430
                # SSLError, Looks like some issue with custom downloaded
431
                # certificate, Try with verify = false
432
                if verify == cert_path:
433
                    verify = False
434
                    continue
435

436
                # If verify is instance of bool and True, then custom cert
437
                # is required, download the cert and retry
438
                try:
439
                    save_https_cert(domain, port, cert_path)
440
                    verify = cert_path
441
                except Exception:
442
                    verify = False
443

444
                # Done with collecting cert, continue
445
                continue
446
            except Exception as e:
447
                node_output_notok("{0}".format(e))
448
                break
449

450
        if resp.status_code != 200:
451
            node_output_notok("{0}".format(resp.status_code))
452

453
        node_output_ok()
454

455

456
class WebhookTestCmd(Cmd):
457
    name = "webhook-test"
458

459
    def args(self, parser):
460
        parser.add_argument("url", help="URL of Webhook")
461
        parser.add_argument("--bearer_token", "-t", help="Bearer Token")
462
        parser.add_argument("--secret", "-s",
463
                            help="Secret to generate Bearer Token")
464

465
    def run(self, args):
466
        url = args.url
467
        bearer_token = args.bearer_token
468
        secret = args.secret
469

470
        if not args.url:
471
            url = "."
472
        if not args.bearer_token:
473
            bearer_token = "."
474
        if not args.secret:
475
            secret = "."
476

477
        out = execute_in_peers("node-webhook-test", [url, bearer_token,
478
                                                     secret])
479

480
        if not args.json:
481
            table = PrettyTable(["NODE", "NODE STATUS", "WEBHOOK STATUS"])
482
            table.align["NODE STATUS"] = "r"
483
            table.align["WEBHOOK STATUS"] = "r"
484

485
        num_ok_rows = 0
486
        json_out = []
487
        if args.json:
488
            num_ok_rows = rows_to_json(json_out, "webhook_status", out)
489
        else:
490
            num_ok_rows = rows_to_table(table, out)
491

492
        ret = 0
493
        if num_ok_rows == 0:
494
            ret = ERROR_ALL_NODES_STATUS_NOT_OK
495
        elif num_ok_rows != len(out):
496
            ret = ERROR_PARTIAL_SUCCESS
497

498
        if args.json:
499
            print (json.dumps({
500
                "output": json_out,
501
                "error": ""
502
            }))
503
        else:
504
            print (table)
505

506
        sys.exit(ret)
507

508

509
class ConfigGetCmd(Cmd):
510
    name = "config-get"
511

512
    def args(self, parser):
513
        parser.add_argument("--name", help="Config Name")
514

515
    def run(self, args):
516
        data = json.load(open(DEFAULT_CONFIG_FILE))
517
        if os.path.exists(CUSTOM_CONFIG_FILE):
518
            data.update(json.load(open(CUSTOM_CONFIG_FILE)))
519

520
        if args.name is not None and args.name not in CONFIG_KEYS:
521
            handle_output_error("Invalid Config item",
522
                                errcode=ERROR_INVALID_CONFIG,
523
                                json_output=args.json)
524

525
        if args.json:
526
            json_out = {}
527
            if args.name is None:
528
                json_out = data
529
            else:
530
                json_out[args.name] = data[args.name]
531

532
            print (json.dumps({
533
                "output": json_out,
534
                "error": ""
535
            }))
536
        else:
537
            table = PrettyTable(["NAME", "VALUE"])
538
            if args.name is None:
539
                for k, v in data.items():
540
                    table.add_row([k, v])
541
            else:
542
                table.add_row([args.name, data[args.name]])
543

544
            print (table)
545

546

547
def read_file_content_json(fname):
548
    content = "{}"
549
    with open(fname) as f:
550
        content = f.read()
551
        if content.strip() == "":
552
            content = "{}"
553

554
    return json.loads(content)
555

556

557
class ConfigSetCmd(Cmd):
558
    name = "config-set"
559

560
    def args(self, parser):
561
        parser.add_argument("name", help="Config Name")
562
        parser.add_argument("value", help="Config Value")
563

564
    def run(self, args):
565
        if args.name not in CONFIG_KEYS:
566
            handle_output_error("Invalid Config item",
567
                                errcode=ERROR_INVALID_CONFIG,
568
                                json_output=args.json)
569

570
        create_custom_config_file_if_not_exists(args)
571

572
        with LockedOpen(CUSTOM_CONFIG_FILE, 'r+'):
573
            data = json.load(open(DEFAULT_CONFIG_FILE))
574
            if os.path.exists(CUSTOM_CONFIG_FILE):
575
                config_json = read_file_content_json(CUSTOM_CONFIG_FILE)
576
                data.update(config_json)
577

578
            # Do Nothing if same as previous value
579
            if data[args.name] == args.value:
580
                handle_output_error("Config value not changed. Same config",
581
                                    errcode=ERROR_SAME_CONFIG,
582
                                    json_output=args.json)
583

584
            # TODO: Validate Value
585
            new_data = read_file_content_json(CUSTOM_CONFIG_FILE)
586

587
            v = args.value
588
            if args.name in BOOL_CONFIGS:
589
                v = boolify(args.value)
590

591
            if args.name in INT_CONFIGS:
592
                v = int(args.value)
593

594
            new_data[args.name] = v
595
            file_content_overwrite(CUSTOM_CONFIG_FILE, new_data)
596

597
            # If any value changed which requires restart of REST server
598
            restart = False
599
            if args.name in RESTART_CONFIGS:
600
                restart = True
601

602
            if restart:
603
                print ("\nRestart glustereventsd in all nodes")
604

605
            sync_to_peers(args)
606

607

608
class ConfigResetCmd(Cmd):
609
    name = "config-reset"
610

611
    def args(self, parser):
612
        parser.add_argument("name", help="Config Name or all")
613

614
    def run(self, args):
615
        create_custom_config_file_if_not_exists(args)
616

617
        with LockedOpen(CUSTOM_CONFIG_FILE, 'r+'):
618
            changed_keys = []
619
            data = {}
620
            if os.path.exists(CUSTOM_CONFIG_FILE):
621
                data = read_file_content_json(CUSTOM_CONFIG_FILE)
622

623
            # If No data available in custom config or, the specific config
624
            # item is not available in custom config
625
            if not data or \
626
               (args.name != "all" and data.get(args.name, None) is None):
627
                handle_output_error("Config value not reset. Already "
628
                                    "set to default value",
629
                                    errcode=ERROR_SAME_CONFIG,
630
                                    json_output=args.json)
631

632
            if args.name.lower() == "all":
633
                for k, v in data.items():
634
                    changed_keys.append(k)
635

636
                # Reset all keys
637
                file_content_overwrite(CUSTOM_CONFIG_FILE, {})
638
            else:
639
                changed_keys.append(args.name)
640
                del data[args.name]
641
                file_content_overwrite(CUSTOM_CONFIG_FILE, data)
642

643
            # If any value changed which requires restart of REST server
644
            restart = False
645
            for key in changed_keys:
646
                if key in RESTART_CONFIGS:
647
                    restart = True
648
                    break
649

650
            if restart:
651
                print ("\nRestart glustereventsd in all nodes")
652

653
            sync_to_peers(args)
654

655

656
class SyncCmd(Cmd):
657
    name = "sync"
658

659
    def run(self, args):
660
        sync_to_peers(args)
661

662

663
def common_args(parser):
664
    parser.add_argument("--json", help="JSON Output", action="store_true")
665

666

667
if __name__ == "__main__":
668
    set_common_args_func(common_args)
669
    runcli()
670

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.