merakisync 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. merakisync/__about__.py +4 -0
  2. merakisync/__init__.py +60 -0
  3. merakisync/cli/__init__.py +0 -0
  4. merakisync/cli/cli.py +196 -0
  5. merakisync/cli/cmd_init.py +120 -0
  6. merakisync/cli/cmd_migrate.py +59 -0
  7. merakisync/cli/cmd_sync.py +167 -0
  8. merakisync/cli/cmd_update.py +60 -0
  9. merakisync/config.py +220 -0
  10. merakisync/dashboard.py +73 -0
  11. merakisync/database.py +84 -0
  12. merakisync/exceptions.py +21 -0
  13. merakisync/logging.py +48 -0
  14. merakisync/migrations/env.py +66 -0
  15. merakisync/migrations/script.py.mako +28 -0
  16. merakisync/migrations/versions/0001_initial_schema.py +321 -0
  17. merakisync/migrations/versions/0002_add_vlan.py +61 -0
  18. merakisync/migrations/versions/0003_reconcile_production_schema.py +257 -0
  19. merakisync/migrations/versions/0004_add_ssid.py +72 -0
  20. merakisync/migrations/versions/0005_drop_switchport_perdevice_fields.py +56 -0
  21. merakisync/migrations/versions/0006_drop_switchport_legacy_columns.py +59 -0
  22. merakisync/migrations/versions/0007_add_uplink_provider.py +29 -0
  23. merakisync/migrations/versions/0008_add_ssid_psk.py +31 -0
  24. merakisync/migrations/versions/0009_jsonb_tags_product_types.py +58 -0
  25. merakisync/models/__init__.py +25 -0
  26. merakisync/models/alert.py +194 -0
  27. merakisync/models/base.py +564 -0
  28. merakisync/models/device.py +226 -0
  29. merakisync/models/dhcp_server_policy.py +120 -0
  30. merakisync/models/l3_firewall_rule.py +160 -0
  31. merakisync/models/network.py +183 -0
  32. merakisync/models/organization.py +122 -0
  33. merakisync/models/ssid.py +164 -0
  34. merakisync/models/switchport.py +222 -0
  35. merakisync/models/uplink.py +227 -0
  36. merakisync/models/uplink_usage.py +268 -0
  37. merakisync/models/vlan.py +191 -0
  38. merakisync/utils/__init__.py +12 -0
  39. merakisync/utils/action_batch.py +136 -0
  40. merakisync/utils/casing.py +28 -0
  41. merakisync/utils/confirm.py +14 -0
  42. merakisync/utils/filter_array.py +13 -0
  43. merakisync/utils/prompt.py +38 -0
  44. merakisync-0.1.6.dist-info/METADATA +527 -0
  45. merakisync-0.1.6.dist-info/RECORD +48 -0
  46. merakisync-0.1.6.dist-info/WHEEL +4 -0
  47. merakisync-0.1.6.dist-info/entry_points.txt +2 -0
  48. merakisync-0.1.6.dist-info/licenses/LICENSE.txt +18 -0
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026-present Nathan Anderson <nathanea05@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ __version__ = "0.1.6"
merakisync/__init__.py ADDED
@@ -0,0 +1,60 @@
1
+ # SPDX-FileCopyrightText: 2026-present Nathan Anderson <nathanea05@gmail.com>
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+
5
+ from merakisync.__about__ import __version__
6
+
7
+ # Public API surface — import only stable, non-circular symbols at package level.
8
+ # Infrastructure helpers
9
+ from merakisync.config import get_config
10
+ from merakisync.dashboard import get_dashboard
11
+ from merakisync.database import get_engine, get_session
12
+
13
+ # Models
14
+ from merakisync.models.organization import Organization
15
+ from merakisync.models.network import Network
16
+ from merakisync.models.device import Device
17
+ from merakisync.models.switchport import Switchport
18
+ from merakisync.models.uplink import Uplink
19
+ from merakisync.models.uplink_usage import UplinkUsage
20
+ from merakisync.models.dhcp_server_policy import DhcpServerPolicy
21
+ from merakisync.models.alert import Alert
22
+ from merakisync.models.l3_firewall_rule import L3FirewallRule
23
+ from merakisync.models.vlan import Vlan
24
+ from merakisync.models.ssid import Ssid
25
+
26
+ # Exceptions
27
+ from merakisync.exceptions import (
28
+ MissingConfigError,
29
+ ConfigWriteError,
30
+ MerakiConnectionError,
31
+ DatabaseConnectionError,
32
+ UpsertError,
33
+ )
34
+
35
+ __all__ = [
36
+ "__version__",
37
+ # Infrastructure
38
+ "get_config",
39
+ "get_dashboard",
40
+ "get_engine",
41
+ "get_session",
42
+ # Models
43
+ "Organization",
44
+ "Network",
45
+ "Device",
46
+ "Switchport",
47
+ "Uplink",
48
+ "UplinkUsage",
49
+ "DhcpServerPolicy",
50
+ "Alert",
51
+ "L3FirewallRule",
52
+ "Vlan",
53
+ "Ssid",
54
+ # Exceptions
55
+ "MissingConfigError",
56
+ "ConfigWriteError",
57
+ "MerakiConnectionError",
58
+ "DatabaseConnectionError",
59
+ "UpsertError",
60
+ ]
File without changes
merakisync/cli/cli.py ADDED
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from merakisync.__about__ import __version__
7
+ from merakisync.cli.cmd_sync import SyncFlags
8
+
9
+
10
+ def _add_log_flags(p: argparse.ArgumentParser) -> None:
11
+ """Add --verbose/--quiet to a subparser without overriding the parent value."""
12
+ g = p.add_mutually_exclusive_group()
13
+ g.add_argument(
14
+ "-v", "--verbose", action="store_true", default=argparse.SUPPRESS,
15
+ help="Enable debug logging.",
16
+ )
17
+ g.add_argument(
18
+ "-q", "--quiet", action="store_true", default=argparse.SUPPRESS,
19
+ help="Suppress all output below WARNING.",
20
+ )
21
+
22
+
23
+ def _build_parser() -> argparse.ArgumentParser:
24
+ parser = argparse.ArgumentParser(
25
+ prog="merakisync",
26
+ description="Sync Meraki Dashboard data into PostgreSQL.",
27
+ )
28
+
29
+ parser.add_argument(
30
+ "--version", action="version", version=f"merakisync {__version__}",
31
+ )
32
+
33
+ # Global log-level flags — also added to each subcommand below so they
34
+ # can appear either before or after the subcommand name.
35
+ log_group = parser.add_mutually_exclusive_group()
36
+ log_group.add_argument(
37
+ "-v", "--verbose", action="store_true", help="Enable debug logging."
38
+ )
39
+ log_group.add_argument(
40
+ "-q", "--quiet", action="store_true", help="Suppress all output below WARNING."
41
+ )
42
+
43
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
44
+
45
+ # init
46
+ init_parser = subparsers.add_parser(
47
+ "init",
48
+ help="Configure the Meraki API key and/or database connection.",
49
+ )
50
+ init_parser.add_argument(
51
+ "--meraki", action="store_true", default=False,
52
+ help="Configure Meraki API key only (preserve existing database settings).",
53
+ )
54
+ init_parser.add_argument(
55
+ "--database", action="store_true", default=False,
56
+ help="Configure database connection only (preserve existing Meraki API key).",
57
+ )
58
+ _add_log_flags(init_parser)
59
+
60
+ # update
61
+ update_parser = subparsers.add_parser(
62
+ "update",
63
+ help="Download the latest binary and apply database migrations.",
64
+ )
65
+ _add_log_flags(update_parser)
66
+
67
+ # migrate
68
+ migrate_parser = subparsers.add_parser(
69
+ "migrate",
70
+ help="Apply database migrations (Alembic upgrade head).",
71
+ )
72
+ migrate_parser.add_argument(
73
+ "--revision",
74
+ default="head",
75
+ metavar="REV",
76
+ help="Alembic revision target (default: head).",
77
+ )
78
+ _add_log_flags(migrate_parser)
79
+
80
+ # sync
81
+ sync_parser = subparsers.add_parser(
82
+ "sync",
83
+ help="Sync Meraki data into the database.",
84
+ )
85
+ _add_log_flags(sync_parser)
86
+ sync_parser.add_argument(
87
+ "-o", "--organizations", action="store_true", help="Sync organizations."
88
+ )
89
+ sync_parser.add_argument(
90
+ "-n", "--networks", action="store_true", help="Sync networks."
91
+ )
92
+ sync_parser.add_argument(
93
+ "-d", "--devices", action="store_true", help="Sync devices."
94
+ )
95
+ sync_parser.add_argument(
96
+ "--switchports", action="store_true", help="Sync switch port configurations."
97
+ )
98
+ sync_parser.add_argument(
99
+ "--uplinks", action="store_true", help="Sync uplink statuses."
100
+ )
101
+ sync_parser.add_argument(
102
+ "--uplink-usage", action="store_true", dest="uplink_usage",
103
+ help="Sync uplink bandwidth usage (current month)."
104
+ )
105
+ sync_parser.add_argument(
106
+ "--dhcp-server-policy", action="store_true", dest="dhcp_server_policy",
107
+ help="Sync switch DHCP server policies."
108
+ )
109
+ sync_parser.add_argument(
110
+ "--alerts", action="store_true", help="Sync assurance alerts."
111
+ )
112
+ sync_parser.add_argument(
113
+ "--l3-firewall-rules", action="store_true", dest="l3_firewall_rules",
114
+ help="Sync MX L3 firewall rules."
115
+ )
116
+ sync_parser.add_argument(
117
+ "--vlans", action="store_true", help="Sync MX appliance VLANs."
118
+ )
119
+ sync_parser.add_argument(
120
+ "--ssids", action="store_true", help="Sync wireless SSIDs."
121
+ )
122
+
123
+ return parser
124
+
125
+
126
+ def main() -> None:
127
+ parser = _build_parser()
128
+ args = parser.parse_args()
129
+
130
+ # Configure logging before doing anything else
131
+ from merakisync.logging import configure_logging
132
+ configure_logging(verbose=getattr(args, "verbose", False),
133
+ quiet=getattr(args, "quiet", False))
134
+
135
+ if args.command is None:
136
+ parser.print_help()
137
+ sys.exit(0)
138
+
139
+ if args.command == "init":
140
+ from merakisync.cli.cmd_init import run
141
+ configure_meraki = args.meraki or not args.database
142
+ configure_database = args.database or not args.meraki
143
+ run(configure_meraki=configure_meraki, configure_database=configure_database)
144
+ return
145
+
146
+ if args.command == "update":
147
+ from merakisync.cli.cmd_update import run
148
+ run()
149
+ return
150
+
151
+ if args.command == "migrate":
152
+ from merakisync.cli.cmd_migrate import run
153
+ run(revision=args.revision)
154
+ return
155
+
156
+ if args.command == "sync":
157
+ import logging
158
+ from merakisync.config import get_config
159
+ from merakisync.exceptions import MissingConfigError
160
+ log = logging.getLogger(__name__)
161
+ try:
162
+ conf = get_config()
163
+ except MissingConfigError as exc:
164
+ log.error("%s", exc)
165
+ sys.exit(1)
166
+ if conf.meraki_api_key is None:
167
+ log.error(
168
+ "Meraki API key is not configured. Run `merakisync init --meraki`."
169
+ )
170
+ sys.exit(1)
171
+ if conf.db is None:
172
+ log.error(
173
+ "Database is not configured. Run `merakisync init --database`."
174
+ )
175
+ sys.exit(1)
176
+
177
+ from merakisync.cli.cmd_sync import run
178
+ flags = SyncFlags(
179
+ organizations=args.organizations,
180
+ networks=args.networks,
181
+ devices=args.devices,
182
+ switchports=args.switchports,
183
+ uplinks=args.uplinks,
184
+ uplink_usage=args.uplink_usage,
185
+ dhcp_server_policy=args.dhcp_server_policy,
186
+ alerts=args.alerts,
187
+ l3_firewall_rules=args.l3_firewall_rules,
188
+ vlans=args.vlans,
189
+ ssids=args.ssids,
190
+ )
191
+ run(flags)
192
+ return
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ from merakisync.config import (
4
+ Configuration,
5
+ DbConfig,
6
+ get_config,
7
+ get_save_path,
8
+ prompt_api_key,
9
+ prompt_database,
10
+ write_config,
11
+ )
12
+ from merakisync.dashboard import validate_api_key
13
+ from merakisync.database import validate_connection
14
+ from merakisync.exceptions import (
15
+ ConfigWriteError,
16
+ DatabaseConnectionError,
17
+ MerakiConnectionError,
18
+ MissingConfigError,
19
+ )
20
+ from merakisync.utils.confirm import confirm
21
+
22
+
23
+ def run(*, configure_meraki: bool = True, configure_database: bool = True) -> None:
24
+ """Interactive `merakisync init` wizard.
25
+
26
+ Args:
27
+ configure_meraki: Prompt for and save the Meraki API key.
28
+ configure_database: Prompt for and save the database connection.
29
+ """
30
+ save_path = get_save_path()
31
+
32
+ # Load existing config to preserve whichever section won't be re-configured.
33
+ existing: Configuration | None = None
34
+ try:
35
+ existing = get_config()
36
+ config_exists = True
37
+ except MissingConfigError:
38
+ config_exists = False
39
+
40
+ if config_exists:
41
+ print(f"Found an existing configuration at {save_path}")
42
+ if not confirm("Continue anyway?", default=True):
43
+ return
44
+ print("")
45
+
46
+ api_key: str | None = existing.meraki_api_key if existing else None
47
+ db_config: DbConfig | None = existing.db if existing else None
48
+ database_validated = False
49
+
50
+ # --- Meraki API key --------------------------------------------------
51
+ if configure_meraki:
52
+ print("=== Meraki Configuration ===")
53
+ while True:
54
+ new_api_key = prompt_api_key()
55
+ print("Validating API key...")
56
+ try:
57
+ validate_api_key(new_api_key)
58
+ print("OK Meraki API key validated.")
59
+ api_key = new_api_key
60
+ break
61
+ except MerakiConnectionError as exc:
62
+ print(f"FAIL {exc}")
63
+ print("")
64
+
65
+ # --- Database --------------------------------------------------------
66
+ if configure_database:
67
+ print("=== Database Configuration ===")
68
+ while True:
69
+ new_db_config = prompt_database()
70
+ print("Validating database connection...")
71
+ try:
72
+ validate_connection(new_db_config.get_dsn())
73
+ print("OK Database connection successful.")
74
+ db_config = new_db_config
75
+ database_validated = True
76
+ break
77
+ except DatabaseConnectionError:
78
+ print(f"FAIL Unable to connect to Postgres at {new_db_config.host}:{new_db_config.port}")
79
+ print(" Is the server running and accepting TCP connections?")
80
+ if confirm("Continue with these settings despite failed validation?", default=False):
81
+ print("WARN Configuration will be saved without successful validation.")
82
+ db_config = new_db_config
83
+ break
84
+ print("")
85
+ print("=== Re-enter Database Configuration ===")
86
+ print("")
87
+
88
+ # --- Save ------------------------------------------------------------
89
+ conf = Configuration.from_parts(api_key=api_key, db=db_config)
90
+ print("Configuration complete.")
91
+ print(f"Settings will be saved to: {save_path}")
92
+ config_saved = False
93
+ if confirm("Save configuration?", default=True):
94
+ try:
95
+ write_config(path=save_path, conf=conf)
96
+ print(f"OK Config saved to {save_path}")
97
+ config_saved = True
98
+ except ConfigWriteError as exc:
99
+ print(f"FAIL {exc}")
100
+ print("")
101
+
102
+ # --- Migrations ------------------------------------------------------
103
+ if config_saved and configure_database and database_validated:
104
+ print("=== Database Migrations ===")
105
+ if confirm("Apply database migrations now?", default=True):
106
+ _run_migrations()
107
+ else:
108
+ print("WARN Schema not applied. Run `merakisync migrate` before syncing.")
109
+ print("")
110
+
111
+
112
+ def _run_migrations() -> None:
113
+ """Invoke Alembic migrations programmatically."""
114
+ try:
115
+ from merakisync.cli.cmd_migrate import run as migrate_run
116
+ migrate_run()
117
+ print("OK Migrations applied.")
118
+ except SystemExit:
119
+ print("FAIL Migration failed.")
120
+ print(" Run `merakisync migrate` manually to retry.")
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import sys
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def _check_db_configured() -> None:
10
+ from merakisync.config import get_config
11
+ from merakisync.exceptions import MissingConfigError
12
+ conf = get_config()
13
+ if conf.db is None:
14
+ raise MissingConfigError(
15
+ "Database is not configured. Run `merakisync init --database`."
16
+ )
17
+
18
+
19
+ def run(revision: str = "head") -> None:
20
+ """Run Alembic migrations up to *revision* (default: head).
21
+
22
+ Reads the DSN from the merakisync configuration so no separate
23
+ database URL needs to be passed on the command line.
24
+
25
+ Works in three environments:
26
+ - Development (src layout): importlib.resources resolves to src/
27
+ - Pip install: importlib.resources resolves to site-packages/
28
+ - PyInstaller frozen binary: data files are extracted to sys._MEIPASS
29
+ """
30
+ try:
31
+ from merakisync.exceptions import MissingConfigError
32
+ _check_db_configured()
33
+ except MissingConfigError as exc:
34
+ logger.error("%s", exc)
35
+ sys.exit(1)
36
+
37
+ try:
38
+ import pathlib
39
+ from alembic import command
40
+ from alembic.config import Config as AlembicConfig
41
+
42
+ if getattr(sys, "frozen", False):
43
+ # PyInstaller one-file binary: migrations/ is extracted alongside
44
+ # the merakisync package under sys._MEIPASS at startup.
45
+ migrations_dir = str(pathlib.Path(sys._MEIPASS) / "merakisync" / "migrations")
46
+ else:
47
+ import importlib.resources as pkg_resources
48
+ import merakisync
49
+ migrations_dir = str(pkg_resources.files(merakisync) / "migrations")
50
+
51
+ # Use a bare AlembicConfig (no ini file) and set every required option
52
+ # programmatically. This avoids any need to locate or bundle alembic.ini.
53
+ alembic_cfg = AlembicConfig()
54
+ alembic_cfg.set_main_option("script_location", migrations_dir)
55
+ command.upgrade(alembic_cfg, revision)
56
+ logger.info("Migrations applied to revision: %s", revision)
57
+ except Exception as exc:
58
+ logger.error("Migration failed: %s", exc)
59
+ sys.exit(1)
@@ -0,0 +1,167 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ @dataclass
10
+ class SyncFlags:
11
+ """Which resource types to sync. All False means sync everything."""
12
+
13
+ organizations: bool = False
14
+ networks: bool = False
15
+ devices: bool = False
16
+ switchports: bool = False
17
+ uplinks: bool = False
18
+ uplink_usage: bool = False
19
+ dhcp_server_policy: bool = False
20
+ alerts: bool = False
21
+ l3_firewall_rules: bool = False
22
+ vlans: bool = False
23
+ ssids: bool = False
24
+
25
+ @property
26
+ def sync_all(self) -> bool:
27
+ return not any(vars(self).values())
28
+
29
+
30
+ def run(flags: SyncFlags | None = None) -> None:
31
+ """Orchestrate a full or selective sync.
32
+
33
+ Sync order respects dependencies:
34
+ Organizations → Networks → Devices → per-network/per-device resources
35
+
36
+ Args:
37
+ flags: Controls which resource types are synced. Pass None or an
38
+ all-False SyncFlags to sync everything.
39
+ """
40
+ if flags is None:
41
+ flags = SyncFlags()
42
+
43
+ from merakisync.models.organization import Organization
44
+ from merakisync.models.network import Network
45
+ from merakisync.models.device import Device
46
+ from merakisync.models.switchport import Switchport
47
+ from merakisync.models.uplink import Uplink
48
+ from merakisync.models.uplink_usage import UplinkUsage
49
+ from merakisync.models.dhcp_server_policy import DhcpServerPolicy
50
+ from merakisync.models.alert import Alert
51
+ from merakisync.models.l3_firewall_rule import L3FirewallRule
52
+ from merakisync.models.vlan import Vlan
53
+ from merakisync.models.ssid import Ssid
54
+
55
+ do_all = flags.sync_all
56
+
57
+ # ------------------------------------------------------------------
58
+ # Organizations
59
+ # ------------------------------------------------------------------
60
+ if do_all or flags.organizations:
61
+ logger.info("Syncing organizations...")
62
+ orgs = Organization.sync()
63
+ else:
64
+ # We always need the org list to drive child syncs
65
+ logger.debug("Fetching organizations (not syncing to DB)...")
66
+ orgs = Organization.get(source="meraki")
67
+
68
+ if not orgs:
69
+ logger.error("No organizations found. Aborting sync.")
70
+ return
71
+
72
+ logger.info("Found %d organization(s).", len(orgs))
73
+
74
+ for org in orgs:
75
+ org_id = org.id
76
+ logger.info("Processing org: %s (%s)", org.name, org_id)
77
+
78
+ # --------------------------------------------------------------
79
+ # Alerts (org-level, no network dependency)
80
+ # --------------------------------------------------------------
81
+ if do_all or flags.alerts:
82
+ logger.info(" Syncing alerts for org %s (%s)...", org_id, org.name)
83
+ Alert.sync(org_id)
84
+
85
+ # --------------------------------------------------------------
86
+ # Uplinks (org-level)
87
+ # --------------------------------------------------------------
88
+ if do_all or flags.uplinks:
89
+ logger.info(" Syncing uplinks for org %s (%s)...", org_id, org.name)
90
+ Uplink.sync(org_id)
91
+
92
+ # --------------------------------------------------------------
93
+ # Uplink usage (org-level)
94
+ # --------------------------------------------------------------
95
+ if do_all or flags.uplink_usage:
96
+ logger.info(" Syncing uplink usage for org %s (%s)...", org_id, org.name)
97
+ UplinkUsage.sync(org_id)
98
+
99
+ # --------------------------------------------------------------
100
+ # Devices (org-level)
101
+ # --------------------------------------------------------------
102
+ if do_all or flags.devices:
103
+ logger.info(" Syncing devices for org %s (%s)...", org_id, org.name)
104
+ Device.sync(org_id)
105
+
106
+ # --------------------------------------------------------------
107
+ # Switchports (org-level, no network dependency)
108
+ # --------------------------------------------------------------
109
+ if do_all or flags.switchports:
110
+ logger.info(" Syncing switchports for org %s (%s)...", org_id, org.name)
111
+ Switchport.sync(org_id)
112
+
113
+ # --------------------------------------------------------------
114
+ # Networks → per-network resources
115
+ # --------------------------------------------------------------
116
+ need_networks = do_all or flags.networks or flags.dhcp_server_policy \
117
+ or flags.l3_firewall_rules or flags.vlans or flags.ssids
118
+
119
+ if not need_networks:
120
+ continue
121
+
122
+ if do_all or flags.networks:
123
+ logger.info(" Syncing networks for org %s (%s)...", org_id, org.name)
124
+ networks = Network.sync(org_id)
125
+ else:
126
+ networks = Network.get(org_id, source="meraki")
127
+
128
+ if not networks:
129
+ logger.warning(" No networks found for org %s (%s).", org_id, org.name)
130
+ continue
131
+
132
+ logger.info(" Found %d network(s) in org %s (%s).", len(networks), org_id, org.name)
133
+
134
+ for network in networks:
135
+ net_id = network.id
136
+ net_name = network.name
137
+ product_types = set(network.product_types or [])
138
+
139
+ # ----------------------------------------------------------
140
+ # DHCP server policy (switch networks only)
141
+ # ----------------------------------------------------------
142
+ if (do_all or flags.dhcp_server_policy) and "switch" in product_types:
143
+ logger.debug(" Syncing DHCP server policy for network %s (%s)...", net_id, net_name)
144
+ DhcpServerPolicy.sync(net_id)
145
+
146
+ # ----------------------------------------------------------
147
+ # L3 firewall rules (appliance networks only)
148
+ # ----------------------------------------------------------
149
+ if (do_all or flags.l3_firewall_rules) and "appliance" in product_types:
150
+ logger.debug(" Syncing L3 firewall rules for network %s (%s)...", net_id, net_name)
151
+ L3FirewallRule.sync(net_id)
152
+
153
+ # ----------------------------------------------------------
154
+ # VLANs (appliance networks only)
155
+ # ----------------------------------------------------------
156
+ if (do_all or flags.vlans) and "appliance" in product_types:
157
+ logger.info(" Syncing VLANs for network %s (%s)...", net_id, net_name)
158
+ Vlan.sync(net_id)
159
+
160
+ # ----------------------------------------------------------
161
+ # SSIDs (wireless networks only)
162
+ # ----------------------------------------------------------
163
+ if (do_all or flags.ssids) and "wireless" in product_types:
164
+ logger.debug(" Syncing SSIDs for network %s (%s)...", net_id, net_name)
165
+ Ssid.sync(net_id)
166
+
167
+ logger.info("Sync complete.")
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import urllib.request
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _INSTALL_SCRIPT_URL = (
13
+ "https://raw.githubusercontent.com/nathanea05/merakisync/main/install.sh"
14
+ )
15
+
16
+
17
+ def run() -> None:
18
+ if not getattr(sys, "frozen", False):
19
+ logger.error(
20
+ "`merakisync update` is only supported for binary installs. "
21
+ "To update a pip-installed library run:\n"
22
+ " pip install --upgrade merakisync\n"
23
+ " merakisync migrate"
24
+ )
25
+ sys.exit(1)
26
+
27
+ binary_path = sys.executable
28
+ install_dir = os.path.dirname(os.path.abspath(binary_path))
29
+
30
+ logger.info("Downloading install script...")
31
+ try:
32
+ with urllib.request.urlopen(_INSTALL_SCRIPT_URL) as resp:
33
+ script_bytes = resp.read()
34
+ except Exception as exc:
35
+ logger.error(
36
+ "Failed to download install script: %s\n"
37
+ "Run the update manually:\n"
38
+ " curl -LsSf %s | sh\n"
39
+ " merakisync migrate",
40
+ exc,
41
+ _INSTALL_SCRIPT_URL,
42
+ )
43
+ sys.exit(1)
44
+
45
+ tmp_fd, tmp_path = tempfile.mkstemp(suffix=".sh")
46
+ try:
47
+ os.write(tmp_fd, script_bytes)
48
+ os.close(tmp_fd)
49
+
50
+ result = subprocess.run(["sh", tmp_path, "--install-dir", install_dir])
51
+ if result.returncode != 0:
52
+ logger.error("Binary update failed (exit code %d).", result.returncode)
53
+ sys.exit(result.returncode)
54
+ finally:
55
+ os.unlink(tmp_path)
56
+
57
+ logger.info("Applying database migrations...")
58
+ migrate = subprocess.run([binary_path, "migrate"])
59
+ if migrate.returncode != 0:
60
+ sys.exit(migrate.returncode)