Issues (42)

dev/mini.py (7 issues)

1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
# Isomer - The distributed application framework
5
# ==============================================
6
# Copyright (C) 2011-2019 Heiko 'riot' Weinen <[email protected]> and others.
7
#
8
# This program is free software: you can redistribute it and/or modify
9
# it under the terms of the GNU Affero General Public License as published by
10
# the Free Software Foundation, either version 3 of the License, or
11
# (at your option) any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
# GNU Affero General Public License for more details.
17
#
18
# You should have received a copy of the GNU Affero General Public License
19
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
20
21
"""
22
Isomer - Backend
23
24
Application
25
===========
26
27
See README.rst for Build/Installation and setup details.
28
29
URLs & Contact
30
==============
31
32
Mail: [email protected]
33
IRC: #[email protected]
34
35
Project repository: http://github.com/isomeric/isomer
36
Frontend repository: http://github.com/isomeric/isomer-frontend
37
38
39
"""
40
41
import grp
42
import pwd
43
import sys
44
import pyinotify
45
46
import click
47
import os
48
from circuits import Event
49
from circuits.web import Server, Static
50
from circuits.web.websockets.dispatcher import WebSocketsDispatcher
51
52
# from circuits.web.errors import redirect
53
# from circuits.app.daemon import Daemon
54
55
from isomer.misc.path import set_instance, get_path
56
from isomer.component import handler, ConfigurableComponent
57
58
# from isomer.schemata.component import ComponentBaseConfigSchema
59
from isomer.database import initialize  # , schemastore
60
from isomer.events.system import populate_user_events, frontendbuildrequest
61
from isomer.logger import (
62
    isolog,
63
    verbose,
64
    debug,
65
    warn,
66
    error,
67
    critical,
68
    setup_root,
69
    verbosity,
70
    set_logfile,
71
)
72
from isomer.debugger import cli_register_event
73
from isomer.ui.builder import install_frontend
74
from isomer.error import abort, EXIT_NO_CERTIFICATE
75
from isomer.tool.etc import load_instance
76
77
78
# from pprint import pprint
79
80
81
class ready(Event):
82
    """Event fired to signal completeness of the local node's setup"""
83
84
    pass
85
86
87
class cli_components(Event):
88
    """List registered and running components"""
89
90
    pass
91
92
93
class cli_reload_db(Event):
94
    """Reload database and schemata (Dangerous!) WiP - does nothing right now"""
95
96
    pass
97
98
99
class cli_reload(Event):
100
    """Reload all components and data models"""
101
102
    pass
103
104
105
class cli_info(Event):
106
    """Provide information about the running instance"""
107
108
    pass
109
110
111
class cli_quit(Event):
112
    """Stop this instance
113
114
    Uses sys.exit() to quit.
115
    """
116
117
    pass
118
119
120
class cli_drop_privileges(Event):
121
    """Try to drop possible root privileges"""
122
123
    pass
124
125
126
class FrontendHandler(pyinotify.ProcessEvent):
127
    def __init__(self, launcher, *args, **kwargs):
128
        super(FrontendHandler, self).__init__(*args, **kwargs)
129
        self.launcher = launcher
130
131
    def process_IN_CLOSE_WRITE(self, event):
132
        print("CHANGE EVENT:", event)
133
        install_frontend(self.launcher.instance, install=False, development=True)
134
135
136 View Code Duplication
def drop_privileges(uid_name="isomer", gid_name="isomer"):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
137
    """Attempt to drop privileges and change user to 'isomer' user/group"""
138
139
    if os.getuid() != 0:
140
        isolog("Not root, cannot drop privileges", lvl=warn, emitter="CORE")
141
        return
142
143
    try:
144
        # Get the uid/gid from the name
145
        running_uid = pwd.getpwnam(uid_name).pw_uid
146
        running_gid = grp.getgrnam(gid_name).gr_gid
147
148
        # Remove group privileges
149
        os.setgroups([])
150
151
        # Try setting the new uid/gid
152
        os.setgid(running_gid)
153
        os.setuid(running_uid)
154
155
        # Ensure a very conservative umask
156
        # old_umask = os.umask(22)
157
        isolog("Privileges dropped", emitter="CORE")
158
    except Exception as e:
159
        isolog(
160
            "Could not drop privileges:",
161
            e,
162
            type(e),
163
            exc=True,
164
            lvl=error,
165
            emitter="CORE",
166
        )
167
168
169
class Core(ConfigurableComponent):
170
    """Isomer Core Backend Application"""
171
172
    # TODO: Move most of this stuff over to a new FrontendBuilder
173
174
    configprops = {
175
        "enabled": {
176
            "type": "array",
177
            "title": "Available modules",
178
            "description": "Modules found and activatable by the system.",
179
            "default": [],
180
            "items": {"type": "string"},
181
        },
182
        "components": {
183
            "type": "object",
184
            "title": "Components",
185
            "description": "Component metadata",
186
            "default": {},
187
        },
188
        "frontendenabled": {
189
            "type": "boolean",
190
            "title": "Frontend enabled",
191
            "description": "Option to toggle frontend activation",
192
            "default": True,
193
        },
194
    }
195
196
    def __init__(self, name, instance, **kwargs):
197
        super(Core, self).__init__("CORE", **kwargs)
198
        self.log("Starting system (channel ", self.channel, ")")
199
200
        self.insecure = kwargs["insecure"]
201
        self.development = kwargs["dev"]
202
203
        self.instance = name
204
205
        host = kwargs.get("web_address", None)
206
        port = kwargs.get("web_port", None)
207
208
        # self.log(instance, pretty=True, lvl=verbose)
209
210
        self.host = instance["web_address"] if host is None else host
211
        self.port = instance["web_port"] if port is None else port
212
213
        self.log("Web configuration: %s:%i" % (self.host, int(self.port)), lvl=debug)
214
215
        self.certificate = certificate = (
216
            instance["web_certificate"] if instance["web_certificate"] != "" else None
217
        )
218
219
        if certificate:
220
            if not os.path.exists(certificate):
221
                self.log(
222
                    "SSL certificate usage requested but certificate "
223
                    "cannot be found!",
224
                    lvl=error,
225
                )
226
                abort(EXIT_NO_CERTIFICATE)
227
228
        # TODO: Find a way to synchronize this with the paths in i.u.builder
229 View Code Duplication
        if self.development:
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
230
            self.frontend_root = os.path.abspath(
231
                os.path.dirname(os.path.realpath(__file__)) + "/../frontend"
232
            )
233
            self.frontend_target = get_path("lib", "frontend-dev")
234
            self.module_root = os.path.abspath(
235
                os.path.dirname(os.path.realpath(__file__)) + "/../modules"
236
            )
237
        else:
238
            self.frontend_root = get_path("lib", "repository/frontend")
239
            self.frontend_target = get_path("lib", "frontend")
240
            self.module_root = ""
241
242
        self.log(
243
            "Frontend & module paths:",
244
            self.frontend_root,
245
            self.frontend_target,
246
            self.module_root,
247
            lvl=verbose,
248
        )
249
250
        self.loadable_components = {}
251
        self.running_components = {}
252
253
        self.frontend_running = False
254
        self.frontend_watcher = None
255
        self.frontend_watch_manager = None
256
257
        self.static = None
258
        self.websocket = None
259
260
        # TODO: Cleanup
261
        self.component_blacklist = [
262
            # 'camera',
263
            # 'logger',
264
            # 'debugger',
265
            "recorder",
266
            "playback",
267
            # 'sensors',
268
            # 'navdatasim'
269
            # 'ldap',
270
            # 'navdata',
271
            # 'nmeaparser',
272
            # 'objectmanager',
273
            # 'wiki',
274
            # 'clientmanager',
275
            # 'library',
276
            # 'nmeaplayback',
277
            # 'alert',
278
            # 'tilecache',
279
            # 'schemamanager',
280
            # 'chat',
281
            # 'debugger',
282
            # 'rcmanager',
283
            # 'auth',
284
            # 'machineroom'
285
        ]
286
287
        self.component_blacklist += kwargs["blacklist"]
288
289
        self.update_components()
290
        self._write_config()
291
292
        self.server = None
293
294
        if self.insecure:
295
            self.log("Not dropping privileges - this may be insecure!", lvl=warn)
296
297
    @handler("started", channel="*")
298
    def ready(self, source):
299
        """All components have initialized, set up the component
300
        configuration schema-store, run the local server and drop privileges"""
301
302
        from isomer.schemastore import configschemastore
303
304
        configschemastore[self.name] = self.configschema
305
306
        self._start_server()
307
308
        if not self.insecure:
309
            self._drop_privileges()
310
311
        self.fireEvent(cli_register_event("components", cli_components))
312
        self.fireEvent(cli_register_event("drop_privileges", cli_drop_privileges))
313
        self.fireEvent(cli_register_event("reload_db", cli_reload_db))
314
        self.fireEvent(cli_register_event("reload", cli_reload))
315
        self.fireEvent(cli_register_event("quit", cli_quit))
316
        self.fireEvent(cli_register_event("info", cli_info))
317
318
    @handler("frontendbuildrequest", channel="setup")
319
    def trigger_frontend_build(self, event):
320
        """Event hook to trigger a new frontend build"""
321
322
        from isomer.database import instance
323
324
        install_frontend(
325
            instance=instance,
326
            forcerebuild=event.force,
327
            install=event.install,
328
            development=self.development,
329
        )
330
331
    @handler("cli_drop_privileges")
332
    def cli_drop_privileges(self, event):
333
        """Drop possible user privileges"""
334
335
        self.log("Trying to drop privileges", lvl=debug)
336
        self._drop_privileges()
337
338
    @handler("cli_components")
339
    def cli_components(self, event):
340
        """List all running components"""
341
342
        self.log("Running components: ", sorted(self.running_components.keys()))
343
344
    @handler("cli_reload_db")
345
    def cli_reload_db(self, event):
346
        """Experimental call to reload the database"""
347
348
        self.log("This command is WiP.")
349
350
        initialize()
351
352
    @handler("cli_reload")
353
    def cli_reload(self, event):
354
        """Experimental call to reload the component tree"""
355
356
        self.log("Reloading all components.")
357
358
        self.update_components(forcereload=True)
359
        initialize()
360
361
        from isomer.debugger import cli_compgraph
362
363
        self.fireEvent(cli_compgraph())
364
365
    @handler("cli_quit")
366
    def cli_quit(self, event):
367
        """Stop the instance immediately"""
368
369
        self.log("Quitting on CLI request.")
370
        if self.frontend_watcher is not None:
371
            self.frontend_watcher.stop()
372
        sys.exit()
373
374
    @handler("cli_info")
375
    def cli_info(self, event):
376
        """Provides information about the running instance"""
377
378
        self.log(
379
            "Instance:",
380
            self.instance,
381
            "Dev:",
382
            self.development,
383
            "Host:",
384
            self.host,
385
            "Port:",
386
            self.port,
387
            "Insecure:",
388
            self.insecure,
389
            "Frontend:",
390
            self.frontend_target,
391
        )
392
393 View Code Duplication
    def _start_server(self, *args):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
394
        """Run the node local server"""
395
396
        self.log("Starting server", args)
397
        secure = self.certificate is not None
398
        if secure:
399
            self.log("Running SSL server with cert:", self.certificate)
400
        else:
401
            self.log(
402
                "Running insecure server without SSL. Do not use without SSL proxy in production!",
403
                lvl=warn,
404
            )
405
406
        try:
407
            self.server = Server(
408
                (self.host, self.port),
409
                display_banner=False,
410
                secure=secure,
411
                certfile=self.certificate  # ,
412
                # inherit=True
413
            ).register(self)
414
        except PermissionError as e:
415
            if self.port <= 1024:
416
                self.log(
417
                    "Could not open privileged port (%i), check permissions!"
418
                    % self.port,
419
                    e,
420
                    lvl=critical,
421
                )
422
            else:
423
                self.log("Could not open port (%i):" % self.port, e, lvl=critical)
424
        except OSError as e:
425
            if e.errno == 98:
426
                self.log("Port (%i) is already opened!" % self.port, lvl=critical)
427
            else:
428
                self.log("Could not open port (%i):" % self.port, e, lvl=critical)
429
430
    def _drop_privileges(self, *args):
431
        self.log("Dropping privileges", lvl=debug)
432
        drop_privileges()
433
434
    # Moved to manage tool, maybe of interest later, though:
435
    #
436
    # @handler("componentupdaterequest", channel="setup")
437
    # def trigger_component_update(self, event):
438
    #     self.update_components(forcereload=event.force)
439
440
    def update_components(
441
        self, forcereload=False, forcerebuild=False, forcecopy=True, install=False
442
    ):
443
        """Check all known entry points for components. If necessary,
444
        manage configuration updates"""
445
446
        # TODO: See if we can pull out major parts of the component handling.
447
        # They are also used in the manage tool to instantiate the
448
        # component frontend bits.
449
450
        self.log("Updating components")
451
        components = {}
452
453
        try:
454
455
            from pkg_resources import iter_entry_points
456
457
            entry_point_tuple = (
458
                iter_entry_points(group="isomer.base", name=None),
459
                iter_entry_points(group="isomer.sails", name=None),
460
                iter_entry_points(group="isomer.components", name=None),
461
            )
462
            self.log("Entrypoints:", entry_point_tuple, pretty=True, lvl=verbose)
463
            for iterator in entry_point_tuple:
464
                for entry_point in iterator:
465
                    self.log("Entrypoint:", entry_point, pretty=True, lvl=verbose)
466
                    try:
467
                        name = entry_point.name
468
                        location = entry_point.dist.location
469
                        loaded = entry_point.load()
470
471
                        self.log(
472
                            "Entry point: ",
473
                            entry_point,
474
                            name,
475
                            entry_point.resolve(),
476
                            lvl=verbose,
477
                        )
478
479
                        self.log("Loaded: ", loaded, lvl=verbose)
480
                        comp = {
481
                            "package": entry_point.dist.project_name,
482
                            "location": location,
483
                            "version": str(entry_point.dist.parsed_version),
484
                            "description": loaded.__doc__,
485
                        }
486
487
                        components[name] = comp
488
                        self.loadable_components[name] = loaded
489
490
                        self.log("Loaded component:", comp, lvl=verbose)
491
492
                    except Exception as e:
493
                        self.log(
494
                            "Could not inspect entrypoint: ",
495
                            e,
496
                            type(e),
497
                            entry_point,
498
                            iterator,
499
                            lvl=error,
500
                            exc=True,
501
                        )
502
503
                        # for name in components.keys():
504
                        #     try:
505
                        #         self.log(self.loadable_components[name])
506
                        #         configobject = {
507
                        #             'type': 'object',
508
                        #             'properties':
509
                        # self.loadable_components[name].configprops
510
                        #         }
511
                        #         ComponentBaseConfigSchema['schema'][
512
                        # 'properties'][
513
                        #             'settings'][
514
                        #             'oneOf'].append(configobject)
515
                        #     except (KeyError, AttributeError) as e:
516
                        #         self.log('Problematic configuration
517
                        # properties in '
518
                        #                  'component ', name, exc=True)
519
                        #
520
                        # schemastore['component'] = ComponentBaseConfigSchema
521
522
        except Exception as e:
523
            self.log("Component update error: ", e, type(e), lvl=error, exc=True)
524
            return
525
526
        self.log(
527
            "Checking component frontend bits in ", self.frontend_root, lvl=verbose
528
        )
529
530
        # pprint(self.config._fields)
531
        diff = set(components) ^ set(self.config.components)
532
        if diff or forcecopy and self.config.frontendenabled:
533
            self.log("Old component configuration differs:", diff, lvl=debug)
534
            self.log(self.config.components, components, lvl=verbose)
535
            self.config.components = components
536
        else:
537
            self.log("No component configuration change. Proceeding.")
538
539
        if forcereload:
540
            self.log("Restarting all components.", lvl=warn)
541
            self._instantiate_components(clear=True)
542
543 View Code Duplication
    def _start_frontend(self, restart=False):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
544
        """Check if it is enabled and start the frontend http & websocket"""
545
546
        self.log(self.config, self.config.frontendenabled, lvl=verbose)
547
        if self.config.frontendenabled and not self.frontend_running or restart:
548
            self.log("Restarting webfrontend services on", self.frontend_target)
549
550
            self.static = Static("/", docroot=self.frontend_target).register(self)
551
            self.websocket = WebSocketsDispatcher("/websocket").register(self)
552
            self.frontend_running = True
553
554
            if self.development:
555
                self.frontend_watch_manager = pyinotify.WatchManager()
556
                self.frontend_watcher = pyinotify.ThreadedNotifier(
557
                    self.frontend_watch_manager, FrontendHandler(self)
558
                )
559
                self.frontend_watcher.start()
560
                mask = (
561
                    pyinotify.IN_DELETE | pyinotify.IN_CREATE | pyinotify.IN_CLOSE_WRITE
562
                )
563
                self.log("Frontend root:", self.frontend_root, lvl=debug)
564
                self.frontend_watch_manager.add_watch(self.module_root, mask, rec=True)
565
566 View Code Duplication
    def _instantiate_components(self, clear=True):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
567
        """Inspect all loadable components and run them"""
568
569
        if clear:
570
            # import objgraph
571
            # from copy import deepcopy
572
            from circuits.tools import kill
573
            from circuits import Component
574
575
            for comp in self.running_components.values():
576
                self.log(comp, type(comp), isinstance(comp, Component), pretty=True)
577
                kill(comp)
578
            # removables = deepcopy(list(self.runningcomponents.keys()))
579
            #
580
            # for key in removables:
581
            #     comp = self.runningcomponents[key]
582
            #     self.log(comp)
583
            #     comp.unregister()
584
            #     comp.stop()
585
            #     self.runningcomponents.pop(key)
586
            #
587
            #     objgraph.show_backrefs([comp],
588
            #                            max_depth=5,
589
            #                            filter=lambda x: type(x) not in [list, tuple, set],
590
            #                            highlight=lambda x: type(x) in [ConfigurableComponent],
591
            #                            filename='backref-graph_%s.png' % comp.uniquename)
592
            #     del comp
593
            # del removables
594
            self.running_components = {}
595
596
        self.log(
597
            "Not running blacklisted components: ", self.component_blacklist, lvl=debug
598
        )
599
600
        running = set(self.loadable_components.keys()).difference(
601
            self.component_blacklist
602
        )
603
        self.log("Starting components: ", sorted(running))
604
        for name, componentdata in self.loadable_components.items():
605
            if name in self.component_blacklist:
606
                continue
607
            self.log("Running component: ", name, lvl=verbose)
608
            try:
609
                if name in self.running_components:
610
                    self.log("Component already running: ", name, lvl=warn)
611
                else:
612
                    runningcomponent = componentdata()
613
                    runningcomponent.register(self)
614
                    self.running_components[name] = runningcomponent
615
            except Exception as e:
616
                self.log(
617
                    "Could not register component: ",
618
                    name,
619
                    e,
620
                    type(e),
621
                    lvl=error,
622
                    exc=True,
623
                )
624
625 View Code Duplication
    def started(self, component):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
626
        """Sets up the application after startup."""
627
628
        self.log("Running.")
629
        self.log("Started event origin: ", component, lvl=verbose)
630
        populate_user_events()
631
632
        from isomer.events.system import AuthorizedEvents
633
634
        self.log(
635
            len(AuthorizedEvents),
636
            "authorized event sources:",
637
            list(AuthorizedEvents.keys()),
638
            lvl=debug,
639
        )
640
641
        self._instantiate_components()
642
        self._start_frontend()
643
        self.fire(ready(), "isomer-web")
644
645
646 View Code Duplication
def construct_graph(name, instance, args):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
647
    """Preliminary Isomer application Launcher"""
648
649
    app = Core(name, instance, **args)
650
651
    setup_root(app)
652
653
    if args["debug"]:
654
        from circuits import Debugger
655
656
        isolog("Starting circuits debugger", lvl=warn, emitter="GRAPH")
657
        dbg = Debugger().register(app)
658
        # TODO: Make these configurable from modules, navdata is _very_ noisy
659
        # but should not be listed _here_
660
        dbg.IgnoreEvents.extend(
661
            [
662
                "read",
663
                "_read",
664
                "write",
665
                "_write",
666
                "stream_success",
667
                "stream_complete",
668
                "serial_packet",
669
                "raw_data",
670
                "stream",
671
                "navdatapush",
672
                "referenceframe",
673
                "updateposition",
674
                "updatesubscriptions",
675
                "generatevesseldata",
676
                "generatenavdata",
677
                "sensordata",
678
                "reset_flood_offenders",
679
                "reset_flood_counters",  # Flood counters
680
                "task_success",
681
                "task_done",  # Thread completion
682
                "keepalive",  # IRC Gateway
683
            ]
684
        )
685
686
    isolog("Beginning graph assembly.", emitter="GRAPH")
687
688
    if args["draw_graph"]:
689
        from circuits.tools import graph
690
691
        graph(app)
692
693
    if args["open_gui"]:
694
        import webbrowser
695
696
        # TODO: Fix up that url:
697
        webbrowser.open("http://%s:%i/" % (args["host"], args["port"]))
698
699
    isolog("Graph assembly done.", emitter="GRAPH")
700
701
    return app
702
703
704
@click.command()
705
@click.option(
706
    "--web-port", "-p", help="Define port for UI server", type=int, default=None
707
)
708
@click.option(
709
    "--web-address",
710
    "-a",
711
    help="Define listening address for UI server",
712
    type=str,
713
    default="127.0.0.1",
714
)
715
@click.option(
716
    "--web-certificate", "-c", help="Certificate file path", type=str, default=None
717
)
718
@click.option("--profile", help="Enable profiler", is_flag=True)
719
@click.option(
720
    "--open-gui",
721
    help="Launch web browser for GUI inspection after startup",
722
    is_flag=True,
723
)
724
@click.option(
725
    "--draw-graph",
726
    help="Draw a snapshot of the component graph after construction",
727
    is_flag=True,
728
)
729
@click.option("--live-log", help="Log to in-memory structure as well", is_flag=True)
730
@click.option("--debug", help="Run circuits debugger", is_flag=True)
731
@click.option("--dev", help="Run development server", is_flag=True, default=True)
732
@click.option("--insecure", help="Keep privileges - INSECURE", is_flag=True)
733
@click.option("--no-run", "-n", help="Only assemble system, do not run", is_flag=True)
734
@click.option(
735
    "--blacklist",
736
    "-b",
737
    help="Blacklist a component (can be repeated)",
738
    multiple=True,
739
    default=[],
740
)
741
@click.pass_context
742
def launch(ctx, run=True, **args):
743
    """Assemble and run an Isomer instance"""
744
745
    isolog("Launching Mini instance")
746
747
    if ctx.params["live_log"] is True:
748
        from isomer import logger
749
750
        logger.live = True
751
752
    if args["web_certificate"] is not None:
753
        isolog(
754
            "Warning! Using SSL on the backend is currently not recommended!",
755
            lvl=critical,
756
            emitter="CORE",
757
        )
758
759
    server = construct_graph('MINI', {}, args)
760
    if run and not args["no_run"]:
761
        server.run()
762
763
    return server
764