From 56d533c8021c2424503579f7e4c00048718b4a69 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Mon, 16 Jan 2023 19:02:09 +0800 Subject: [PATCH] v3.0.0-rc1 (#9322) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf:automation * pref: 修改账号推送 * perf: 修改 assets * perf: 修改 accounts * feat: 优化代码 * fix: 修复 ObjectRelatedField 获取 value attr 时先判断是否有 attr 属性 * perf: 增加翻译 * feat: 增加部分翻译 * feat: 去除无用列 * perf: ticket remove app * fix: 修复创建账号备份任务失败的问题 * perf: 添加 accounts app * perf: ticket type serializer (#9252) Co-authored-by: feng <1304903146@qq.com> * perf: ticket * perf: 修改 accounts api * perf: 优化 AssetPermissionSerializer fields 顺序 * perf: 修改 accounts * feat: 限制常用用户名api返回长度 * feat: 限制常用用户名api返回长度 * perf: 修改 LoginAssetACL 序列类,增加 users_username_group, accounts_username_group... 字段 * perf: 修改 CommandFilterACLSerializer 增加 command_groups_amount 字段 * perf: 修改rbac API啥的 (#9254) * perf: migrate * perf: 修改 AssetPermedSerializer domain 字段类型 * perf: 放开push account 权限位 * perf: 修改 accounts * perf: 修改 LoginACLSerializer 字段类型 * pref: 修改数据库 migrations * perf: filter asset systemuser * perf: 修改 SessionSerializer 字段类型 * pref: 修改 applet host * perf: 修改 SessionCommandSerializer 字段类型 * perf: 修改 accounts import * perf: 修改 celery datetime * perf: 修改 asset serializer * pref: 修改 labeled field * feat: 修改翻译 * perf: 修改 JobSerializer 字段类型 * feat: 支持使用 ws 发送终断任务 * perf: add AccessTokenAuthentication * perf: 修改 BaseStorageSerializer 字段类型 * perf: 修改 AppletHostSerializer 字段类型 * perf: signal event * perf: asset types automations (#9259) Co-authored-by: feng <1304903146@qq.com> * perf: 修改下载 rdp 文件时返回的 address 地址信息为空的问题 * perf: 修改 AssetSerializer.accounts.secret 为 write_only; 修改 DomainWithGatewaySerializer.gateways 返回 account 信息及 secret 字段; * perf: automation 干库 (#9260) Co-authored-by: feng <1304903146@qq.com> * perf: account push api * feat: 修改迁移文件 * feat: 删除无用代码 * feat: 优化部分资源无操作日志 * perf: 修改 account * perf: perm tree * perf: asset serializers retrieve * perf: 格式化代码 * perf: AutomationExecution (#9268) Co-authored-by: feng <1304903146@qq.com> * perf: AssetDetailSerializer 和 Asset Model 添加 specific_info 字段; * perf: 修改账号推送 * feat: handle ws heartbeat status * perf: k8s tree (#9269) Co-authored-by: feng <1304903146@qq.com> * perf: 修改账号推送 * perf: 修改 asset detail serializer * fix: 修复 windows 不能运行 powershell 命令的问题 * feat: 支持按照资源时间线查看操作活动 * feat: 翻译 * feat: 优化操作日志 * perf: asset clone * fix: 错误的修改改回去 * perf: create asset account * feat: 增加task 刷新续传功能 * fix: applet host deloypment filter host * perf: 修改了 common 结构,和 push accounts * perf: 整理 common 结构 * perf: 修改 const import * perf: 修改 allow bulk destroy * fix: applet host search fileds * perf: applet bulk delete * fix: applet list 404 * perf: 修改 common view * feat: 增加一些翻译, 修复 playbook 上传的错误 * fix: 修改错别字 * perf: 修改 applets status * perf: 修改网关 api * perf: automateion (#9281) Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> * perf: 失效 connect methods 当 applet 删除 或者 host 删除 * perf: 网关账号的密码类型改成 LabelField * perf: chrome applet script * perf: verify code ttl (#9282) Co-authored-by: feng <1304903146@qq.com> * perf: database ping * perf: ws * perf: 修改网关创建 * perf: account task org (#9285) Co-authored-by: feng <1304903146@qq.com> * perf: asset test api * perf: port 添加 account * pref: 修改 db mapper permission * fix: db port mapper list api * perf: account change secret (#9286) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 setup_eager_loading * perf: SecretStrategy * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * feat: 修改 ConnectionToken Create API 支持校验 ACL 逻辑 * pref: web database 信号转发 * perf: account push automation * perf: push filter account * perf: 修改 publish 版本 * perf: 修改网关 * fix: 修改资产 Specific 信息中 JSONField 字段返回 json.loads 对象 * feat: 远程应用内置Navicat Premium 16 * feat: 更新下载链接 * feat: 整理代码格式 * perf: 修改 terminal point * perf: update chrome applet script * fix: 资产 specific 获取 JSONField 时, 判断值的类型不为 list, dict * perf: domain (#9292) Co-authored-by: feng <1304903146@qq.com> * perf: 优化 endpoint 监听端口,仅 oracle 动态 * perf: 修改翻译 * perf: 修改文案 * perf: 修改缺失的翻译 * perf: 修改 endpoint help text * feat: 还原格式 * feat: 去掉基类 * feat: 增加特权账号字段 * perf: decode content * fix: check pid * perf: 修改 smart endpoint * perf: 修改 endpoint mysql default port * feat: 优化 * perf: 修改 endpoint mysql default port * perf: gateway test (#9295) Co-authored-by: feng <1304903146@qq.com> * perf: migrate * perf: 修改 endpoint mysql default port * fix: 修复获取任务执行结果死循环 * feat: 作业审计日志增加字段 * fix: add on_transaction_commit task post save * perf: gateway (#9297) Co-authored-by: feng <1304903146@qq.com> * feat: 过滤 jumpserver 自动产生的用户 * fix: 修复ops节点选择的问题 * fix: 修改 统一 connection-token 和 command 的 review API 返回数据 from_ticket_info * perf: change secret (#9298) Co-authored-by: feng <1304903146@qq.com> * perf: 修改 db port manager * perf: 修改 db port manager * perf: add celery log mark * perf: remove debug log data * fix: navicat use manual type * fix: remove navicate download url * perf: push_account_enabled (#9301) Co-authored-by: feng <1304903146@qq.com> * fix: 修改navicat启动程序MD5值 * perf: push account (#9303) Co-authored-by: feng <1304903146@qq.com> * feat: Redis/MongoDB 支持SSL * fix: 修改授权规则过滤字段 node_name,node_id; 修复获取授权节点下的资产为空的问题; * perf: push account button (#9305) Co-authored-by: feng <1304903146@qq.com> * perf: account push * fix: 修复获取 /user//assets/tree/ 返回用户授权的所有资产 * perf: asset ping (#9307) Co-authored-by: feng <1304903146@qq.com> * perf: asset enabled_info * perf: 优化activity记录都保存至operatelog中 * feat: 远程应用navicat支持试用版连接 * perf: 优化迁移文件 * perf: 修改资产列表 API category type 字段 choices 根据 category 进行返回 * fix * perf: 修改账号列表 API 解决根据 node_id asset_id 搜索账号列表无效的问题 * fix: navicat dba账号登录 * perf: 优化navicat连接 * perf: 修改账号列表 Model Manager 继承自 OrgManager,解决组织过滤问题 * perf: 修改账号列表 Filter 支持根据 platform,category,type 字段搜索 * perf: change secret email (#9312) Co-authored-by: feng <1304903146@qq.com> * feat: 保证认证信息一定清理 * perf: add mariadb * perf: 修改资产类型树数量统计资产或账号 * perf: applet chrome quit * perf: 优化关闭欢迎页面 * fix * perf: executed amount * perf: 修改 built-in applet installation * perf: 修改资产列表增加标签搜索 * perf: 修改资产列表增加标签搜索 * perf: account task automation (#9319) Co-authored-by: feng <1304903146@qq.com> * perf: account trigger * perf: 修改系统设置文案:批量命令执行 -> 作业中心 * perf: 优化migrate (#9320) Co-authored-by: feng <1304903146@qq.com> * perf: 修改资产节点树 API,支持搜索资产、节点 * perf: audit dashboard (#9321) Co-authored-by: feng <1304903146@qq.com> * fix: 修改 has_perm 权限判断兼容 list 和 str 类型 * perf: 修改一些换行 * perf: 修改 ansible config * fix: oracle依赖文件地址错误 (#9324) * perf: ansible mudules * perf: 修改 runner host cwd Co-authored-by: ibuler Co-authored-by: Aaron3S Co-authored-by: Bai Co-authored-by: feng <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> Co-authored-by: Eric Co-authored-by: jiangweidong Co-authored-by: jiangweidong <80373698+Hi-JWD@users.noreply.github.com> --- .gitignore | 2 +- Dockerfile | 1 - .../backup_account => accounts}/__init__.py | 0 apps/accounts/admin.py | 3 + apps/accounts/api/__init__.py | 2 + .../api/account/__init__.py | 0 .../api/account/account.py | 14 +- .../api/account/backup.py | 19 +- .../api/account/template.py | 12 +- .../api/automations/__init__.py | 1 + .../api/automations/base.py | 20 +- .../accounts/api/automations/change_secret.py | 82 + .../api/automations/gather_accounts.py | 18 +- apps/accounts/api/automations/push_account.py | 69 + apps/accounts/apps.py | 10 + apps/accounts/automations/__init__.py | 2 + .../automations/backup_account}/__init__.py | 0 .../automations/backup_account/handlers.py | 6 +- .../automations/backup_account/manager.py | 0 .../automations/base}/__init__.py | 0 .../automations/base/base_inventory.txt | 14 + apps/accounts/automations/base/manager.py | 55 + .../automations/change_secret}/__init__.py | 0 .../change_secret/database/mongodb/main.yml | 0 .../database/mongodb/manifest.yml | 2 +- .../change_secret/database/mysql/main.yml | 0 .../change_secret/database/mysql/manifest.yml | 3 +- .../change_secret/database/oracle/main.yml | 0 .../database/oracle/manifest.yml | 2 +- .../database/postgresql/main.yml | 0 .../database/postgresql/manifest.yml | 2 +- .../change_secret/database/sqlserver/main.yml | 0 .../database/sqlserver/manifest.yml | 2 +- .../change_secret/demo_inventory.txt | 0 .../change_secret/host/posix/main.yml | 0 .../change_secret/host/posix/manifest.yml | 0 .../change_secret/host/windows/main.yml | 0 .../change_secret/host/windows/manifest.yml | 2 +- .../automations/change_secret/manager.py | 117 +- apps/accounts/automations/endpoint.py | 24 + .../automations/gather_accounts}/__init__.py | 0 .../gather_accounts/database/mongodb/main.yml | 0 .../database/mongodb/manifest.yml | 0 .../gather_accounts/database/mysql/main.yml | 0 .../database/mysql/manifest.yml | 1 + .../gather_accounts/database/oracle/main.yml | 0 .../database/oracle/manifest.yml | 0 .../database/postgresql/main.yml | 0 .../database/postgresql/manifest.yml | 0 .../automations/gather_accounts/filter.py | 0 .../gather_accounts/host/posix/main.yml | 0 .../gather_accounts/host/posix/manifest.yml | 0 .../gather_accounts/host/windows/main.yml | 0 .../gather_accounts/host/windows/manifest.yml | 0 .../automations/gather_accounts/manager.py | 6 +- apps/accounts/automations/methods.py | 30 + .../automations/push_account/__init__.py} | 0 .../automations/push_account/manager.py | 112 + .../automations/verify_account/__init__.py} | 0 .../verify_account/database/mongodb/main.yml | 0 .../database/mongodb/manifest.yml | 0 .../verify_account/database/mysql/main.yml | 0 .../database/mysql/manifest.yml | 0 .../verify_account/database/oracle/main.yml | 0 .../database/oracle/manifest.yml | 0 .../database/postgresql/main.yml | 0 .../database/postgresql/manifest.yml | 0 .../database/sqlserver/main.yml | 0 .../database/sqlserver/manifest.yml | 0 .../verify_account/host/posix/main.yml | 0 .../verify_account/host/posix/manifest.yml | 0 .../verify_account/host/windows/main.yml | 0 .../verify_account/host/windows/manifest.yml | 0 .../automations/verify_account/manager.py | 15 +- apps/accounts/const/__init__.py | 2 + apps/{assets => accounts}/const/account.py | 6 - apps/accounts/const/automation.py | 94 + apps/accounts/filters.py | 49 + apps/accounts/migrations/0001_initial.py | 113 + .../migrations/0002_auto_20220616_0021.py | 44 + apps/accounts/migrations/0003_automation.py | 194 ++ .../migrations/0004_auto_20230106_1507.py | 23 + .../0005_alter_changesecretrecord_options.py | 17 + apps/accounts/migrations/__init__.py | 0 apps/accounts/models/__init__.py | 3 + apps/{assets => accounts}/models/account.py | 5 +- apps/accounts/models/automations/__init__.py | 6 + .../models/automations/backup_account.py} | 19 +- apps/accounts/models/automations/base.py | 41 + .../models/automations/change_secret.py | 30 +- .../models/automations/gather_account.py} | 10 +- .../models/automations/push_account.py | 41 + .../models/automations/verify_account.py | 6 +- apps/accounts/models/base.py | 154 + apps/{assets => accounts}/notifications.py | 24 +- apps/accounts/serializers/__init__.py | 2 + .../serializers/account/__init__.py | 3 +- .../serializers/account/account.py | 11 +- .../serializers/account/backup.py | 23 +- apps/accounts/serializers/account/base.py | 81 + .../serializers/account/template.py | 4 +- .../serializers/automations/__init__.py | 4 + apps/accounts/serializers/automations/base.py | 83 + .../serializers/automations/change_secret.py | 61 +- .../automations/gather_accounts.py | 8 +- .../serializers/automations/push_account.py | 73 + apps/accounts/signal_handlers.py | 26 + apps/accounts/tasks/__init__.py | 5 + apps/accounts/tasks/automation.py | 20 + .../tasks/backup_account.py} | 4 +- apps/accounts/tasks/common.py | 13 + .../tasks/gather_accounts.py | 21 +- apps/accounts/tasks/push_account.py | 43 + .../tasks/verify_account.py | 25 +- apps/accounts/tests.py | 3 + apps/accounts/urls.py | 41 + apps/accounts/utils.py | 54 + apps/acls/api/command_acl.py | 20 +- apps/acls/api/login_acl.py | 2 +- apps/acls/api/login_asset_check.py | 2 +- ...0_alter_commandfilteracl_command_groups.py | 18 + apps/acls/models/base.py | 3 + apps/acls/models/command_acl.py | 2 +- apps/acls/models/login_asset_acl.py | 2 +- apps/acls/serializers/base.py | 25 +- apps/acls/serializers/command_acl.py | 8 +- apps/acls/serializers/login_acl.py | 13 +- .../migrations/0026_auto_20220817_1716.py | 2 +- apps/assets/api/__init__.py | 2 - apps/assets/api/asset/asset.py | 56 +- apps/assets/api/automations/change_secret.py | 49 - apps/assets/api/category.py | 2 +- apps/assets/api/domain.py | 16 +- apps/assets/api/node.py | 17 +- apps/assets/api/platform.py | 4 +- apps/assets/api/tree.py | 38 +- apps/assets/automations/base/manager.py | 62 +- apps/assets/automations/endpoint.py | 15 +- .../gather_facts/database/mysql/manifest.yml | 1 + apps/assets/automations/methods.py | 16 +- .../ping/database/mysql/manifest.yml | 1 + .../automations/ping_gateway/__init__.py | 0 .../automations/ping_gateway/manager.py | 123 + .../automations/push_account/manager.py | 17 - apps/assets/const/__init__.py | 11 +- apps/assets/const/automation.py | 34 +- apps/assets/const/category.py | 6 + apps/assets/const/cloud.py | 1 + apps/assets/const/database.py | 10 +- apps/assets/const/device.py | 1 + apps/assets/const/host.py | 5 +- apps/assets/const/types.py | 27 +- apps/assets/filters.py | 38 +- .../migrations/0093_auto_20220403_1627.py | 5 + .../migrations/0095_auto_20220407_1726.py | 5 - .../migrations/0096_auto_20220426_1550.py | 7 +- .../migrations/0097_auto_20220426_1558.py | 22 + .../migrations/0099_auto_20220711_1409.py | 137 +- .../migrations/0100_auto_20220711_1413.py | 10 +- .../migrations/0101_auto_20220803_1448.py | 27 - ...811_1511.py => 0101_auto_20220811_1511.py} | 6 +- .../migrations/0102_auto_20220803_1859.py | 52 - ...816_1022.py => 0102_auto_20220816_1022.py} | 5 +- ...to_asset.py => 0103_auto_20220902_1021.py} | 5 +- ...817_1544.py => 0104_auto_20220817_1544.py} | 3 +- ...220_1956.py => 0105_auto_20221220_1956.py} | 102 +- .../migrations/0106_auto_20220916_1556.py | 71 - .../migrations/0106_auto_20221228_1838.py | 84 + .../migrations/0107_auto_20221019_1115.py | 175 -- apps/assets/migrations/0107_automation.py | 91 + ...08_alter_automationexecution_automation.py | 19 + .../migrations/0108_auto_20221027_1053.py | 50 - ...09_alter_baseautomation_unique_together.py | 27 + .../migrations/0109_auto_20221102_2017.py | 53 - .../0110_alter_favoriteasset_options.py | 17 + .../0110_changesecretrecord_asset.py | 24 - .../0111_alter_automationexecution_status.py | 18 - .../migrations/0113_auto_20221122_2015.py | 43 - .../migrations/0114_remove_redundant_macos.py | 34 - .../0116_alter_automationexecution_options.py | 17 - .../migrations/0117_alter_gateway_options.py | 17 - .../migrations/0118_auto_20221227_1504.py | 50 - .../migrations/0119_auto_20221227_1740.py | 21 - apps/assets/models/__init__.py | 8 +- apps/assets/models/asset/common.py | 68 +- apps/assets/models/asset/database.py | 11 +- apps/assets/models/automations/__init__.py | 6 +- apps/assets/models/automations/base.py | 55 +- .../assets/models/automations/gather_facts.py | 4 +- apps/assets/models/automations/ping.py | 4 +- .../assets/models/automations/push_account.py | 20 - apps/assets/models/base.py | 138 +- apps/assets/models/domain.py | 14 +- apps/assets/models/favorite_asset.py | 5 + apps/assets/models/gateway.py | 90 +- apps/assets/models/node.py | 6 + apps/assets/models/platform.py | 10 +- apps/assets/models/utils.py | 61 - apps/assets/serializers/__init__.py | 1 - apps/assets/serializers/account/base.py | 30 - apps/assets/serializers/asset/common.py | 143 +- apps/assets/serializers/asset/database.py | 17 +- .../serializers/automations/__init__.py | 2 - apps/assets/serializers/automations/base.py | 20 +- apps/assets/serializers/base.py | 54 - apps/assets/serializers/domain.py | 18 +- apps/assets/serializers/favorite_asset.py | 3 +- apps/assets/serializers/gateway.py | 28 +- apps/assets/serializers/platform.py | 17 +- apps/assets/serializers/utils.py | 23 - apps/assets/signal_handlers/__init__.py | 1 - apps/assets/signal_handlers/account.py | 15 - apps/assets/signal_handlers/asset.py | 54 +- apps/assets/tasks/__init__.py | 4 - apps/assets/tasks/automation.py | 2 +- apps/assets/tasks/common.py | 44 + apps/assets/tasks/gather_facts.py | 62 +- apps/assets/tasks/ping.py | 57 +- apps/assets/tasks/push_account.py | 41 - apps/assets/urls/api_urls.py | 21 - apps/assets/utils/node.py | 2 +- apps/audits/api.py | 26 +- apps/audits/backends/db.py | 24 +- apps/audits/const.py | 36 +- apps/audits/handler.py | 49 +- .../migrations/0016_auto_20221111_1919.py | 8 +- .../migrations/0018_operatelog_resource_id.py | 18 + .../0019_alter_operatelog_options.py | 17 + .../migrations/0020_auto_20230112_1034.py} | 10 +- apps/audits/models.py | 6 + apps/audits/serializers.py | 19 +- apps/audits/signal_handlers.py | 38 +- apps/audits/urls/api_urls.py | 6 +- apps/audits/utils.py | 63 +- apps/authentication/api/connection_token.py | 70 +- apps/authentication/api/dingtalk.py | 2 +- apps/authentication/api/feishu.py | 2 +- apps/authentication/api/sso.py | 4 +- apps/authentication/api/temp_token.py | 2 +- apps/authentication/api/wecom.py | 2 +- .../migrations/0017_auto_20230105_1743.py | 25 + .../authentication/models/connection_token.py | 13 +- apps/authentication/serializers/confirm.py | 2 +- .../serializers/connect_token_secret.py | 17 +- .../serializers/connection_token.py | 19 +- .../serializers/password_mfa.py | 2 +- apps/authentication/views/dingtalk.py | 7 +- apps/authentication/views/feishu.py | 5 +- apps/authentication/views/wecom.py | 7 +- apps/common/{mixins => }/api/__init__.py | 7 +- apps/common/{mixins => }/api/action.py | 0 apps/common/{api.py => api/common.py} | 16 +- apps/common/{mixins => }/api/filter.py | 0 apps/common/api/generic.py | 21 + .../{mixins/api/common.py => api/mixin.py} | 28 +- apps/common/{mixins => }/api/patch.py | 0 apps/common/{mixins => }/api/permission.py | 4 +- apps/common/{mixins => }/api/serializer.py | 0 apps/common/compat.py | 82 - apps/common/db/fields.py | 32 +- apps/common/db/managers.py | 13 + .../common/{mixins/models.py => db/mixins.py} | 12 - apps/common/db/models.py | 41 - apps/common/db/validators.py | 22 + apps/common/drf/api.py | 33 - apps/common/drf/exc_handlers.py | 9 +- apps/common/drf/metadata.py | 17 +- .../commands/services/services/celery_base.py | 7 +- apps/common/mixins/__init__.py | 5 - apps/common/mixins/api/queryset.py | 14 - apps/common/{drf => }/serializers/__init__.py | 0 apps/common/{drf => }/serializers/common.py | 0 apps/common/{drf => serializers}/fields.py | 75 +- apps/common/{drf => }/serializers/mixin.py | 16 +- apps/common/signal_handlers.py | 18 + apps/common/utils/http.py | 7 + apps/common/utils/verify_code.py | 6 +- apps/common/views/__init__.py | 1 + apps/common/{ => views}/http.py | 9 - .../{mixins/views.py => views/mixins.py} | 1 - apps/common/{views.py => views/msg.py} | 0 apps/jumpserver/api.py | 2 +- apps/jumpserver/conf.py | 13 +- apps/jumpserver/middleware.py | 44 +- apps/jumpserver/routing.py | 12 +- apps/jumpserver/settings/base.py | 7 +- apps/jumpserver/settings/custom.py | 3 +- apps/jumpserver/settings/libs.py | 2 - apps/jumpserver/settings/logging.py | 10 +- apps/jumpserver/urls.py | 5 +- apps/jumpserver/views/index.py | 2 +- apps/jumpserver/views/other.py | 8 +- apps/locale/ja/LC_MESSAGES/django.mo | 4 +- apps/locale/ja/LC_MESSAGES/django.po | 2617 ++++++++++------- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 2540 +++++++++------- apps/notifications/api/notifications.py | 2 +- apps/notifications/api/site_msgs.py | 6 +- .../serializers/notifications.py | 2 +- apps/ops/ansible/ansible.cfg | 13 + .../ansible/modules_utils/oracle_common.py | 2 +- apps/ops/ansible/runner.py | 3 +- apps/ops/api/celery.py | 2 +- apps/ops/api/job.py | 8 +- apps/ops/celery/logger.py | 10 +- apps/ops/const.py | 4 + .../ops/migrations/0022_auto_20220817_1346.py | 64 +- .../ops/migrations/0023_auto_20220912_0021.py | 245 ++ .../ops/migrations/0023_auto_20220929_2025.py | 44 - ...0024_alter_celerytask_date_last_publish.py | 23 + .../ops/migrations/0024_auto_20221008_1514.py | 58 - .../ops/migrations/0025_auto_20221008_1631.py | 72 - .../ops/migrations/0026_auto_20221009_2050.py | 108 - .../ops/migrations/0027_auto_20221024_1709.py | 273 -- .../ops/migrations/0028_auto_20221205_1627.py | 34 - .../ops/migrations/0029_auto_20221215_1712.py | 33 - .../ops/migrations/0030_auto_20221220_1941.py | 80 - .../ops/migrations/0031_auto_20221220_1956.py | 57 - .../ops/migrations/0032_auto_20221221_1513.py | 23 - .../ops/migrations/0033_auto_20221223_1536.py | 41 - .../0034_alter_celerytask_options.py | 17 - .../ops/migrations/0035_auto_20221227_1520.py | 33 - apps/ops/models/adhoc.py | 5 +- apps/ops/models/celery.py | 4 +- apps/ops/models/job.py | 35 +- apps/ops/serializers/adhoc.py | 6 +- apps/ops/serializers/celery.py | 2 +- apps/ops/serializers/job.py | 54 +- apps/ops/serializers/playbook.py | 2 +- apps/ops/signal_handlers.py | 2 +- apps/ops/tasks.py | 12 +- apps/ops/urls/api_urls.py | 2 +- apps/ops/utils.py | 66 - apps/ops/views.py | 2 +- apps/ops/ws.py | 17 +- apps/orgs/api.py | 24 +- apps/orgs/caches.py | 3 +- apps/orgs/mixins/api.py | 14 +- apps/orgs/mixins/serializers.py | 18 +- apps/orgs/models.py | 14 +- apps/orgs/signal_handlers/cache.py | 3 +- apps/orgs/utils.py | 5 + apps/perms/api/asset_permission_relation.py | 4 +- apps/perms/api/user_permission/tree/asset.py | 8 +- apps/perms/api/user_permission/tree/mixin.py | 4 +- .../user_permission/tree/node_with_asset.py | 11 +- apps/perms/filters.py | 11 +- apps/perms/hands.py | 5 +- .../migrations/0032_auto_20221111_1919.py | 8 +- .../0033_alter_assetpermission_actions.py | 18 - ...220_1956.py => 0033_auto_20221220_1956.py} | 3 +- apps/perms/models/asset_permission.py | 6 +- apps/perms/models/perm_node.py | 3 +- apps/perms/serializers/permission.py | 15 +- apps/perms/serializers/permission_relation.py | 2 +- apps/perms/serializers/user_permission.py | 9 +- apps/perms/urls/user_permission.py | 2 +- apps/perms/utils/account.py | 4 +- apps/perms/utils/permission.py | 18 +- apps/perms/utils/user_perm.py | 2 +- apps/rbac/api/permission.py | 2 +- apps/rbac/api/role.py | 5 +- apps/rbac/backends.py | 7 +- apps/rbac/builtin.py | 2 - apps/rbac/const.py | 26 +- .../0011_remove_redundant_permission.py | 12 +- apps/rbac/serializers/role.py | 2 +- apps/rbac/tree.py | 29 +- apps/settings/serializers/auth/dingtalk.py | 2 +- apps/settings/serializers/auth/feishu.py | 3 +- apps/settings/serializers/auth/ldap.py | 2 +- apps/settings/serializers/auth/oauth2.py | 3 +- apps/settings/serializers/auth/oidc.py | 3 +- apps/settings/serializers/auth/radius.py | 2 +- apps/settings/serializers/auth/sms.py | 2 +- apps/settings/serializers/auth/wecom.py | 2 +- apps/settings/serializers/email.py | 5 +- apps/settings/serializers/security.py | 7 +- apps/static/img/login_image.png | Bin 188631 -> 488955 bytes apps/terminal/api/applet/applet.py | 10 +- apps/terminal/api/applet/host.py | 6 +- apps/terminal/api/applet/relation.py | 2 +- apps/terminal/api/component/endpoint.py | 2 +- apps/terminal/api/component/terminal.py | 2 +- apps/terminal/api/db_listen_port.py | 19 +- apps/terminal/api/session/command.py | 4 +- apps/terminal/api/session/session.py | 2 +- apps/terminal/applets/chrome/app.py | 42 +- apps/terminal/applets/chrome/common.py | 28 +- apps/terminal/applets/navicat/README.md | 5 + apps/terminal/applets/navicat/app.py | 216 ++ apps/terminal/applets/navicat/common.py | 212 ++ apps/terminal/applets/navicat/i18n.yml | 3 + apps/terminal/applets/navicat/icon.png | Bin 0 -> 5132 bytes apps/terminal/applets/navicat/main.py | 22 + apps/terminal/applets/navicat/manifest.yml | 17 + apps/terminal/applets/navicat/setup.yml | 6 + apps/terminal/backends/command/serializers.py | 13 +- apps/terminal/connect_methods.py | 1 + apps/terminal/const.py | 7 + apps/terminal/migrations/0001_initial.py | 14 +- .../migrations/0048_endpoint_endpointrule.py | 14 +- .../migrations/0049_endpoint_redis_port.py | 2 +- .../migrations/0052_auto_20220713_1417.py | 4 +- .../migrations/0054_auto_20221027_1125.py | 86 +- .../migrations/0055_auto_20221031_1848.py | 49 - ...220_1956.py => 0055_auto_20221228_1804.py} | 64 +- .../migrations/0056_auto_20221101_1353.py | 23 - .../migrations/0056_auto_20221228_1808.py | 125 + .../migrations/0057_auto_20221102_1941.py | 24 - .../migrations/0057_auto_20230109_1447.py | 40 + .../migrations/0058_auto_20221103_1624.py | 33 - .../migrations/0058_auto_20230110_1445.py | 27 + .../0059_applethostdeployment_task.py | 18 - ...0060_alter_applethostdeployment_options.py | 17 - ...0061_rename_system_user_command_account.py | 23 - .../migrations/0062_auto_20221216_1529.py | 23 - .../migrations/0065_auto_20221223_1536.py | 21 - apps/terminal/models/applet/applet.py | 14 +- apps/terminal/models/applet/host.py | 4 +- apps/terminal/models/component/endpoint.py | 20 +- apps/terminal/serializers/applet.py | 15 +- apps/terminal/serializers/applet_host.py | 9 +- apps/terminal/serializers/endpoint.py | 43 +- apps/terminal/serializers/session.py | 9 +- apps/terminal/serializers/storage.py | 7 +- apps/terminal/serializers/terminal.py | 4 +- apps/terminal/signal_handlers/__init__.py | 3 + .../applet.py} | 41 +- apps/terminal/signal_handlers/db_port.py | 39 + apps/terminal/signal_handlers/terminal.py | 35 + apps/terminal/urls/api_urls.py | 6 +- apps/terminal/urls/ws_urls.py | 9 + apps/terminal/utils/db_port_mapper.py | 75 +- apps/terminal/ws.py | 79 + apps/tickets/api/flow.py | 2 +- apps/tickets/api/relation.py | 2 +- apps/tickets/api/ticket.py | 4 +- apps/tickets/handlers/base.py | 4 +- apps/tickets/handlers/login_asset_confirm.py | 6 + .../migrations/0028_remove_app_tickets.py | 18 + .../migrations/0029_auto_20230110_1445.py | 28 + apps/tickets/models/comment.py | 3 + apps/tickets/models/ticket/apply_asset.py | 7 +- apps/tickets/models/ticket/general.py | 39 +- .../models/ticket/login_asset_confirm.py | 6 + apps/tickets/serializers/comment.py | 4 +- apps/tickets/serializers/flow.py | 3 +- apps/tickets/serializers/super_ticket.py | 8 +- .../tickets/serializers/ticket/apply_asset.py | 2 +- apps/tickets/serializers/ticket/ticket.py | 17 +- apps/users/api/relation.py | 2 +- apps/users/api/user.py | 4 +- apps/users/serializers/profile.py | 2 +- apps/users/serializers/user.py | 4 +- apps/users/views/profile/mfa.py | 3 +- apps/users/views/profile/otp.py | 4 +- apps/users/views/profile/pubkey.py | 2 +- generateV3Data.py | 38 - 459 files changed, 8461 insertions(+), 6595 deletions(-) rename apps/{assets/automations/backup_account => accounts}/__init__.py (100%) create mode 100644 apps/accounts/admin.py create mode 100644 apps/accounts/api/__init__.py rename apps/{assets => accounts}/api/account/__init__.py (100%) rename apps/{assets => accounts}/api/account/account.py (92%) rename apps/{assets => accounts}/api/account/backup.py (70%) rename apps/{assets => accounts}/api/account/template.py (67%) rename apps/{assets => accounts}/api/automations/__init__.py (74%) rename apps/{assets => accounts}/api/automations/base.py (88%) create mode 100644 apps/accounts/api/automations/change_secret.py rename apps/{assets => accounts}/api/automations/gather_accounts.py (52%) create mode 100644 apps/accounts/api/automations/push_account.py create mode 100644 apps/accounts/apps.py create mode 100644 apps/accounts/automations/__init__.py rename apps/{assets/automations/change_secret => accounts/automations/backup_account}/__init__.py (100%) rename apps/{assets => accounts}/automations/backup_account/handlers.py (97%) rename apps/{assets => accounts}/automations/backup_account/manager.py (100%) rename apps/{assets/automations/gather_accounts => accounts/automations/base}/__init__.py (100%) create mode 100644 apps/accounts/automations/base/base_inventory.txt create mode 100644 apps/accounts/automations/base/manager.py rename apps/{assets/automations/push_account => accounts/automations/change_secret}/__init__.py (100%) rename apps/{assets => accounts}/automations/change_secret/database/mongodb/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/database/mongodb/manifest.yml (71%) rename apps/{assets => accounts}/automations/change_secret/database/mysql/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/database/mysql/manifest.yml (65%) rename apps/{assets => accounts}/automations/change_secret/database/oracle/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/database/oracle/manifest.yml (71%) rename apps/{assets => accounts}/automations/change_secret/database/postgresql/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/database/postgresql/manifest.yml (71%) rename apps/{assets => accounts}/automations/change_secret/database/sqlserver/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/database/sqlserver/manifest.yml (71%) rename apps/{assets => accounts}/automations/change_secret/demo_inventory.txt (100%) rename apps/{assets => accounts}/automations/change_secret/host/posix/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/host/posix/manifest.yml (100%) rename apps/{assets => accounts}/automations/change_secret/host/windows/main.yml (100%) rename apps/{assets => accounts}/automations/change_secret/host/windows/manifest.yml (67%) rename apps/{assets => accounts}/automations/change_secret/manager.py (66%) create mode 100644 apps/accounts/automations/endpoint.py rename apps/{assets/automations/verify_account => accounts/automations/gather_accounts}/__init__.py (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/mongodb/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/mongodb/manifest.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/mysql/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/mysql/manifest.yml (90%) rename apps/{assets => accounts}/automations/gather_accounts/database/oracle/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/oracle/manifest.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/postgresql/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/database/postgresql/manifest.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/filter.py (100%) rename apps/{assets => accounts}/automations/gather_accounts/host/posix/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/host/posix/manifest.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/host/windows/main.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/host/windows/manifest.yml (100%) rename apps/{assets => accounts}/automations/gather_accounts/manager.py (93%) create mode 100644 apps/accounts/automations/methods.py rename apps/{assets/backends/db.py => accounts/automations/push_account/__init__.py} (100%) create mode 100644 apps/accounts/automations/push_account/manager.py rename apps/{assets/serializers/account.py => accounts/automations/verify_account/__init__.py} (100%) rename apps/{assets => accounts}/automations/verify_account/database/mongodb/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/mongodb/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/mysql/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/mysql/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/oracle/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/oracle/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/postgresql/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/postgresql/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/sqlserver/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/database/sqlserver/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/host/posix/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/host/posix/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/host/windows/main.yml (100%) rename apps/{assets => accounts}/automations/verify_account/host/windows/manifest.yml (100%) rename apps/{assets => accounts}/automations/verify_account/manager.py (51%) create mode 100644 apps/accounts/const/__init__.py rename apps/{assets => accounts}/const/account.py (80%) create mode 100644 apps/accounts/const/automation.py create mode 100644 apps/accounts/filters.py create mode 100644 apps/accounts/migrations/0001_initial.py create mode 100644 apps/accounts/migrations/0002_auto_20220616_0021.py create mode 100644 apps/accounts/migrations/0003_automation.py create mode 100644 apps/accounts/migrations/0004_auto_20230106_1507.py create mode 100644 apps/accounts/migrations/0005_alter_changesecretrecord_options.py create mode 100644 apps/accounts/migrations/__init__.py create mode 100644 apps/accounts/models/__init__.py rename apps/{assets => accounts}/models/account.py (96%) create mode 100644 apps/accounts/models/automations/__init__.py rename apps/{assets/models/backup.py => accounts/models/automations/backup_account.py} (85%) create mode 100644 apps/accounts/models/automations/base.py rename apps/{assets => accounts}/models/automations/change_secret.py (83%) rename apps/{assets/models/automations/gather_accounts.py => accounts/models/automations/gather_account.py} (58%) create mode 100644 apps/accounts/models/automations/push_account.py rename apps/{assets => accounts}/models/automations/verify_account.py (67%) create mode 100644 apps/accounts/models/base.py rename apps/{assets => accounts}/notifications.py (61%) create mode 100644 apps/accounts/serializers/__init__.py rename apps/{assets => accounts}/serializers/account/__init__.py (77%) rename apps/{assets => accounts}/serializers/account/account.py (92%) rename apps/{assets => accounts}/serializers/account/backup.py (58%) create mode 100644 apps/accounts/serializers/account/base.py rename apps/{assets => accounts}/serializers/account/template.py (89%) create mode 100644 apps/accounts/serializers/automations/__init__.py create mode 100644 apps/accounts/serializers/automations/base.py rename apps/{assets => accounts}/serializers/automations/change_secret.py (77%) rename apps/{assets => accounts}/serializers/automations/gather_accounts.py (68%) create mode 100644 apps/accounts/serializers/automations/push_account.py create mode 100644 apps/accounts/signal_handlers.py create mode 100644 apps/accounts/tasks/__init__.py create mode 100644 apps/accounts/tasks/automation.py rename apps/{assets/tasks/backup.py => accounts/tasks/backup_account.py} (82%) create mode 100644 apps/accounts/tasks/common.py rename apps/{assets => accounts}/tasks/gather_accounts.py (63%) create mode 100644 apps/accounts/tasks/push_account.py rename apps/{assets => accounts}/tasks/verify_account.py (59%) create mode 100644 apps/accounts/tests.py create mode 100644 apps/accounts/urls.py create mode 100644 apps/accounts/utils.py create mode 100644 apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py delete mode 100644 apps/assets/api/automations/change_secret.py create mode 100644 apps/assets/automations/ping_gateway/__init__.py create mode 100644 apps/assets/automations/ping_gateway/manager.py delete mode 100644 apps/assets/automations/push_account/manager.py delete mode 100644 apps/assets/migrations/0101_auto_20220803_1448.py rename apps/assets/migrations/{0103_auto_20220811_1511.py => 0101_auto_20220811_1511.py} (87%) delete mode 100644 apps/assets/migrations/0102_auto_20220803_1859.py rename apps/assets/migrations/{0104_auto_20220816_1022.py => 0102_auto_20220816_1022.py} (94%) rename apps/assets/migrations/{0112_gateway_to_asset.py => 0103_auto_20220902_1021.py} (95%) rename apps/assets/migrations/{0105_auto_20220817_1544.py => 0104_auto_20220817_1544.py} (97%) rename apps/assets/migrations/{0115_auto_20221220_1956.py => 0105_auto_20221220_1956.py} (55%) delete mode 100644 apps/assets/migrations/0106_auto_20220916_1556.py create mode 100644 apps/assets/migrations/0106_auto_20221228_1838.py delete mode 100644 apps/assets/migrations/0107_auto_20221019_1115.py create mode 100644 apps/assets/migrations/0107_automation.py create mode 100644 apps/assets/migrations/0108_alter_automationexecution_automation.py delete mode 100644 apps/assets/migrations/0108_auto_20221027_1053.py create mode 100644 apps/assets/migrations/0109_alter_baseautomation_unique_together.py delete mode 100644 apps/assets/migrations/0109_auto_20221102_2017.py create mode 100644 apps/assets/migrations/0110_alter_favoriteasset_options.py delete mode 100644 apps/assets/migrations/0110_changesecretrecord_asset.py delete mode 100644 apps/assets/migrations/0111_alter_automationexecution_status.py delete mode 100644 apps/assets/migrations/0113_auto_20221122_2015.py delete mode 100644 apps/assets/migrations/0114_remove_redundant_macos.py delete mode 100644 apps/assets/migrations/0116_alter_automationexecution_options.py delete mode 100644 apps/assets/migrations/0117_alter_gateway_options.py delete mode 100644 apps/assets/migrations/0118_auto_20221227_1504.py delete mode 100644 apps/assets/migrations/0119_auto_20221227_1740.py delete mode 100644 apps/assets/models/automations/push_account.py delete mode 100644 apps/assets/serializers/account/base.py delete mode 100644 apps/assets/signal_handlers/account.py delete mode 100644 apps/assets/tasks/push_account.py create mode 100644 apps/audits/migrations/0018_operatelog_resource_id.py create mode 100644 apps/audits/migrations/0019_alter_operatelog_options.py rename apps/{terminal/migrations/0063_applet_builtin.py => audits/migrations/0020_auto_20230112_1034.py} (56%) create mode 100644 apps/authentication/migrations/0017_auto_20230105_1743.py rename apps/common/{mixins => }/api/__init__.py (75%) rename apps/common/{mixins => }/api/action.py (100%) rename apps/common/{api.py => api/common.py} (87%) rename apps/common/{mixins => }/api/filter.py (100%) create mode 100644 apps/common/api/generic.py rename apps/common/{mixins/api/common.py => api/mixin.py} (84%) rename apps/common/{mixins => }/api/patch.py (100%) rename apps/common/{mixins => }/api/permission.py (90%) rename apps/common/{mixins => }/api/serializer.py (100%) delete mode 100644 apps/common/compat.py create mode 100644 apps/common/db/managers.py rename apps/common/{mixins/models.py => db/mixins.py} (76%) create mode 100644 apps/common/db/validators.py delete mode 100644 apps/common/drf/api.py delete mode 100644 apps/common/mixins/__init__.py delete mode 100644 apps/common/mixins/api/queryset.py rename apps/common/{drf => }/serializers/__init__.py (100%) rename apps/common/{drf => }/serializers/common.py (100%) rename apps/common/{drf => serializers}/fields.py (77%) rename apps/common/{drf => }/serializers/mixin.py (96%) create mode 100644 apps/common/views/__init__.py rename apps/common/{ => views}/http.py (56%) rename apps/common/{mixins/views.py => views/mixins.py} (98%) rename apps/common/{views.py => views/msg.py} (100%) create mode 100644 apps/ops/ansible/ansible.cfg create mode 100644 apps/ops/migrations/0023_auto_20220912_0021.py delete mode 100644 apps/ops/migrations/0023_auto_20220929_2025.py create mode 100644 apps/ops/migrations/0024_alter_celerytask_date_last_publish.py delete mode 100644 apps/ops/migrations/0024_auto_20221008_1514.py delete mode 100644 apps/ops/migrations/0025_auto_20221008_1631.py delete mode 100644 apps/ops/migrations/0026_auto_20221009_2050.py delete mode 100644 apps/ops/migrations/0027_auto_20221024_1709.py delete mode 100644 apps/ops/migrations/0028_auto_20221205_1627.py delete mode 100644 apps/ops/migrations/0029_auto_20221215_1712.py delete mode 100644 apps/ops/migrations/0030_auto_20221220_1941.py delete mode 100644 apps/ops/migrations/0031_auto_20221220_1956.py delete mode 100644 apps/ops/migrations/0032_auto_20221221_1513.py delete mode 100644 apps/ops/migrations/0033_auto_20221223_1536.py delete mode 100644 apps/ops/migrations/0034_alter_celerytask_options.py delete mode 100644 apps/ops/migrations/0035_auto_20221227_1520.py delete mode 100644 apps/perms/migrations/0033_alter_assetpermission_actions.py rename apps/perms/migrations/{0034_auto_20221220_1956.py => 0033_auto_20221220_1956.py} (96%) create mode 100644 apps/terminal/applets/navicat/README.md create mode 100644 apps/terminal/applets/navicat/app.py create mode 100644 apps/terminal/applets/navicat/common.py create mode 100644 apps/terminal/applets/navicat/i18n.yml create mode 100644 apps/terminal/applets/navicat/icon.png create mode 100644 apps/terminal/applets/navicat/main.py create mode 100644 apps/terminal/applets/navicat/manifest.yml create mode 100644 apps/terminal/applets/navicat/setup.yml delete mode 100644 apps/terminal/migrations/0055_auto_20221031_1848.py rename apps/terminal/migrations/{0064_auto_20221220_1956.py => 0055_auto_20221228_1804.py} (81%) delete mode 100644 apps/terminal/migrations/0056_auto_20221101_1353.py create mode 100644 apps/terminal/migrations/0056_auto_20221228_1808.py delete mode 100644 apps/terminal/migrations/0057_auto_20221102_1941.py create mode 100644 apps/terminal/migrations/0057_auto_20230109_1447.py delete mode 100644 apps/terminal/migrations/0058_auto_20221103_1624.py create mode 100644 apps/terminal/migrations/0058_auto_20230110_1445.py delete mode 100644 apps/terminal/migrations/0059_applethostdeployment_task.py delete mode 100644 apps/terminal/migrations/0060_alter_applethostdeployment_options.py delete mode 100644 apps/terminal/migrations/0061_rename_system_user_command_account.py delete mode 100644 apps/terminal/migrations/0062_auto_20221216_1529.py delete mode 100644 apps/terminal/migrations/0065_auto_20221223_1536.py create mode 100644 apps/terminal/signal_handlers/__init__.py rename apps/terminal/{signal_handlers.py => signal_handlers/applet.py} (64%) create mode 100644 apps/terminal/signal_handlers/db_port.py create mode 100644 apps/terminal/signal_handlers/terminal.py create mode 100644 apps/terminal/urls/ws_urls.py create mode 100644 apps/terminal/ws.py create mode 100644 apps/tickets/migrations/0028_remove_app_tickets.py create mode 100644 apps/tickets/migrations/0029_auto_20230110_1445.py delete mode 100644 generateV3Data.py diff --git a/.gitignore b/.gitignore index 3f398175f..316630d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,4 @@ release/* releashe /apps/script.py data/* - +test.py diff --git a/Dockerfile b/Dockerfile index cf8f73a29..25ecaac3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -99,7 +99,6 @@ VOLUME /opt/jumpserver/data VOLUME /opt/jumpserver/logs ENV LANG=zh_CN.UTF-8 -ENV ANSIBLE_LIBRARY=/opt/jumpserver/apps/ops/ansible/modules EXPOSE 8080 diff --git a/apps/assets/automations/backup_account/__init__.py b/apps/accounts/__init__.py similarity index 100% rename from apps/assets/automations/backup_account/__init__.py rename to apps/accounts/__init__.py diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/apps/accounts/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/accounts/api/__init__.py b/apps/accounts/api/__init__.py new file mode 100644 index 000000000..e49a88b3d --- /dev/null +++ b/apps/accounts/api/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automations import * diff --git a/apps/assets/api/account/__init__.py b/apps/accounts/api/account/__init__.py similarity index 100% rename from apps/assets/api/account/__init__.py rename to apps/accounts/api/account/__init__.py diff --git a/apps/assets/api/account/account.py b/apps/accounts/api/account/account.py similarity index 92% rename from apps/assets/api/account/account.py rename to apps/accounts/api/account/account.py index 8dad13b3c..9aa31c72d 100644 --- a/apps/assets/api/account/account.py +++ b/apps/accounts/api/account/account.py @@ -3,12 +3,13 @@ from rest_framework.decorators import action from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.response import Response -from assets import serializers -from assets.filters import AccountFilterSet -from assets.models import Account, Asset -from assets.tasks import verify_accounts_connectivity +from accounts import serializers +from accounts.filters import AccountFilterSet +from accounts.models import Account +from accounts.tasks import verify_accounts_connectivity +from assets.models import Asset from authentication.const import ConfirmType -from common.mixins import RecordViewLogMixin +from common.views.mixins import RecordViewLogMixin from common.permissions import UserConfirmation from orgs.mixins.api import OrgBulkModelViewSet @@ -25,10 +26,9 @@ class AccountViewSet(OrgBulkModelViewSet): filterset_class = AccountFilterSet serializer_classes = { 'default': serializers.AccountSerializer, - 'verify': serializers.AssetTaskSerializer } rbac_perms = { - 'verify': 'assets.test_account', + 'verify_account': 'assets.test_account', 'partial_update': 'assets.change_accountsecret', 'su_from_accounts': 'assets.view_account', } diff --git a/apps/assets/api/account/backup.py b/apps/accounts/api/account/backup.py similarity index 70% rename from apps/assets/api/account/backup.py rename to apps/accounts/api/account/backup.py index ff46c3caf..216cbd585 100644 --- a/apps/assets/api/account/backup.py +++ b/apps/accounts/api/account/backup.py @@ -5,10 +5,10 @@ from rest_framework.response import Response from orgs.mixins.api import OrgBulkModelViewSet from common.const.choices import Trigger -from assets import serializers -from assets.tasks import execute_account_backup_plan -from assets.models import ( - AccountBackupPlan, AccountBackupPlanExecution +from accounts import serializers +from accounts.tasks import execute_account_backup_plan +from accounts.models import ( + AccountBackupAutomation, AccountBackupExecution ) __all__ = [ @@ -17,12 +17,12 @@ __all__ = [ class AccountBackupPlanViewSet(OrgBulkModelViewSet): - model = AccountBackupPlan + model = AccountBackupAutomation filter_fields = ('name',) search_fields = filter_fields ordering_fields = ('name',) ordering = ('name',) - serializer_class = serializers.AccountBackupPlanSerializer + serializer_class = serializers.AccountBackupSerializer class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): @@ -32,7 +32,7 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'post', 'options'] def get_queryset(self): - queryset = AccountBackupPlanExecution.objects.all() + queryset = AccountBackupExecution.objects.all() return queryset def create(self, request, *args, **kwargs): @@ -41,8 +41,3 @@ class AccountBackupPlanExecutionViewSet(viewsets.ModelViewSet): pid = serializer.data.get('plan') task = execute_account_backup_plan.delay(pid=pid, trigger=Trigger.manual) return Response({'task': task.id}, status=status.HTTP_201_CREATED) - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = queryset.order_by('-date_start') - return queryset diff --git a/apps/assets/api/account/template.py b/apps/accounts/api/account/template.py similarity index 67% rename from apps/assets/api/account/template.py rename to apps/accounts/api/account/template.py index aa3be6b4f..cee8bac91 100644 --- a/apps/assets/api/account/template.py +++ b/apps/accounts/api/account/template.py @@ -1,7 +1,10 @@ -from assets import serializers -from assets.models import AccountTemplate -from common.mixins import RecordViewLogMixin +from rbac.permissions import RBACPermission +from common.permissions import UserConfirmation, ConfirmType + +from common.views.mixins import RecordViewLogMixin from orgs.mixins.api import OrgBulkModelViewSet +from accounts import serializers +from accounts.models import AccountTemplate class AccountTemplateViewSet(OrgBulkModelViewSet): @@ -18,8 +21,7 @@ class AccountTemplateSecretsViewSet(RecordViewLogMixin, AccountTemplateViewSet): 'default': serializers.AccountTemplateSecretSerializer, } http_method_names = ['get', 'options'] - # Todo: 记得打开 - # permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] + permission_classes = [RBACPermission, UserConfirmation.require(ConfirmType.MFA)] rbac_perms = { 'list': 'assets.view_accounttemplatesecret', 'retrieve': 'assets.view_accounttemplatesecret', diff --git a/apps/assets/api/automations/__init__.py b/apps/accounts/api/automations/__init__.py similarity index 74% rename from apps/assets/api/automations/__init__.py rename to apps/accounts/api/automations/__init__.py index e4daeda95..2b0aa0029 100644 --- a/apps/assets/api/automations/__init__.py +++ b/apps/accounts/api/automations/__init__.py @@ -1,3 +1,4 @@ from .base import * from .change_secret import * from .gather_accounts import * +from .push_account import * diff --git a/apps/assets/api/automations/base.py b/apps/accounts/api/automations/base.py similarity index 88% rename from apps/assets/api/automations/base.py rename to apps/accounts/api/automations/base.py index 23c2ef129..12fcd7b17 100644 --- a/apps/assets/api/automations/base.py +++ b/apps/accounts/api/automations/base.py @@ -4,8 +4,9 @@ from rest_framework import status, mixins, viewsets from rest_framework.response import Response from assets import serializers -from assets.models import BaseAutomation, AutomationExecution -from assets.tasks import execute_automation +from assets.models import BaseAutomation +from accounts.tasks import execute_automation +from accounts.models import AutomationExecution from common.const.choices import Trigger from orgs.mixins import generics @@ -17,13 +18,14 @@ __all__ = [ class AutomationAssetsListApi(generics.ListAPIView): + model = BaseAutomation serializer_class = serializers.AutomationAssetsSerializer filter_fields = ("name", "address") search_fields = filter_fields def get_object(self): pk = self.kwargs.get('pk') - return get_object_or_404(BaseAutomation, pk=pk) + return get_object_or_404(self.model, pk=pk) def get_queryset(self): instance = self.get_object() @@ -68,7 +70,7 @@ class AutomationAddAssetApi(generics.RetrieveUpdateAPIView): class AutomationNodeAddRemoveApi(generics.RetrieveUpdateAPIView): model = BaseAutomation - serializer_class = serializers.UpdateAssetSerializer + serializer_class = serializers.UpdateNodeSerializer def update(self, request, *args, **kwargs): action_params = ['add', 'remove'] @@ -97,21 +99,17 @@ class AutomationExecutionViewSet( filterset_fields = ('trigger', 'automation_id') serializer_class = serializers.AutomationExecutionSerializer + tp: str + def get_queryset(self): queryset = AutomationExecution.objects.all() return queryset - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - queryset = queryset.order_by('-date_start') - return queryset - def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) automation = serializer.validated_data.get('automation') - tp = serializer.validated_data.get('type') task = execute_automation.delay( - pid=automation.pk, trigger=Trigger.manual, tp=tp + pid=automation.pk, trigger=Trigger.manual, tp=self.tp ) return Response({'task': task.id}, status=status.HTTP_201_CREATED) diff --git a/apps/accounts/api/automations/change_secret.py b/apps/accounts/api/automations/change_secret.py new file mode 100644 index 000000000..b6034c79d --- /dev/null +++ b/apps/accounts/api/automations/change_secret.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import mixins + +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution +from common.utils import get_object_or_none +from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet +from .base import ( + AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, + AutomationNodeAddRemoveApi, AutomationExecutionViewSet +) + +__all__ = [ + 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', + 'ChangSecretExecutionViewSet', 'ChangSecretAssetsListApi', + 'ChangSecretRemoveAssetApi', 'ChangSecretAddAssetApi', + 'ChangSecretNodeAddRemoveApi' +] + + +class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): + model = ChangeSecretAutomation + filter_fields = ('name', 'secret_type', 'secret_strategy') + search_fields = filter_fields + ordering_fields = ('name',) + serializer_class = serializers.ChangeSecretAutomationSerializer + + +class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): + serializer_class = serializers.ChangeSecretRecordSerializer + filter_fields = ['asset', 'execution_id'] + search_fields = ['asset__hostname'] + + def get_queryset(self): + return ChangeSecretRecord.objects.filter( + execution__automation__type=AutomationTypes.change_secret + ) + + def filter_queryset(self, queryset): + queryset = super().filter_queryset(queryset) + eid = self.request.query_params.get('execution_id') + execution = get_object_or_none(AutomationExecution, pk=eid) + if execution: + queryset = queryset.filter(execution=execution) + return queryset + + +class ChangSecretExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "accounts.view_changesecretexecution"), + ("retrieve", "accounts.view_changesecretexecution"), + ("create", "accounts.add_changesecretexecution"), + ) + + tp = AutomationTypes.change_secret + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset + + +class ChangSecretAssetsListApi(AutomationAssetsListApi): + model = ChangeSecretAutomation + + +class ChangSecretRemoveAssetApi(AutomationRemoveAssetApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateAssetSerializer + + +class ChangSecretAddAssetApi(AutomationAddAssetApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateAssetSerializer + + +class ChangSecretNodeAddRemoveApi(AutomationNodeAddRemoveApi): + model = ChangeSecretAutomation + serializer_class = serializers.ChangeSecretUpdateNodeSerializer diff --git a/apps/assets/api/automations/gather_accounts.py b/apps/accounts/api/automations/gather_accounts.py similarity index 52% rename from apps/assets/api/automations/gather_accounts.py rename to apps/accounts/api/automations/gather_accounts.py index ba2d13df0..8fbd2dc92 100644 --- a/apps/assets/api/automations/gather_accounts.py +++ b/apps/accounts/api/automations/gather_accounts.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- # -from assets import serializers -from assets.models import GatherAccountsAutomation +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import GatherAccountsAutomation from orgs.mixins.api import OrgBulkModelViewSet from .base import AutomationExecutionViewSet @@ -20,7 +21,14 @@ class GatherAccountsAutomationViewSet(OrgBulkModelViewSet): class GatherAccountsExecutionViewSet(AutomationExecutionViewSet): rbac_perms = ( - ("list", "assets.view_gatheraccountsexecution"), - ("retrieve", "assets.view_gatheraccountsexecution"), - ("create", "assets.add_gatheraccountsexecution"), + ("list", "accounts.view_gatheraccountsexecution"), + ("retrieve", "accounts.view_gatheraccountsexecution"), + ("create", "accounts.add_gatheraccountsexecution"), ) + + tp = AutomationTypes.gather_accounts + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset diff --git a/apps/accounts/api/automations/push_account.py b/apps/accounts/api/automations/push_account.py new file mode 100644 index 000000000..a736daaf6 --- /dev/null +++ b/apps/accounts/api/automations/push_account.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +from accounts import serializers +from accounts.const import AutomationTypes +from accounts.models import PushAccountAutomation, ChangeSecretRecord +from orgs.mixins.api import OrgBulkModelViewSet + +from .base import ( + AutomationAssetsListApi, AutomationRemoveAssetApi, AutomationAddAssetApi, + AutomationNodeAddRemoveApi, AutomationExecutionViewSet +) +from .change_secret import ChangeSecretRecordViewSet + +__all__ = [ + 'PushAccountAutomationViewSet', 'PushAccountAssetsListApi', 'PushAccountRemoveAssetApi', + 'PushAccountAddAssetApi', 'PushAccountNodeAddRemoveApi', 'PushAccountExecutionViewSet', + 'PushAccountRecordViewSet' +] + + +class PushAccountAutomationViewSet(OrgBulkModelViewSet): + model = PushAccountAutomation + filter_fields = ('name', 'secret_type', 'secret_strategy') + search_fields = filter_fields + ordering_fields = ('name',) + serializer_class = serializers.PushAccountAutomationSerializer + + +class PushAccountExecutionViewSet(AutomationExecutionViewSet): + rbac_perms = ( + ("list", "accounts.view_pushaccountexecution"), + ("retrieve", "accounts.view_pushaccountexecution"), + ("create", "accounts.add_pushaccountexecution"), + ) + + tp = AutomationTypes.push_account + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(automation__type=self.tp) + return queryset + + +class PushAccountRecordViewSet(ChangeSecretRecordViewSet): + serializer_class = serializers.ChangeSecretRecordSerializer + + def get_queryset(self): + return ChangeSecretRecord.objects.filter( + execution__automation__type=AutomationTypes.push_account + ) + + +class PushAccountAssetsListApi(AutomationAssetsListApi): + model = PushAccountAutomation + + +class PushAccountRemoveAssetApi(AutomationRemoveAssetApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateAssetSerializer + + +class PushAccountAddAssetApi(AutomationAddAssetApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateAssetSerializer + + +class PushAccountNodeAddRemoveApi(AutomationNodeAddRemoveApi): + model = PushAccountAutomation + serializer_class = serializers.PushAccountUpdateNodeSerializer diff --git a/apps/accounts/apps.py b/apps/accounts/apps.py new file mode 100644 index 000000000..6fd8c2d9b --- /dev/null +++ b/apps/accounts/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' + + def ready(self): + from . import signal_handlers + __all__ = signal_handlers diff --git a/apps/accounts/automations/__init__.py b/apps/accounts/automations/__init__.py new file mode 100644 index 000000000..6f0f12d03 --- /dev/null +++ b/apps/accounts/automations/__init__.py @@ -0,0 +1,2 @@ +from .endpoint import ExecutionManager +from .methods import platform_automation_methods diff --git a/apps/assets/automations/change_secret/__init__.py b/apps/accounts/automations/backup_account/__init__.py similarity index 100% rename from apps/assets/automations/change_secret/__init__.py rename to apps/accounts/automations/backup_account/__init__.py diff --git a/apps/assets/automations/backup_account/handlers.py b/apps/accounts/automations/backup_account/handlers.py similarity index 97% rename from apps/assets/automations/backup_account/handlers.py rename to apps/accounts/automations/backup_account/handlers.py index 5c4ffce76..743a38c84 100644 --- a/apps/assets/automations/backup_account/handlers.py +++ b/apps/accounts/automations/backup_account/handlers.py @@ -7,10 +7,10 @@ from django.conf import settings from django.db.models import F from rest_framework import serializers -from assets.models import Account +from accounts.models import Account from assets.const import AllTypes -from assets.serializers import AccountSecretSerializer -from assets.notifications import AccountBackupExecutionTaskMsg +from accounts.serializers import AccountSecretSerializer +from accounts.notifications import AccountBackupExecutionTaskMsg from users.models import User from common.utils import get_logger from common.utils.timezone import local_now_display diff --git a/apps/assets/automations/backup_account/manager.py b/apps/accounts/automations/backup_account/manager.py similarity index 100% rename from apps/assets/automations/backup_account/manager.py rename to apps/accounts/automations/backup_account/manager.py diff --git a/apps/assets/automations/gather_accounts/__init__.py b/apps/accounts/automations/base/__init__.py similarity index 100% rename from apps/assets/automations/gather_accounts/__init__.py rename to apps/accounts/automations/base/__init__.py diff --git a/apps/accounts/automations/base/base_inventory.txt b/apps/accounts/automations/base/base_inventory.txt new file mode 100644 index 000000000..a2b73db16 --- /dev/null +++ b/apps/accounts/automations/base/base_inventory.txt @@ -0,0 +1,14 @@ +## all connection vars +hostname asset_name=name asset_type=type asset_primary_protocol=ssh asset_primary_port=22 asset_protocols=[] + +## local connection +hostname ansible_connection=local + +## local connection with gateway +hostname ansible_connection=ssh ansible_user=gateway.username ansible_port=gateway.port ansible_host=gateway.host ansible_ssh_private_key_file=gateway.key + +## ssh connection for windows +hostname ansible_connection=ssh ansible_shell_type=powershell/cmd ansible_user=windows.username ansible_port=windows.port ansible_host=windows.host ansible_ssh_private_key_file=windows.key + +## ssh connection +hostname ansible_user=user ansible_password=pass ansible_host=host ansible_port=port ansible_ssh_private_key_file=key ssh_args="-o StrictHostKeyChecking=no" diff --git a/apps/accounts/automations/base/manager.py b/apps/accounts/automations/base/manager.py new file mode 100644 index 000000000..f39a1847e --- /dev/null +++ b/apps/accounts/automations/base/manager.py @@ -0,0 +1,55 @@ +from copy import deepcopy + +from common.utils import get_logger +from accounts.const import AutomationTypes, SecretType +from assets.automations.base.manager import BasePlaybookManager +from accounts.automations.methods import platform_automation_methods + +logger = get_logger(__name__) + + +class PushOrVerifyHostCallbackMixin: + execution: callable + get_accounts: callable + host_account_mapper: dict + generate_public_key: callable + generate_private_key_path: callable + + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): + host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) + if host.get('error'): + return host + + accounts = asset.accounts.all() + accounts = self.get_accounts(account, accounts) + + inventory_hosts = [] + for account in accounts: + h = deepcopy(host) + h['name'] += '_' + account.username + self.host_account_mapper[h['name']] = account + secret = account.secret + + private_key_path = None + if account.secret_type == SecretType.SSH_KEY: + private_key_path = self.generate_private_key_path(secret, path_dir) + secret = self.generate_public_key(secret) + + h['secret_type'] = account.secret_type + h['account'] = { + 'name': account.name, + 'username': account.username, + 'secret_type': account.secret_type, + 'secret': secret, + 'private_key_path': private_key_path + } + inventory_hosts.append(h) + return inventory_hosts + + +class AccountBasePlaybookManager(BasePlaybookManager): + pass + + @property + def platform_automation_methods(self): + return platform_automation_methods diff --git a/apps/assets/automations/push_account/__init__.py b/apps/accounts/automations/change_secret/__init__.py similarity index 100% rename from apps/assets/automations/push_account/__init__.py rename to apps/accounts/automations/change_secret/__init__.py diff --git a/apps/assets/automations/change_secret/database/mongodb/main.yml b/apps/accounts/automations/change_secret/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/mongodb/main.yml rename to apps/accounts/automations/change_secret/database/mongodb/main.yml diff --git a/apps/assets/automations/change_secret/database/mongodb/manifest.yml b/apps/accounts/automations/change_secret/database/mongodb/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/mongodb/manifest.yml rename to apps/accounts/automations/change_secret/database/mongodb/manifest.yml index a59c0033b..b6eeaa139 100644 --- a/apps/assets/automations/change_secret/database/mongodb/manifest.yml +++ b/apps/accounts/automations/change_secret/database/mongodb/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_mongodb -name: Change password for MongoDB +name: Change secret for MongoDB category: database type: - mongodb diff --git a/apps/assets/automations/change_secret/database/mysql/main.yml b/apps/accounts/automations/change_secret/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/mysql/main.yml rename to apps/accounts/automations/change_secret/database/mysql/main.yml diff --git a/apps/assets/automations/change_secret/database/mysql/manifest.yml b/apps/accounts/automations/change_secret/database/mysql/manifest.yml similarity index 65% rename from apps/assets/automations/change_secret/database/mysql/manifest.yml rename to apps/accounts/automations/change_secret/database/mysql/manifest.yml index ca2aefc01..eac0fb56b 100644 --- a/apps/assets/automations/change_secret/database/mysql/manifest.yml +++ b/apps/accounts/automations/change_secret/database/mysql/manifest.yml @@ -1,6 +1,7 @@ id: change_secret_mysql -name: Change password for MySQL +name: Change secret for MySQL category: database type: - mysql + - mariadb method: change_secret diff --git a/apps/assets/automations/change_secret/database/oracle/main.yml b/apps/accounts/automations/change_secret/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/oracle/main.yml rename to apps/accounts/automations/change_secret/database/oracle/main.yml diff --git a/apps/assets/automations/change_secret/database/oracle/manifest.yml b/apps/accounts/automations/change_secret/database/oracle/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/oracle/manifest.yml rename to apps/accounts/automations/change_secret/database/oracle/manifest.yml index 19f109ba6..ee5cee177 100644 --- a/apps/assets/automations/change_secret/database/oracle/manifest.yml +++ b/apps/accounts/automations/change_secret/database/oracle/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_oracle -name: Change password for Oracle +name: Change secret for Oracle category: database type: - oracle diff --git a/apps/assets/automations/change_secret/database/postgresql/main.yml b/apps/accounts/automations/change_secret/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/postgresql/main.yml rename to apps/accounts/automations/change_secret/database/postgresql/main.yml diff --git a/apps/assets/automations/change_secret/database/postgresql/manifest.yml b/apps/accounts/automations/change_secret/database/postgresql/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/postgresql/manifest.yml rename to apps/accounts/automations/change_secret/database/postgresql/manifest.yml index 48238f5ec..048637c7f 100644 --- a/apps/assets/automations/change_secret/database/postgresql/manifest.yml +++ b/apps/accounts/automations/change_secret/database/postgresql/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_postgresql -name: Change password for PostgreSQL +name: Change secret for PostgreSQL category: database type: - postgresql diff --git a/apps/assets/automations/change_secret/database/sqlserver/main.yml b/apps/accounts/automations/change_secret/database/sqlserver/main.yml similarity index 100% rename from apps/assets/automations/change_secret/database/sqlserver/main.yml rename to apps/accounts/automations/change_secret/database/sqlserver/main.yml diff --git a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml b/apps/accounts/automations/change_secret/database/sqlserver/manifest.yml similarity index 71% rename from apps/assets/automations/change_secret/database/sqlserver/manifest.yml rename to apps/accounts/automations/change_secret/database/sqlserver/manifest.yml index 799c9e623..2a436e27f 100644 --- a/apps/assets/automations/change_secret/database/sqlserver/manifest.yml +++ b/apps/accounts/automations/change_secret/database/sqlserver/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_sqlserver -name: Change password for SQLServer +name: Change secret for SQLServer category: database type: - sqlserver diff --git a/apps/assets/automations/change_secret/demo_inventory.txt b/apps/accounts/automations/change_secret/demo_inventory.txt similarity index 100% rename from apps/assets/automations/change_secret/demo_inventory.txt rename to apps/accounts/automations/change_secret/demo_inventory.txt diff --git a/apps/assets/automations/change_secret/host/posix/main.yml b/apps/accounts/automations/change_secret/host/posix/main.yml similarity index 100% rename from apps/assets/automations/change_secret/host/posix/main.yml rename to apps/accounts/automations/change_secret/host/posix/main.yml diff --git a/apps/assets/automations/change_secret/host/posix/manifest.yml b/apps/accounts/automations/change_secret/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/change_secret/host/posix/manifest.yml rename to apps/accounts/automations/change_secret/host/posix/manifest.yml diff --git a/apps/assets/automations/change_secret/host/windows/main.yml b/apps/accounts/automations/change_secret/host/windows/main.yml similarity index 100% rename from apps/assets/automations/change_secret/host/windows/main.yml rename to apps/accounts/automations/change_secret/host/windows/main.yml diff --git a/apps/assets/automations/change_secret/host/windows/manifest.yml b/apps/accounts/automations/change_secret/host/windows/manifest.yml similarity index 67% rename from apps/assets/automations/change_secret/host/windows/manifest.yml rename to apps/accounts/automations/change_secret/host/windows/manifest.yml index 80d0fb782..d727e3bec 100644 --- a/apps/assets/automations/change_secret/host/windows/manifest.yml +++ b/apps/accounts/automations/change_secret/host/windows/manifest.yml @@ -1,5 +1,5 @@ id: change_secret_local_windows -name: Change password local account for Windows +name: Change secret local account for Windows version: 1 method: change_secret category: host diff --git a/apps/assets/automations/change_secret/manager.py b/apps/accounts/automations/change_secret/manager.py similarity index 66% rename from apps/assets/automations/change_secret/manager.py rename to apps/accounts/automations/change_secret/manager.py index 30606956d..4360a9311 100644 --- a/apps/assets/automations/change_secret/manager.py +++ b/apps/accounts/automations/change_secret/manager.py @@ -1,35 +1,38 @@ import os import time -import random -import string from copy import deepcopy from openpyxl import Workbook from collections import defaultdict -from django.utils import timezone from django.conf import settings +from django.utils import timezone -from common.utils.timezone import local_now_display -from common.utils.file import encrypt_and_compress_zip_file -from common.utils import get_logger, lazyproperty, gen_key_pair from users.models import User -from assets.models import ChangeSecretRecord -from assets.notifications import ChangeSecretExecutionTaskMsg -from assets.serializers import ChangeSecretRecordBackUpSerializer -from assets.const import ( - AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy, DEFAULT_PASSWORD_RULES -) -from ..base.manager import BasePlaybookManager +from accounts.models import ChangeSecretRecord +from accounts.notifications import ChangeSecretExecutionTaskMsg +from accounts.serializers import ChangeSecretRecordBackUpSerializer +from accounts.const import AutomationTypes, SecretType, SSHKeyStrategy, SecretStrategy +from common.utils import get_logger, lazyproperty +from common.utils.file import encrypt_and_compress_zip_file +from common.utils.timezone import local_now_display +from ...utils import SecretGenerator +from ..base.manager import AccountBasePlaybookManager logger = get_logger(__name__) -class ChangeSecretManager(BasePlaybookManager): +class ChangeSecretManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.method_hosts_mapper = defaultdict(list) self.secret_type = self.execution.snapshot['secret_type'] - self.secret_strategy = self.execution.snapshot['secret_strategy'] + self.secret_strategy = self.execution.snapshot.get( + 'secret_strategy', SecretStrategy.custom + ) + self.ssh_key_change_strategy = self.execution.snapshot.get( + 'ssh_key_change_strategy', SSHKeyStrategy.add + ) + self.snapshot_account_usernames = self.execution.snapshot['accounts'] self._password_generated = None self._ssh_key_generated = None self.name_recorder_mapper = {} # 做个映射,方便后面处理 @@ -42,74 +45,31 @@ class ChangeSecretManager(BasePlaybookManager): def related_accounts(self): pass - @staticmethod - def generate_ssh_key(): - private_key, public_key = gen_key_pair() - return private_key - - def generate_password(self): - kwargs = self.execution.snapshot['password_rules'] or {} - length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) - symbol_set = kwargs.get('symbol_set') - if symbol_set is None: - symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] - - no_special_chars = string.ascii_letters + string.digits - chars = no_special_chars + symbol_set - - first_char = random.choice(no_special_chars) - password = ''.join([random.choice(chars) for _ in range(length - 1)]) - password = first_char + password - return password - - def get_ssh_key(self): - if self.secret_strategy == SecretStrategy.custom: - secret = self.execution.snapshot['secret'] - if not secret: - raise ValueError("Automation SSH key must be set") - return secret - elif self.secret_strategy == SecretStrategy.random_one: - if not self._ssh_key_generated: - self._ssh_key_generated = self.generate_ssh_key() - return self._ssh_key_generated - else: - return self.generate_ssh_key() - - def get_password(self): - if self.secret_strategy == SecretStrategy.custom: - password = self.execution.snapshot['secret'] - if not password: - raise ValueError("Automation Password must be set") - return password - elif self.secret_strategy == SecretStrategy.random_one: - if not self._password_generated: - self._password_generated = self.generate_password() - return self._password_generated - else: - return self.generate_password() - - def get_secret(self): - if self.secret_type == SecretType.SSH_KEY: - secret = self.get_ssh_key() - elif self.secret_type == SecretType.PASSWORD: - secret = self.get_password() - else: - raise ValueError("Secret must be set") - return secret - def get_kwargs(self, account, secret): kwargs = {} if self.secret_type != SecretType.SSH_KEY: return kwargs - kwargs['strategy'] = self.execution.snapshot['ssh_key_change_strategy'] + kwargs['strategy'] = self.ssh_key_change_strategy kwargs['exclusive'] = 'yes' if kwargs['strategy'] == SSHKeyStrategy.set else 'no' if kwargs['strategy'] == SSHKeyStrategy.set_jms: kwargs['dest'] = '/home/{}/.ssh/authorized_keys'.format(account.username) kwargs['regexp'] = '.*{}$'.format(secret.split()[2].strip()) - return kwargs + @lazyproperty + def secret_generator(self): + return SecretGenerator( + self.secret_strategy, self.secret_type, + self.execution.snapshot.get('password_rules') + ) + + def get_secret(self): + if self.secret_strategy == SecretStrategy.custom: + return self.execution.snapshot['secret'] + else: + return self.secret_generator.get_secret() + def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) if host.get('error'): @@ -117,10 +77,10 @@ class ChangeSecretManager(BasePlaybookManager): accounts = asset.accounts.all() if account: - accounts = accounts.exclude(id=account.id) + accounts = accounts.exclude(username=account.username) - if '*' not in self.execution.snapshot['accounts']: - accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) + if '*' not in self.snapshot_account_usernames: + accounts = accounts.filter(username__in=self.snapshot_account_usernames) accounts = accounts.filter(secret_type=self.secret_type) method_attr = getattr(automation, self.method_type() + '_method') @@ -128,7 +88,6 @@ class ChangeSecretManager(BasePlaybookManager): method_hosts = [h for h in method_hosts if h != host['name']] inventory_hosts = [] records = [] - host['secret_type'] = self.secret_type for account in accounts: h = deepcopy(host) @@ -197,7 +156,8 @@ class ChangeSecretManager(BasePlaybookManager): recipients = self.execution.recipients if not recorders or not recipients: return - recipients = User.objects.filter(id__in=list(recipients)) + + recipients = User.objects.filter(id__in=list(recipients.keys())) name = self.execution.snapshot['name'] path = os.path.join(os.path.dirname(settings.BASE_DIR), 'tmp') @@ -219,7 +179,8 @@ class ChangeSecretManager(BasePlaybookManager): def create_file(recorders, filename): serializer_cls = ChangeSecretRecordBackUpSerializer serializer = serializer_cls(recorders, many=True) - header = [v.label for v in serializer.child.fields.values()] + + header = [str(v.label) for v in serializer.child.fields.values()] rows = [list(row.values()) for row in serializer.data] if not rows: return False diff --git a/apps/accounts/automations/endpoint.py b/apps/accounts/automations/endpoint.py new file mode 100644 index 000000000..2a1a821ea --- /dev/null +++ b/apps/accounts/automations/endpoint.py @@ -0,0 +1,24 @@ +from .change_secret.manager import ChangeSecretManager +from .gather_accounts.manager import GatherAccountsManager +from .verify_account.manager import VerifyAccountManager +from .push_account.manager import PushAccountManager +from .backup_account.manager import AccountBackupManager +from ..const import AutomationTypes + + +class ExecutionManager: + manager_type_mapper = { + AutomationTypes.push_account: PushAccountManager, + AutomationTypes.change_secret: ChangeSecretManager, + AutomationTypes.verify_account: VerifyAccountManager, + AutomationTypes.gather_accounts: GatherAccountsManager, + # TODO 后期迁移到自动化策略中 + 'backup_account': AccountBackupManager, + } + + def __init__(self, execution): + self.execution = execution + self._runner = self.manager_type_mapper[execution.manager_type](execution) + + def run(self, *args, **kwargs): + return self._runner.run(*args, **kwargs) diff --git a/apps/assets/automations/verify_account/__init__.py b/apps/accounts/automations/gather_accounts/__init__.py similarity index 100% rename from apps/assets/automations/verify_account/__init__.py rename to apps/accounts/automations/gather_accounts/__init__.py diff --git a/apps/assets/automations/gather_accounts/database/mongodb/main.yml b/apps/accounts/automations/gather_accounts/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mongodb/main.yml rename to apps/accounts/automations/gather_accounts/database/mongodb/main.yml diff --git a/apps/assets/automations/gather_accounts/database/mongodb/manifest.yml b/apps/accounts/automations/gather_accounts/database/mongodb/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mongodb/manifest.yml rename to apps/accounts/automations/gather_accounts/database/mongodb/manifest.yml diff --git a/apps/assets/automations/gather_accounts/database/mysql/main.yml b/apps/accounts/automations/gather_accounts/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/mysql/main.yml rename to apps/accounts/automations/gather_accounts/database/mysql/main.yml diff --git a/apps/assets/automations/gather_accounts/database/mysql/manifest.yml b/apps/accounts/automations/gather_accounts/database/mysql/manifest.yml similarity index 90% rename from apps/assets/automations/gather_accounts/database/mysql/manifest.yml rename to apps/accounts/automations/gather_accounts/database/mysql/manifest.yml index e69cca67b..be104b783 100644 --- a/apps/assets/automations/gather_accounts/database/mysql/manifest.yml +++ b/apps/accounts/automations/gather_accounts/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Gather account from MySQL category: database type: - mysql + - mariadb method: gather_accounts diff --git a/apps/assets/automations/gather_accounts/database/oracle/main.yml b/apps/accounts/automations/gather_accounts/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/oracle/main.yml rename to apps/accounts/automations/gather_accounts/database/oracle/main.yml diff --git a/apps/assets/automations/gather_accounts/database/oracle/manifest.yml b/apps/accounts/automations/gather_accounts/database/oracle/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/oracle/manifest.yml rename to apps/accounts/automations/gather_accounts/database/oracle/manifest.yml diff --git a/apps/assets/automations/gather_accounts/database/postgresql/main.yml b/apps/accounts/automations/gather_accounts/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/postgresql/main.yml rename to apps/accounts/automations/gather_accounts/database/postgresql/main.yml diff --git a/apps/assets/automations/gather_accounts/database/postgresql/manifest.yml b/apps/accounts/automations/gather_accounts/database/postgresql/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/database/postgresql/manifest.yml rename to apps/accounts/automations/gather_accounts/database/postgresql/manifest.yml diff --git a/apps/assets/automations/gather_accounts/filter.py b/apps/accounts/automations/gather_accounts/filter.py similarity index 100% rename from apps/assets/automations/gather_accounts/filter.py rename to apps/accounts/automations/gather_accounts/filter.py diff --git a/apps/assets/automations/gather_accounts/host/posix/main.yml b/apps/accounts/automations/gather_accounts/host/posix/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/posix/main.yml rename to apps/accounts/automations/gather_accounts/host/posix/main.yml diff --git a/apps/assets/automations/gather_accounts/host/posix/manifest.yml b/apps/accounts/automations/gather_accounts/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/posix/manifest.yml rename to apps/accounts/automations/gather_accounts/host/posix/manifest.yml diff --git a/apps/assets/automations/gather_accounts/host/windows/main.yml b/apps/accounts/automations/gather_accounts/host/windows/main.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/windows/main.yml rename to apps/accounts/automations/gather_accounts/host/windows/main.yml diff --git a/apps/assets/automations/gather_accounts/host/windows/manifest.yml b/apps/accounts/automations/gather_accounts/host/windows/manifest.yml similarity index 100% rename from apps/assets/automations/gather_accounts/host/windows/manifest.yml rename to apps/accounts/automations/gather_accounts/host/windows/manifest.yml diff --git a/apps/assets/automations/gather_accounts/manager.py b/apps/accounts/automations/gather_accounts/manager.py similarity index 93% rename from apps/assets/automations/gather_accounts/manager.py rename to apps/accounts/automations/gather_accounts/manager.py index da1b44abe..efbe1a965 100644 --- a/apps/assets/automations/gather_accounts/manager.py +++ b/apps/accounts/automations/gather_accounts/manager.py @@ -1,15 +1,15 @@ from django.utils.translation import ugettext_lazy as _ from common.utils import get_logger -from assets.const import AutomationTypes, Source +from accounts.const import AutomationTypes, Source from orgs.utils import tmp_to_org from .filter import GatherAccountsFilter -from ..base.manager import BasePlaybookManager +from ..base.manager import AccountBasePlaybookManager logger = get_logger(__name__) -class GatherAccountsManager(BasePlaybookManager): +class GatherAccountsManager(AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.host_asset_mapper = {} diff --git a/apps/accounts/automations/methods.py b/apps/accounts/automations/methods.py new file mode 100644 index 000000000..be5890701 --- /dev/null +++ b/apps/accounts/automations/methods.py @@ -0,0 +1,30 @@ +import os +import copy + +from accounts.const import AutomationTypes +from assets.automations.methods import get_platform_automation_methods + + +def copy_change_secret_to_push_account(methods): + push_account = AutomationTypes.push_account + change_secret = AutomationTypes.change_secret + copy_methods = copy.deepcopy(methods) + for method in copy_methods: + if not method['id'].startswith(change_secret): + continue + copy_method = copy.deepcopy(method) + copy_method['method'] = push_account.value + copy_method['id'] = copy_method['id'].replace( + change_secret, push_account + ) + copy_method['name'] = copy_method['name'].replace( + 'Change secret', 'Push account' + ) + methods.append(copy_method) + return methods + + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +automation_methods = get_platform_automation_methods(BASE_DIR) + +platform_automation_methods = copy_change_secret_to_push_account(automation_methods) diff --git a/apps/assets/backends/db.py b/apps/accounts/automations/push_account/__init__.py similarity index 100% rename from apps/assets/backends/db.py rename to apps/accounts/automations/push_account/__init__.py diff --git a/apps/accounts/automations/push_account/manager.py b/apps/accounts/automations/push_account/manager.py new file mode 100644 index 000000000..896304db1 --- /dev/null +++ b/apps/accounts/automations/push_account/manager.py @@ -0,0 +1,112 @@ +from django.db.models import QuerySet + +from common.utils import get_logger +from accounts.const import AutomationTypes +from accounts.models import Account +from ..base.manager import PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager + +logger = get_logger(__name__) + + +class PushAccountManager(PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.secret_type = self.execution.snapshot['secret_type'] + self.host_account_mapper = {} + + @classmethod + def method_type(cls): + return AutomationTypes.push_account + + def create_nonlocal_accounts(self, accounts, snapshot_account_usernames, asset): + secret = self.execution.snapshot['secret'] + usernames = accounts.filter(secret_type=self.secret_type).values_list( + 'username', flat=True + ) + create_usernames = set(snapshot_account_usernames) - set(usernames) + create_account_objs = [ + Account( + name=username, username=username, secret=secret, + secret_type=self.secret_type, asset=asset, + ) + for username in create_usernames + ] + Account.objects.bulk_create(create_account_objs) + + def get_accounts(self, privilege_account, accounts: QuerySet): + if not privilege_account: + logger.debug(f'not privilege account') + return [] + snapshot_account_usernames = self.execution.snapshot['accounts'] + accounts = accounts.exclude(username=privilege_account.username) + if '*' in snapshot_account_usernames: + return accounts + + asset = privilege_account.asset + self.create_nonlocal_accounts(accounts, snapshot_account_usernames, asset) + accounts = asset.accounts.exclude(username=privilege_account.username).filter( + username__in=snapshot_account_usernames, secret_type=self.secret_type + ) + return accounts + + # @classmethod + # def trigger_by_asset_create(cls, asset): + # automations = PushAccountAutomation.objects.filter( + # triggers__contains=TriggerChoice.on_asset_create + # ) + # account_automation_map = {auto.username: auto for auto in automations} + # + # util = AssetPermissionUtil() + # permissions = util.get_permissions_for_assets([asset], with_node=True) + # account_permission_map = defaultdict(list) + # for permission in permissions: + # for account in permission.accounts: + # account_permission_map[account].append(permission) + # + # username_automation_map = {} + # for username, automation in account_automation_map.items(): + # if username != '@USER': + # username_automation_map[username] = automation + # continue + # + # asset_permissions = account_permission_map.get(username) + # if not asset_permissions: + # continue + # asset_permissions = util.get_permissions([p.id for p in asset_permissions]) + # usernames = asset_permissions.values_list('users__username', flat=True).distinct() + # for _username in usernames: + # username_automation_map[_username] = automation + # + # asset_usernames_exists = asset.accounts.values_list('username', flat=True) + # accounts_to_create = [] + # accounts_to_push = [] + # for username, automation in username_automation_map.items(): + # if username in asset_usernames_exists: + # continue + # + # if automation.secret_strategy != SecretStrategy.custom: + # secret_generator = SecretGenerator( + # automation.secret_strategy, automation.secret_type, + # automation.password_rules + # ) + # secret = secret_generator.get_secret() + # else: + # secret = automation.secret + # + # account = Account( + # username=username, secret=secret, + # asset=asset, secret_type=automation.secret_type, + # comment='Create by account creation {}'.format(automation.name), + # ) + # accounts_to_create.append(account) + # if automation.action == 'create_and_push': + # accounts_to_push.append(account) + # else: + # accounts_to_create.append(account) + # + # logger.debug(f'Create account {account} for asset {asset}') + + # @classmethod + # def trigger_by_permission_accounts_change(cls): + # pass diff --git a/apps/assets/serializers/account.py b/apps/accounts/automations/verify_account/__init__.py similarity index 100% rename from apps/assets/serializers/account.py rename to apps/accounts/automations/verify_account/__init__.py diff --git a/apps/assets/automations/verify_account/database/mongodb/main.yml b/apps/accounts/automations/verify_account/database/mongodb/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mongodb/main.yml rename to apps/accounts/automations/verify_account/database/mongodb/main.yml diff --git a/apps/assets/automations/verify_account/database/mongodb/manifest.yml b/apps/accounts/automations/verify_account/database/mongodb/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mongodb/manifest.yml rename to apps/accounts/automations/verify_account/database/mongodb/manifest.yml diff --git a/apps/assets/automations/verify_account/database/mysql/main.yml b/apps/accounts/automations/verify_account/database/mysql/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mysql/main.yml rename to apps/accounts/automations/verify_account/database/mysql/main.yml diff --git a/apps/assets/automations/verify_account/database/mysql/manifest.yml b/apps/accounts/automations/verify_account/database/mysql/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/mysql/manifest.yml rename to apps/accounts/automations/verify_account/database/mysql/manifest.yml diff --git a/apps/assets/automations/verify_account/database/oracle/main.yml b/apps/accounts/automations/verify_account/database/oracle/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/oracle/main.yml rename to apps/accounts/automations/verify_account/database/oracle/main.yml diff --git a/apps/assets/automations/verify_account/database/oracle/manifest.yml b/apps/accounts/automations/verify_account/database/oracle/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/oracle/manifest.yml rename to apps/accounts/automations/verify_account/database/oracle/manifest.yml diff --git a/apps/assets/automations/verify_account/database/postgresql/main.yml b/apps/accounts/automations/verify_account/database/postgresql/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/postgresql/main.yml rename to apps/accounts/automations/verify_account/database/postgresql/main.yml diff --git a/apps/assets/automations/verify_account/database/postgresql/manifest.yml b/apps/accounts/automations/verify_account/database/postgresql/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/postgresql/manifest.yml rename to apps/accounts/automations/verify_account/database/postgresql/manifest.yml diff --git a/apps/assets/automations/verify_account/database/sqlserver/main.yml b/apps/accounts/automations/verify_account/database/sqlserver/main.yml similarity index 100% rename from apps/assets/automations/verify_account/database/sqlserver/main.yml rename to apps/accounts/automations/verify_account/database/sqlserver/main.yml diff --git a/apps/assets/automations/verify_account/database/sqlserver/manifest.yml b/apps/accounts/automations/verify_account/database/sqlserver/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/database/sqlserver/manifest.yml rename to apps/accounts/automations/verify_account/database/sqlserver/manifest.yml diff --git a/apps/assets/automations/verify_account/host/posix/main.yml b/apps/accounts/automations/verify_account/host/posix/main.yml similarity index 100% rename from apps/assets/automations/verify_account/host/posix/main.yml rename to apps/accounts/automations/verify_account/host/posix/main.yml diff --git a/apps/assets/automations/verify_account/host/posix/manifest.yml b/apps/accounts/automations/verify_account/host/posix/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/host/posix/manifest.yml rename to apps/accounts/automations/verify_account/host/posix/manifest.yml diff --git a/apps/assets/automations/verify_account/host/windows/main.yml b/apps/accounts/automations/verify_account/host/windows/main.yml similarity index 100% rename from apps/assets/automations/verify_account/host/windows/main.yml rename to apps/accounts/automations/verify_account/host/windows/main.yml diff --git a/apps/assets/automations/verify_account/host/windows/manifest.yml b/apps/accounts/automations/verify_account/host/windows/manifest.yml similarity index 100% rename from apps/assets/automations/verify_account/host/windows/manifest.yml rename to apps/accounts/automations/verify_account/host/windows/manifest.yml diff --git a/apps/assets/automations/verify_account/manager.py b/apps/accounts/automations/verify_account/manager.py similarity index 51% rename from apps/assets/automations/verify_account/manager.py rename to apps/accounts/automations/verify_account/manager.py index f261631e5..12c247849 100644 --- a/apps/assets/automations/verify_account/manager.py +++ b/apps/accounts/automations/verify_account/manager.py @@ -1,12 +1,13 @@ +from django.db.models import QuerySet + from common.utils import get_logger -from assets.const import AutomationTypes, Connectivity -from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin +from accounts.const import AutomationTypes, Connectivity +from ..base.manager import PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager logger = get_logger(__name__) -class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): - need_privilege_account = False +class VerifyAccountManager(PushOrVerifyHostCallbackMixin, AccountBasePlaybookManager): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -16,6 +17,12 @@ class VerifyAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): def method_type(cls): return AutomationTypes.verify_account + def get_accounts(self, privilege_account, accounts: QuerySet): + snapshot_account_usernames = self.execution.snapshot['accounts'] + if '*' not in snapshot_account_usernames: + accounts = accounts.filter(username__in=snapshot_account_usernames) + return accounts + def on_host_success(self, host, result): account = self.host_account_mapper.get(host) account.set_connectivity(Connectivity.OK) diff --git a/apps/accounts/const/__init__.py b/apps/accounts/const/__init__.py new file mode 100644 index 000000000..6db502556 --- /dev/null +++ b/apps/accounts/const/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automation import * diff --git a/apps/assets/const/account.py b/apps/accounts/const/account.py similarity index 80% rename from apps/assets/const/account.py rename to apps/accounts/const/account.py index 4c15bb519..109044934 100644 --- a/apps/assets/const/account.py +++ b/apps/accounts/const/account.py @@ -2,12 +2,6 @@ from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -class Connectivity(TextChoices): - UNKNOWN = 'unknown', _('Unknown') - OK = 'ok', _('Ok') - FAILED = 'failed', _('Failed') - - class SecretType(TextChoices): PASSWORD = 'password', _('Password') SSH_KEY = 'ssh_key', _('SSH key') diff --git a/apps/accounts/const/automation.py b/apps/accounts/const/automation.py new file mode 100644 index 000000000..2c3b37bde --- /dev/null +++ b/apps/accounts/const/automation.py @@ -0,0 +1,94 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from assets.const import Connectivity +from common.db.fields import TreeChoices + +string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' +DEFAULT_PASSWORD_LENGTH = 30 +DEFAULT_PASSWORD_RULES = { + 'length': DEFAULT_PASSWORD_LENGTH, + 'symbol_set': string_punctuation +} + +__all__ = [ + 'AutomationTypes', 'SecretStrategy', 'SSHKeyStrategy', 'Connectivity', + 'DEFAULT_PASSWORD_LENGTH', 'DEFAULT_PASSWORD_RULES', 'TriggerChoice', + 'PushAccountActionChoice', +] + + +class AutomationTypes(models.TextChoices): + push_account = 'push_account', _('Push account') + change_secret = 'change_secret', _('Change secret') + verify_account = 'verify_account', _('Verify account') + gather_accounts = 'gather_accounts', _('Gather accounts') + + @classmethod + def get_type_model(cls, tp): + from accounts.models import ( + PushAccountAutomation, ChangeSecretAutomation, + VerifyAccountAutomation, GatherAccountsAutomation, + ) + type_model_dict = { + cls.push_account: PushAccountAutomation, + cls.change_secret: ChangeSecretAutomation, + cls.verify_account: VerifyAccountAutomation, + cls.gather_accounts: GatherAccountsAutomation, + } + return type_model_dict.get(tp) + + +class SecretStrategy(models.TextChoices): + custom = 'specific', _('Specific password') + random = 'random', _('Random') + + +class SSHKeyStrategy(models.TextChoices): + add = 'add', _('Append SSH KEY') + set = 'set', _('Empty and append SSH KEY') + set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') + + +class TriggerChoice(models.TextChoices, TreeChoices): + # 当资产创建时,直接创建账号,如果是动态账号,需要从授权中查询该资产被授权过的用户,已用户用户名为账号,创建 + on_asset_create = 'on_asset_create', _('On asset create') + # 授权变化包含,用户加入授权,用户组加入授权,资产加入授权,节点加入授权,账号变化 + # 当添加用户到授权时,查询所有同名账号 automation, 把本授权上的用户 (用户组), 创建到本授权的资产(节点)上 + on_perm_add_user = 'on_perm_add_user', _('On perm add user') + # 当添加用户组到授权时,查询所有同名账号 automation, 把本授权上的用户 (用户组), 创建到本授权的资产(节点)上 + on_perm_add_user_group = 'on_perm_add_user_group', _('On perm add user group') + # 当添加资产到授权时,查询授权的所有账号 automation, 创建到本授权的资产上 + on_perm_add_asset = 'on_perm_add_asset', _('On perm add asset') + # 当添加节点到授权时,查询授权的所有账号 automation, 创建到本授权的节点的资产上 + on_perm_add_node = 'on_perm_add_node', _('On perm add node') + # 当授权的账号变化时,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_perm_add_account = 'on_perm_add_account', _('On perm add account') + # 当资产添加到节点时,查询节点的授权规则,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_asset_join_node = 'on_asset_join_node', _('On asset join node') + # 当用户加入到用户组时,查询用户组的授权规则,查询授权的所有账号 automation, 创建到本授权的资产(节点)上 + on_user_join_group = 'on_user_join_group', _('On user join group') + + @classmethod + def branches(cls): + # 和用户和用户组相关的都是动态账号 + # + return [ + cls.on_asset_create, + (_("On perm change"), [ + cls.on_perm_add_user, + cls.on_perm_add_user_group, + cls.on_perm_add_asset, + cls.on_perm_add_node, + cls.on_perm_add_account, + ]), + (_("Inherit from group or node"), [ + cls.on_asset_join_node, + cls.on_user_join_group, + ]) + ] + + +class PushAccountActionChoice(models.TextChoices): + create_and_push = 'create_and_push', _('Create and push') + only_create = 'only_create', _('Only create') diff --git a/apps/accounts/filters.py b/apps/accounts/filters.py new file mode 100644 index 000000000..28feecef4 --- /dev/null +++ b/apps/accounts/filters.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# +from django.db.models import Q +from django_filters import rest_framework as drf_filters + +from assets.models import Node +from common.drf.filters import BaseFilterSet + +from .models import Account + + +class AccountFilterSet(BaseFilterSet): + ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact') + hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact') + username = drf_filters.CharFilter(field_name="username", lookup_expr='exact') + address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact') + asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact') + assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') + nodes = drf_filters.CharFilter(method='filter_nodes') + node_id = drf_filters.CharFilter(method='filter_nodes') + has_secret = drf_filters.BooleanFilter(method='filter_has_secret') + platform = drf_filters.CharFilter(field_name='asset__platform_id', lookup_expr='exact') + category = drf_filters.CharFilter(field_name='asset__platform__category', lookup_expr='exact') + type = drf_filters.CharFilter(field_name='asset__platform__type', lookup_expr='exact') + + @staticmethod + def filter_has_secret(queryset, name, has_secret): + q = Q(secret__isnull=True) | Q(secret='') + if has_secret: + return queryset.exclude(q) + else: + return queryset.filter(q) + + @staticmethod + def filter_nodes(queryset, name, value): + nodes = Node.objects.filter(id=value) + if not nodes: + return queryset + + node_qs = Node.objects.none() + for node in nodes: + node_qs |= node.get_all_children(with_self=True) + node_ids = list(node_qs.values_list('id', flat=True)) + queryset = queryset.filter(asset__nodes__in=node_ids) + return queryset + + class Meta: + model = Account + fields = ['id', 'asset_id'] diff --git a/apps/accounts/migrations/0001_initial.py b/apps/accounts/migrations/0001_initial.py new file mode 100644 index 000000000..201a5141d --- /dev/null +++ b/apps/accounts/migrations/0001_initial.py @@ -0,0 +1,113 @@ +# Generated by Django 3.2.14 on 2022-12-28 07:29 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('assets', '0098_auto_20220430_2126'), + ] + + operations = [ + migrations.CreateModel( + name='Account', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], + default='unknown', max_length=16, verbose_name='Connectivity')), + ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('version', models.IntegerField(default=0, verbose_name='Version')), + ('source', models.CharField(default='local', max_length=30, verbose_name='Source')), + ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', + to='assets.asset', verbose_name='Asset')), + ('su_from', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', + to='accounts.account', verbose_name='Su from')), + ], + options={ + 'verbose_name': 'Account', + 'permissions': [('view_accountsecret', 'Can view asset account secret'), + ('change_accountsecret', 'Can change asset account secret'), + ('view_historyaccount', 'Can view asset history account'), + ('view_historyaccountsecret', 'Can view asset history account secret')], + 'unique_together': {('username', 'asset', 'secret_type'), ('name', 'asset')}, + }, + ), + migrations.CreateModel( + name='HistoricalAccount', + fields=[ + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('version', models.IntegerField(default=0, verbose_name='Version')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical Account', + 'verbose_name_plural': 'historical Accounts', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='AccountTemplate', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ], + options={ + 'verbose_name': 'Account template', + 'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), + ('change_accounttemplatesecret', 'Can change asset account template secret')], + 'unique_together': {('name', 'org_id')}, + }, + ), + ] diff --git a/apps/accounts/migrations/0002_auto_20220616_0021.py b/apps/accounts/migrations/0002_auto_20220616_0021.py new file mode 100644 index 000000000..155800064 --- /dev/null +++ b/apps/accounts/migrations/0002_auto_20220616_0021.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:39 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0106_auto_20221228_1838'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBackupAutomation', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('types', models.JSONField(default=list)), + ('recipients', models.ManyToManyField(blank=True, related_name='recipient_escape_route_plans', + to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Account backup plan', + 'ordering': ['name'], + 'unique_together': {('name', 'org_id')}, + }, + ) + ] diff --git a/apps/accounts/migrations/0003_automation.py b/apps/accounts/migrations/0003_automation.py new file mode 100644 index 000000000..503c766af --- /dev/null +++ b/apps/accounts/migrations/0003_automation.py @@ -0,0 +1,194 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +import common.db.encoder +import common.db.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0107_automation'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0002_auto_20220616_0021'), + ] + + operations = [ + migrations.CreateModel( + name='AccountBaseAutomation', + fields=[ + ], + options={ + 'verbose_name': 'Account automation task', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='AutomationExecution', + fields=[ + ], + options={ + 'verbose_name': 'Automation execution', + 'verbose_name_plural': 'Automation executions', + 'permissions': [('view_changesecretexecution', 'Can view change secret execution'), + ('add_changesecretexection', 'Can add change secret execution'), + ('view_gatheraccountsexecution', 'Can view gather accounts execution'), + ('add_gatheraccountsexecution', 'Can add gather accounts execution')], + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.automationexecution',), + ), + migrations.CreateModel( + name='PushAccountAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), + ('random_one', 'All assets use the same random password'), + ('random_all', + 'All assets use different random password')], + default='specific', max_length=16, + verbose_name='Secret strategy')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), + ('ssh_key_change_strategy', models.CharField( + choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), + ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, + verbose_name='SSH key change strategy')), + ('triggers', models.JSONField(default=list, max_length=16, verbose_name='Triggers')), + ('username', models.CharField(max_length=128, verbose_name='Username')), + ('action', models.CharField(max_length=16, verbose_name='Action')), + ], + options={ + 'verbose_name': 'Push asset account', + }, + bases=('accounts.accountbaseautomation', models.Model), + ), + + migrations.CreateModel( + name='GatherAccountsAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Gather asset accounts', + }, + bases=('accounts.accountbaseautomation',), + ), + migrations.CreateModel( + name='VerifyAccountAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Verify asset account', + }, + bases=('accounts.accountbaseautomation',), + ), + migrations.CreateModel( + name='ChangeSecretRecord', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')), + ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), + ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), + ('status', models.CharField(default='pending', max_length=16)), + ('error', models.TextField(blank=True, null=True, verbose_name='Error')), + ('account', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), + ('asset', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset')), + ('execution', + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.automationexecution')), + ], + options={ + 'verbose_name': 'Change secret record', + }, + ), + migrations.CreateModel( + name='AccountBackupExecution', + fields=[ + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('date_start', models.DateTimeField(auto_now_add=True, verbose_name='Date start')), + ('timedelta', models.FloatField(default=0.0, null=True, verbose_name='Time')), + ('plan_snapshot', + models.JSONField(blank=True, default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, null=True, + verbose_name='Account backup snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], + default='manual', max_length=128, verbose_name='Trigger mode')), + ('reason', models.CharField(blank=True, max_length=1024, null=True, verbose_name='Reason')), + ('is_success', models.BooleanField(default=False, verbose_name='Is success')), + ('plan', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='execution', + to='accounts.accountbackupautomation', verbose_name='Account backup plan')), + ], + options={ + 'verbose_name': 'Account backup execution', + 'ordering': ('-date_start',), + }, + ), + migrations.CreateModel( + name='ChangeSecretAutomation', + fields=[ + ('baseautomation_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.baseautomation')), + ('secret_type', models.CharField( + choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), + ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), + ('secret_strategy', models.CharField(choices=[('specific', 'Specific password'), + ('random_one', 'All assets use the same random password'), + ('random_all', + 'All assets use different random password')], + default='specific', max_length=16, + verbose_name='Secret strategy')), + ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), + ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), + ('ssh_key_change_strategy', models.CharField( + choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), + ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, + verbose_name='SSH key change strategy')), + ('recipients', + models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), + ], + options={ + 'verbose_name': 'Change secret automation', + }, + bases=('accounts.accountbaseautomation', models.Model), + ), + migrations.AlterModelOptions( + name='automationexecution', + options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), + ('add_changesecretexection', 'Can add change secret execution'), + ('view_gatheraccountsexecution', 'Can view gather accounts execution'), + ('add_gatheraccountsexecution', 'Can add gather accounts execution'), + ('view_pushaccountexecution', 'Can view push account execution'), + ('add_pushaccountexecution', 'Can add push account execution')], + 'verbose_name': 'Automation execution', 'verbose_name_plural': 'Automation executions'}, + ), + migrations.AlterModelOptions( + name='changesecretrecord', + options={'ordering': ('-date_started',), 'verbose_name': 'Change secret record'}, + ), + ] diff --git a/apps/accounts/migrations/0004_auto_20230106_1507.py b/apps/accounts/migrations/0004_auto_20230106_1507.py new file mode 100644 index 000000000..3be64ef5a --- /dev/null +++ b/apps/accounts/migrations/0004_auto_20230106_1507.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2023-01-06 07:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_automation'), + ] + + operations = [ + migrations.AlterField( + model_name='changesecretautomation', + name='secret_strategy', + field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + ), + migrations.AlterField( + model_name='pushaccountautomation', + name='secret_strategy', + field=models.CharField(choices=[('specific', 'Specific password'), ('random', 'Random')], default='specific', max_length=16, verbose_name='Secret strategy'), + ), + ] diff --git a/apps/accounts/migrations/0005_alter_changesecretrecord_options.py b/apps/accounts/migrations/0005_alter_changesecretrecord_options.py new file mode 100644 index 000000000..67971198f --- /dev/null +++ b/apps/accounts/migrations/0005_alter_changesecretrecord_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0004_auto_20230106_1507'), + ] + + operations = [ + migrations.AlterModelOptions( + name='changesecretrecord', + options={'ordering': ('-date_created',), 'verbose_name': 'Change secret record'}, + ), + ] diff --git a/apps/accounts/migrations/__init__.py b/apps/accounts/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/accounts/models/__init__.py b/apps/accounts/models/__init__.py new file mode 100644 index 000000000..c40ee786d --- /dev/null +++ b/apps/accounts/models/__init__.py @@ -0,0 +1,3 @@ +from .base import * +from .account import * +from .automations import * diff --git a/apps/assets/models/account.py b/apps/accounts/models/account.py similarity index 96% rename from apps/assets/models/account.py rename to apps/accounts/models/account.py index e0e87d37c..4b33a1d1a 100644 --- a/apps/assets/models/account.py +++ b/apps/accounts/models/account.py @@ -4,7 +4,8 @@ from simple_history.models import HistoricalRecords from common.utils import lazyproperty from ..const import AliasAccount, Source -from .base import AbsConnectivity, BaseAccount +from assets.models.base import AbsConnectivity +from .base import BaseAccount __all__ = ['Account', 'AccountTemplate'] @@ -46,7 +47,7 @@ class Account(AbsConnectivity, BaseAccount): on_delete=models.CASCADE, verbose_name=_('Asset') ) su_from = models.ForeignKey( - 'assets.Account', related_name='su_to', null=True, + 'accounts.Account', related_name='su_to', null=True, on_delete=models.SET_NULL, verbose_name=_("Su from") ) version = models.IntegerField(default=0, verbose_name=_('Version')) diff --git a/apps/accounts/models/automations/__init__.py b/apps/accounts/models/automations/__init__.py new file mode 100644 index 000000000..682b182b6 --- /dev/null +++ b/apps/accounts/models/automations/__init__.py @@ -0,0 +1,6 @@ +from .base import * +from .backup_account import * +from .change_secret import * +from .gather_account import * +from .push_account import * +from .verify_account import * diff --git a/apps/assets/models/backup.py b/apps/accounts/models/automations/backup_account.py similarity index 85% rename from apps/assets/models/backup.py rename to apps/accounts/models/automations/backup_account.py index d4a8b8cc8..473e2f3c0 100644 --- a/apps/assets/models/backup.py +++ b/apps/accounts/models/automations/backup_account.py @@ -13,12 +13,12 @@ from common.utils import get_logger from ops.mixin import PeriodTaskModelMixin from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel -__all__ = ['AccountBackupPlan', 'AccountBackupPlanExecution'] +__all__ = ['AccountBackupAutomation', 'AccountBackupExecution'] logger = get_logger(__file__) -class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): +class AccountBackupAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): types = models.JSONField(default=list) recipients = models.ManyToManyField( 'users.User', related_name='recipient_escape_route_plans', blank=True, @@ -34,7 +34,7 @@ class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): verbose_name = _('Account backup plan') def get_register_task(self): - from ..tasks import execute_account_backup_plan + from ...tasks import execute_account_backup_plan name = "account_backup_plan_period_{}".format(str(self.id)[:8]) task = execute_account_backup_plan.name args = (str(self.id), Trigger.timing) @@ -56,18 +56,22 @@ class AccountBackupPlan(PeriodTaskModelMixin, JMSOrgBaseModel): } } + @property + def executed_amount(self): + return self.execution.count() + def execute(self, trigger): try: hid = current_task.request.id except AttributeError: hid = str(uuid.uuid4()) - execution = AccountBackupPlanExecution.objects.create( + execution = AccountBackupExecution.objects.create( id=hid, plan=self, plan_snapshot=self.to_attr_json(), trigger=trigger ) return execution.start() -class AccountBackupPlanExecution(OrgModelMixin): +class AccountBackupExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) date_start = models.DateTimeField( auto_now_add=True, verbose_name=_('Date start') @@ -88,11 +92,12 @@ class AccountBackupPlanExecution(OrgModelMixin): ) is_success = models.BooleanField(default=False, verbose_name=_('Is success')) plan = models.ForeignKey( - 'AccountBackupPlan', related_name='execution', on_delete=models.CASCADE, + 'AccountBackupAutomation', related_name='execution', on_delete=models.CASCADE, verbose_name=_('Account backup plan') ) class Meta: + ordering = ('-date_start',) verbose_name = _('Account backup execution') @property @@ -112,6 +117,6 @@ class AccountBackupPlanExecution(OrgModelMixin): return 'backup_account' def start(self): - from assets.automations.endpoint import ExecutionManager + from accounts.automations.endpoint import ExecutionManager manager = ExecutionManager(execution=self) return manager.run() diff --git a/apps/accounts/models/automations/base.py b/apps/accounts/models/automations/base.py new file mode 100644 index 000000000..ce45d6a81 --- /dev/null +++ b/apps/accounts/models/automations/base.py @@ -0,0 +1,41 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from assets.models.automations import ( + BaseAutomation as AssetBaseAutomation, + AutomationExecution as AssetAutomationExecution +) + +__all__ = ['AccountBaseAutomation', 'AutomationExecution'] + + +class AccountBaseAutomation(AssetBaseAutomation): + class Meta: + proxy = True + verbose_name = _("Account automation task") + + @property + def execution_model(self): + return AutomationExecution + + +class AutomationExecution(AssetAutomationExecution): + class Meta: + proxy = True + verbose_name = _("Automation execution") + verbose_name_plural = _("Automation executions") + permissions = [ + ('view_changesecretexecution', _('Can view change secret execution')), + ('add_changesecretexection', _('Can add change secret execution')), + + ('view_gatheraccountsexecution', _('Can view gather accounts execution')), + ('add_gatheraccountsexecution', _('Can add gather accounts execution')), + + ('view_pushaccountexecution', _('Can view push account execution')), + ('add_pushaccountexecution', _('Can add push account execution')), + ] + + def start(self): + from accounts.automations.endpoint import ExecutionManager + manager = ExecutionManager(execution=self) + return manager.run() diff --git a/apps/assets/models/automations/change_secret.py b/apps/accounts/models/automations/change_secret.py similarity index 83% rename from apps/assets/models/automations/change_secret.py rename to apps/accounts/models/automations/change_secret.py index 7fb801cab..d52fc2cae 100644 --- a/apps/assets/models/automations/change_secret.py +++ b/apps/accounts/models/automations/change_secret.py @@ -1,10 +1,12 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy from common.db import fields from common.db.models import JMSBaseModel -from .base import BaseAutomation +from accounts.const import ( + AutomationTypes, SecretType, SecretStrategy, SSHKeyStrategy +) +from .base import AccountBaseAutomation __all__ = ['ChangeSecretAutomation', 'ChangeSecretRecord', 'ChangeSecretMixin'] @@ -28,8 +30,20 @@ class ChangeSecretMixin(models.Model): class Meta: abstract = True + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'secret': self.secret, + 'secret_type': self.secret_type, + 'secret_strategy': self.secret_strategy, + 'password_rules': self.password_rules, + 'ssh_key_change_strategy': self.ssh_key_change_strategy, -class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): + }) + return attr_json + + +class ChangeSecretAutomation(ChangeSecretMixin, AccountBaseAutomation): recipients = models.ManyToManyField('users.User', verbose_name=_("Recipient"), blank=True) def save(self, *args, **kwargs): @@ -42,11 +56,6 @@ class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): def to_attr_json(self): attr_json = super().to_attr_json() attr_json.update({ - 'secret': self.secret, - 'secret_type': self.secret_type, - 'secret_strategy': self.secret_strategy, - 'password_rules': self.password_rules, - 'ssh_key_change_strategy': self.ssh_key_change_strategy, 'recipients': { str(recipient.id): (str(recipient), bool(recipient.secret_key)) for recipient in self.recipients.all() @@ -56,9 +65,9 @@ class ChangeSecretAutomation(BaseAutomation, ChangeSecretMixin): class ChangeSecretRecord(JMSBaseModel): - execution = models.ForeignKey('assets.AutomationExecution', on_delete=models.CASCADE) + execution = models.ForeignKey('accounts.AutomationExecution', on_delete=models.CASCADE) asset = models.ForeignKey('assets.Asset', on_delete=models.CASCADE, null=True) - account = models.ForeignKey('assets.Account', on_delete=models.CASCADE, null=True) + account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE, null=True) old_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Old secret')) new_secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) date_started = models.DateTimeField(blank=True, null=True, verbose_name=_('Date started')) @@ -67,6 +76,7 @@ class ChangeSecretRecord(JMSBaseModel): error = models.TextField(blank=True, null=True, verbose_name=_('Error')) class Meta: + ordering = ('-date_created',) verbose_name = _("Change secret record") def __str__(self): diff --git a/apps/assets/models/automations/gather_accounts.py b/apps/accounts/models/automations/gather_account.py similarity index 58% rename from apps/assets/models/automations/gather_accounts.py rename to apps/accounts/models/automations/gather_account.py index a3aa42383..1dd5500f7 100644 --- a/apps/assets/models/automations/gather_accounts.py +++ b/apps/accounts/models/automations/gather_account.py @@ -1,19 +1,15 @@ from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from .base import BaseAutomation +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation __all__ = ['GatherAccountsAutomation'] -class GatherAccountsAutomation(BaseAutomation): +class GatherAccountsAutomation(AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.gather_accounts super().save(*args, **kwargs) class Meta: verbose_name = _("Gather asset accounts") - - @property - def executed_amount(self): - return self.executions.count() diff --git a/apps/accounts/models/automations/push_account.py b/apps/accounts/models/automations/push_account.py new file mode 100644 index 000000000..4e4d95d9d --- /dev/null +++ b/apps/accounts/models/automations/push_account.py @@ -0,0 +1,41 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation +from .change_secret import ChangeSecretMixin + +__all__ = ['PushAccountAutomation'] + + +class PushAccountAutomation(ChangeSecretMixin, AccountBaseAutomation): + accounts = None + triggers = models.JSONField(max_length=16, default=list, verbose_name=_('Triggers')) + username = models.CharField(max_length=128, verbose_name=_('Username')) + action = models.CharField(max_length=16, verbose_name=_('Action')) + + def set_period_schedule(self): + pass + + @property + def dynamic_username(self): + return self.username == '@USER' + + @dynamic_username.setter + def dynamic_username(self, value): + if value: + self.username = '@USER' + + def save(self, *args, **kwargs): + self.type = AutomationTypes.push_account + super().save(*args, **kwargs) + + def to_attr_json(self): + attr_json = super().to_attr_json() + attr_json.update({ + 'username': self.username + }) + return attr_json + + class Meta: + verbose_name = _("Push asset account") diff --git a/apps/assets/models/automations/verify_account.py b/apps/accounts/models/automations/verify_account.py similarity index 67% rename from apps/assets/models/automations/verify_account.py rename to apps/accounts/models/automations/verify_account.py index cf7004820..03ec4f8c9 100644 --- a/apps/assets/models/automations/verify_account.py +++ b/apps/accounts/models/automations/verify_account.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from .base import BaseAutomation +from accounts.const import AutomationTypes +from .base import AccountBaseAutomation __all__ = ['VerifyAccountAutomation'] -class VerifyAccountAutomation(BaseAutomation): +class VerifyAccountAutomation(AccountBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.verify_account super().save(*args, **kwargs) diff --git a/apps/accounts/models/base.py b/apps/accounts/models/base.py new file mode 100644 index 000000000..ffae7051b --- /dev/null +++ b/apps/accounts/models/base.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +import os +from hashlib import md5 + +import sshpubkeys +from django.conf import settings +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from accounts.const import SecretType +from common.db import fields +from common.utils import ( + ssh_key_string_to_obj, ssh_key_gen, get_logger, + random_string, lazyproperty, parse_ssh_public_key_str +) +from orgs.mixins.models import JMSOrgBaseModel, OrgManager + +logger = get_logger(__file__) + + +class BaseAccountQuerySet(models.QuerySet): + def active(self): + return self.filter(is_active=True) + + +class BaseAccountManager(OrgManager): + def active(self): + return self.get_queryset().active() + + +class BaseAccount(JMSOrgBaseModel): + name = models.CharField(max_length=128, verbose_name=_("Name")) + username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) + secret_type = models.CharField( + max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type') + ) + secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) + privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) + is_active = models.BooleanField(default=True, verbose_name=_("Is active")) + + objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)() + + @property + def has_secret(self): + return bool(self.secret) + + @property + def has_username(self): + return bool(self.username) + + @property + def specific(self): + data = {} + if self.secret_type != SecretType.SSH_KEY: + return data + data['ssh_key_fingerprint'] = self.ssh_key_fingerprint + return data + + @property + def password(self): + if self.secret_type == SecretType.PASSWORD: + return self.secret + return None + + @property + def private_key(self): + if self.secret_type == SecretType.SSH_KEY: + return self.secret + return None + + @private_key.setter + def private_key(self, value): + self.secret = value + self.secret_type = SecretType.SSH_KEY + + @lazyproperty + def public_key(self): + if self.secret_type == SecretType.SSH_KEY and self.private_key: + return parse_ssh_public_key_str(self.private_key) + return None + + @property + def ssh_key_fingerprint(self): + if self.public_key: + public_key = self.public_key + elif self.private_key: + try: + public_key = parse_ssh_public_key_str(self.private_key) + except IOError as e: + return str(e) + else: + return '' + + public_key_obj = sshpubkeys.SSHKey(public_key) + fingerprint = public_key_obj.hash_md5() + return fingerprint + + @property + def private_key_obj(self): + if self.private_key: + key_obj = ssh_key_string_to_obj(self.private_key) + return key_obj + else: + return None + + @property + def private_key_path(self): + if not self.secret_type != SecretType.SSH_KEY \ + or not self.secret \ + or not self.private_key: + return None + project_dir = settings.PROJECT_DIR + tmp_dir = os.path.join(project_dir, 'tmp') + key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() + key_path = os.path.join(tmp_dir, key_name) + if not os.path.exists(key_path): + self.private_key_obj.write_private_key_file(key_path) + os.chmod(key_path, 0o400) + return key_path + + def get_private_key(self): + if not self.private_key: + return None + return self.private_key + + @property + def public_key_obj(self): + if self.public_key: + try: + return sshpubkeys.SSHKey(self.public_key) + except TabError: + pass + return None + + @staticmethod + def gen_password(length=36): + return random_string(length, special_char=True) + + @staticmethod + def gen_key(username): + private_key, public_key = ssh_key_gen(username=username) + return private_key, public_key + + def _to_secret_json(self): + """Push system user use it""" + return { + 'name': self.name, + 'username': self.username, + 'public_key': self.public_key, + } + + class Meta: + abstract = True diff --git a/apps/assets/notifications.py b/apps/accounts/notifications.py similarity index 61% rename from apps/assets/notifications.py rename to apps/accounts/notifications.py index a797bc845..3c4897df1 100644 --- a/apps/assets/notifications.py +++ b/apps/accounts/notifications.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ -from users.models import User from common.tasks import send_mail_attachment_async +from users.models import User class AccountBackupExecutionTaskMsg(object): @@ -15,11 +15,13 @@ class AccountBackupExecutionTaskMsg(object): def message(self): name = self.name if self.user.secret_key: - return _('{} - The account backup passage task has been completed. See the attachment for details').format( - name) - return _("{} - The account backup passage task has been completed: the encryption password has not been set - " - "please go to personal information -> file encryption password to set the encryption password").format( - name) + return _('{} - The account backup passage task has been completed.' + ' See the attachment for details').format(name) + else: + return _("{} - The account backup passage task has been completed: " + "the encryption password has not been set - " + "please go to personal information -> file encryption password " + "to set the encryption password").format(name) def publish(self, attachment_list=None): send_mail_attachment_async( @@ -38,10 +40,12 @@ class ChangeSecretExecutionTaskMsg(object): def message(self): name = self.name if self.user.secret_key: - return _('{} - The encryption change task has been completed. See the attachment for details').format(name) - return _("{} - The encryption change task has been completed: the encryption password has not been set - " - "please go to personal information -> file encryption password to set the encryption password").format( - name) + return _('{} - The encryption change task has been completed. ' + 'See the attachment for details').format(name) + else: + return _("{} - The encryption change task has been completed: the encryption " + "password has not been set - please go to personal information -> " + "file encryption password to set the encryption password").format(name) def publish(self, attachments=None): send_mail_attachment_async( diff --git a/apps/accounts/serializers/__init__.py b/apps/accounts/serializers/__init__.py new file mode 100644 index 000000000..e49a88b3d --- /dev/null +++ b/apps/accounts/serializers/__init__.py @@ -0,0 +1,2 @@ +from .account import * +from .automations import * diff --git a/apps/assets/serializers/account/__init__.py b/apps/accounts/serializers/account/__init__.py similarity index 77% rename from apps/assets/serializers/account/__init__.py rename to apps/accounts/serializers/account/__init__.py index 1e5dde298..725f00e94 100644 --- a/apps/assets/serializers/account/__init__.py +++ b/apps/accounts/serializers/account/__init__.py @@ -1,3 +1,4 @@ from .account import * -from .template import * from .backup import * +from .base import * +from .template import * diff --git a/apps/assets/serializers/account/account.py b/apps/accounts/serializers/account/account.py similarity index 92% rename from apps/assets/serializers/account/account.py rename to apps/accounts/serializers/account/account.py index c2eb786dd..43cc8e3d3 100644 --- a/apps/assets/serializers/account/account.py +++ b/apps/accounts/serializers/account/account.py @@ -1,11 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from assets.const import SecretType, Source -from assets.models import Account, AccountTemplate, Asset -from assets.tasks import push_accounts_to_assets -from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import SecretReadableMixin, BulkModelSerializer +from assets.models import Asset +from accounts.const import SecretType, Source +from accounts.models import Account, AccountTemplate +from accounts.tasks import push_accounts_to_assets +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers import SecretReadableMixin, BulkModelSerializer from .base import BaseAccountSerializer diff --git a/apps/assets/serializers/account/backup.py b/apps/accounts/serializers/account/backup.py similarity index 58% rename from apps/assets/serializers/account/backup.py rename to apps/accounts/serializers/account/backup.py index 34121dadd..57d4c9ccc 100644 --- a/apps/assets/serializers/account/backup.py +++ b/apps/accounts/serializers/account/backup.py @@ -7,26 +7,28 @@ from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ops.mixin import PeriodTaskSerializerMixin from common.utils import get_logger from common.const.choices import Trigger -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField -from assets.models import AccountBackupPlan, AccountBackupPlanExecution +from accounts.models import AccountBackupAutomation, AccountBackupExecution logger = get_logger(__file__) -__all__ = ['AccountBackupPlanSerializer', 'AccountBackupPlanExecutionSerializer'] +__all__ = ['AccountBackupSerializer', 'AccountBackupPlanExecutionSerializer'] -class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): +class AccountBackupSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): class Meta: - model = AccountBackupPlan - fields = [ - 'id', 'name', 'is_periodic', 'interval', 'crontab', 'date_created', - 'date_updated', 'created_by', 'periodic_display', 'comment', - 'recipients', 'types' + model = AccountBackupAutomation + read_only_fields = [ + 'date_created', 'date_updated', 'created_by', 'periodic_display', 'executed_amount' + ] + fields = read_only_fields + [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', 'recipients', 'types' ] extra_kwargs = { 'name': {'required': True}, 'periodic_display': {'label': _('Periodic perform')}, + 'executed_amount': {'label': _('Executed amount')}, 'recipients': {'label': _('Recipient'), 'help_text': _( 'Currently only mail sending is supported' )} @@ -34,9 +36,10 @@ class AccountBackupPlanSerializer(PeriodTaskSerializerMixin, BulkOrgResourceMode class AccountBackupPlanExecutionSerializer(serializers.ModelSerializer): + trigger = LabeledChoiceField(choices=Trigger.choices, label=_("Trigger mode")) class Meta: - model = AccountBackupPlanExecution + model = AccountBackupExecution read_only_fields = [ 'id', 'date_start', 'timedelta', 'plan_snapshot', 'trigger', 'reason', 'is_success', 'org_id', 'recipients' diff --git a/apps/accounts/serializers/account/base.py b/apps/accounts/serializers/account/base.py new file mode 100644 index 000000000..d92c4cc9c --- /dev/null +++ b/apps/accounts/serializers/account/base.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from accounts.const import SecretType +from accounts.models import BaseAccount +from accounts.utils import validate_password_for_ansible, validate_ssh_key +from common.serializers.fields import EncryptedField, LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer + +__all__ = ['AuthValidateMixin', 'BaseAccountSerializer'] + + +class AuthValidateMixin(serializers.Serializer): + secret_type = LabeledChoiceField( + choices=SecretType.choices, required=True, label=_('Secret type') + ) + secret = EncryptedField( + label=_('Secret'), required=False, max_length=40960, allow_blank=True, + allow_null=True, write_only=True, + ) + passphrase = serializers.CharField( + allow_blank=True, allow_null=True, required=False, max_length=512, + write_only=True, label=_('Key password') + ) + + @property + def initial_secret_type(self): + secret_type = self.initial_data.get('secret_type') + return secret_type + + def validate_secret(self, secret): + if not secret: + return '' + secret_type = self.initial_secret_type + if secret_type == SecretType.PASSWORD: + validate_password_for_ansible(secret) + return secret + elif secret_type == SecretType.SSH_KEY: + passphrase = self.initial_data.get('passphrase') + passphrase = passphrase if passphrase else None + return validate_ssh_key(secret, passphrase) + else: + return secret + + @staticmethod + def clean_auth_fields(validated_data): + for field in ('secret',): + value = validated_data.get(field) + if value is None: + validated_data.pop(field, None) + validated_data.pop('passphrase', None) + + def create(self, validated_data): + self.clean_auth_fields(validated_data) + return super().create(validated_data) + + def update(self, instance, validated_data): + self.clean_auth_fields(validated_data) + return super().update(instance, validated_data) + + +class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): + has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) + + class Meta: + model = BaseAccount + fields_mini = ['id', 'name', 'username'] + fields_small = fields_mini + [ + 'secret_type', 'secret', 'has_secret', 'passphrase', + 'privileged', 'is_active', 'specific', + ] + fields_other = ['created_by', 'date_created', 'date_updated', 'comment'] + fields = fields_small + fields_other + read_only_fields = [ + 'has_secret', 'specific', + 'date_verified', 'created_by', 'date_created', + ] + extra_kwargs = { + 'specific': {'label': _('Specific')}, + } diff --git a/apps/assets/serializers/account/template.py b/apps/accounts/serializers/account/template.py similarity index 89% rename from apps/assets/serializers/account/template.py rename to apps/accounts/serializers/account/template.py index 9227b5585..a72565cf5 100644 --- a/apps/assets/serializers/account/template.py +++ b/apps/accounts/serializers/account/template.py @@ -1,5 +1,5 @@ -from assets.models import AccountTemplate -from common.drf.serializers import SecretReadableMixin +from accounts.models import AccountTemplate +from common.serializers import SecretReadableMixin from .base import BaseAccountSerializer diff --git a/apps/accounts/serializers/automations/__init__.py b/apps/accounts/serializers/automations/__init__.py new file mode 100644 index 000000000..2b0aa0029 --- /dev/null +++ b/apps/accounts/serializers/automations/__init__.py @@ -0,0 +1,4 @@ +from .base import * +from .change_secret import * +from .gather_accounts import * +from .push_account import * diff --git a/apps/accounts/serializers/automations/base.py b/apps/accounts/serializers/automations/base.py new file mode 100644 index 000000000..8c11e8966 --- /dev/null +++ b/apps/accounts/serializers/automations/base.py @@ -0,0 +1,83 @@ +from django.utils.translation import ugettext as _ +from rest_framework import serializers + +from ops.mixin import PeriodTaskSerializerMixin +from assets.const import AutomationTypes +from assets.models import Asset, Node, BaseAutomation +from accounts.models import AutomationExecution +from orgs.mixins.serializers import BulkOrgResourceModelSerializer +from common.utils import get_logger +from common.serializers.fields import ObjectRelatedField + +logger = get_logger(__file__) + +__all__ = [ + 'BaseAutomationSerializer', 'AutomationExecutionSerializer', + 'UpdateAssetSerializer', 'UpdateNodeSerializer', 'AutomationAssetsSerializer', +] + + +class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSerializer): + assets = ObjectRelatedField(many=True, required=False, queryset=Asset.objects, label=_('Assets')) + nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) + + class Meta: + read_only_fields = [ + 'date_created', 'date_updated', 'created_by', 'periodic_display', 'executed_amount' + ] + fields = read_only_fields + [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', + 'type', 'accounts', 'nodes', 'assets', 'is_active', + ] + extra_kwargs = { + 'name': {'required': True}, + 'type': {'read_only': True}, + 'periodic_display': {'label': _('Periodic perform')}, + 'executed_amount': {'label': _('Executed amount')}, + } + + +class AutomationExecutionSerializer(serializers.ModelSerializer): + snapshot = serializers.SerializerMethodField(label=_('Automation snapshot')) + type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type')) + trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode')) + + class Meta: + model = AutomationExecution + read_only_fields = [ + 'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status' + ] + fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields + + @staticmethod + def get_snapshot(obj): + tp = obj.snapshot['type'] + snapshot = { + 'type': tp, + 'name': obj.snapshot['name'], + 'comment': obj.snapshot['comment'], + 'accounts': obj.snapshot['accounts'], + 'node_amount': len(obj.snapshot['nodes']), + 'asset_amount': len(obj.snapshot['assets']), + 'type_display': getattr(AutomationTypes, tp).label, + } + return snapshot + + +class UpdateAssetSerializer(serializers.ModelSerializer): + class Meta: + model = BaseAutomation + fields = ['id', 'assets'] + + +class UpdateNodeSerializer(serializers.ModelSerializer): + class Meta: + model = BaseAutomation + fields = ['id', 'nodes'] + + +class AutomationAssetsSerializer(serializers.ModelSerializer): + class Meta: + model = Asset + only_fields = ['id', 'name', 'address'] + fields = tuple(only_fields) diff --git a/apps/assets/serializers/automations/change_secret.py b/apps/accounts/serializers/automations/change_secret.py similarity index 77% rename from apps/assets/serializers/automations/change_secret.py rename to apps/accounts/serializers/automations/change_secret.py index b0149334d..4fbd55801 100644 --- a/apps/assets/serializers/automations/change_secret.py +++ b/apps/accounts/serializers/automations/change_secret.py @@ -3,12 +3,18 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers +from accounts.const import ( + DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy +) +from accounts.models import ( + Account, ChangeSecretAutomation, + ChangeSecretRecord +) +from accounts.models import AutomationExecution +from accounts.serializers import AuthValidateMixin +from assets.models import Asset +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from common.utils import get_logger -from common.drf.fields import LabeledChoiceField, ObjectRelatedField -from assets.serializers.base import AuthValidateMixin -from assets.const import DEFAULT_PASSWORD_RULES, SecretType, SecretStrategy, SSHKeyStrategy -from assets.models import Asset, Account, ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution - from .base import BaseAutomationSerializer logger = get_logger(__file__) @@ -16,10 +22,19 @@ logger = get_logger(__file__) __all__ = [ 'ChangeSecretAutomationSerializer', 'ChangeSecretRecordSerializer', - 'ChangeSecretRecordBackUpSerializer' + 'ChangeSecretRecordBackUpSerializer', + 'ChangeSecretUpdateAssetSerializer', + 'ChangeSecretUpdateNodeSerializer', ] +def get_secret_types(): + return [ + (SecretType.PASSWORD, _('Password')), + (SecretType.SSH_KEY, _('SSH key')), + ] + + class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializer): secret_strategy = LabeledChoiceField( choices=SecretStrategy.choices, required=True, label=_('Secret strategy') @@ -28,6 +43,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ choices=SSHKeyStrategy.choices, required=False, label=_('SSH Key strategy') ) password_rules = serializers.DictField(default=DEFAULT_PASSWORD_RULES) + secret_type = LabeledChoiceField(choices=get_secret_types(), required=True, label=_('Secret type')) class Meta: model = ChangeSecretAutomation @@ -42,24 +58,14 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ )}, }} - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.set_secret_type_choices() - - def set_secret_type_choices(self): - secret_type = self.fields.get('secret_type') - if not secret_type: - return - choices = secret_type._choices - choices.pop(SecretType.ACCESS_KEY, None) - choices.pop(SecretType.TOKEN, None) - secret_type._choices = choices - def validate_password_rules(self, password_rules): secret_type = self.initial_secret_type if secret_type != SecretType.PASSWORD: return password_rules + if self.initial_data.get('secret_strategy') == SecretStrategy.custom: + return password_rules + length = password_rules.get('length') symbol_set = password_rules.get('symbol_set', '') @@ -69,6 +75,7 @@ class ChangeSecretAutomationSerializer(AuthValidateMixin, BaseAutomationSerializ logger.error(e) msg = _("* Please enter the correct password length") raise serializers.ValidationError(msg) + if length < 6 or length > 30: msg = _('* Password length range 6-30 bits') raise serializers.ValidationError(msg) @@ -113,9 +120,7 @@ class ChangeSecretRecordSerializer(serializers.ModelSerializer): @staticmethod def get_is_success(obj): - if obj.status == 'success': - return _("Success") - return _("Failed") + return obj.status == 'success' class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): @@ -144,3 +149,15 @@ class ChangeSecretRecordBackUpSerializer(serializers.ModelSerializer): if obj.status == 'success': return _("Success") return _("Failed") + + +class ChangeSecretUpdateAssetSerializer(serializers.ModelSerializer): + class Meta: + model = ChangeSecretAutomation + fields = ['id', 'assets'] + + +class ChangeSecretUpdateNodeSerializer(serializers.ModelSerializer): + class Meta: + model = ChangeSecretAutomation + fields = ['id', 'nodes'] diff --git a/apps/assets/serializers/automations/gather_accounts.py b/apps/accounts/serializers/automations/gather_accounts.py similarity index 68% rename from apps/assets/serializers/automations/gather_accounts.py rename to apps/accounts/serializers/automations/gather_accounts.py index 6b86fd64c..ffca89198 100644 --- a/apps/assets/serializers/automations/gather_accounts.py +++ b/apps/accounts/serializers/automations/gather_accounts.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ -from assets.models import GatherAccountsAutomation +from accounts.models import GatherAccountsAutomation from common.utils import get_logger from .base import BaseAutomationSerializer @@ -16,9 +16,7 @@ __all__ = [ class GatherAccountAutomationSerializer(BaseAutomationSerializer): class Meta: model = GatherAccountsAutomation - read_only_fields = BaseAutomationSerializer.Meta.read_only_fields + ['executed_amount'] + read_only_fields = BaseAutomationSerializer.Meta.read_only_fields fields = BaseAutomationSerializer.Meta.fields + read_only_fields - extra_kwargs = {**BaseAutomationSerializer.Meta.extra_kwargs, **{ - 'executed_amount': {'label': _('Executed amount')} - }} + extra_kwargs = BaseAutomationSerializer.Meta.extra_kwargs diff --git a/apps/accounts/serializers/automations/push_account.py b/apps/accounts/serializers/automations/push_account.py new file mode 100644 index 000000000..aad6041ba --- /dev/null +++ b/apps/accounts/serializers/automations/push_account.py @@ -0,0 +1,73 @@ +import copy +from accounts.models import PushAccountAutomation +from .change_secret import ( + ChangeSecretAutomationSerializer, ChangeSecretUpdateAssetSerializer, + ChangeSecretUpdateNodeSerializer +) + + +class PushAccountAutomationSerializer(ChangeSecretAutomationSerializer): + # dynamic_username = serializers.BooleanField(label=_('Dynamic username'), default=False) + # triggers = TreeChoicesField( + # choice_cls=TriggerChoice, label=_('Triggers'), + # default=TriggerChoice.all(), + # ) + # action = LabeledChoiceField( + # choices=PushAccountActionChoice.choices, label=_('Action'), + # default=PushAccountActionChoice.create_and_push + # ) + + class Meta(ChangeSecretAutomationSerializer.Meta): + model = PushAccountAutomation + fields = copy.copy(ChangeSecretAutomationSerializer.Meta.fields) + fields.remove('recipients') + + # fields = ChangeSecretAutomationSerializer.Meta.fields + [ + # 'dynamic_username', 'triggers', 'action' + # ] + + # def validate_username(self, value): + # if self.initial_data.get('dynamic_username'): + # value = '@USER' + # queryset = self.Meta.model.objects.filter(username=value) + # if self.instance: + # queryset = queryset.exclude(id=self.instance.id) + # if queryset.exists(): + # raise serializers.ValidationError(_('Username already exists')) + # return value + # + # def validate_dynamic_username(self, value): + # if not value: + # return value + # queryset = self.Meta.model.objects.filter(username='@USER') + # if self.instance: + # queryset = queryset.exclude(id=self.instance.id) + # if queryset.exists(): + # raise serializers.ValidationError(_('Dynamic username already exists')) + # return value + # + # def validate_triggers(self, value): + # # Now triggers readonly, set all + # return TriggerChoice.all() + # + # def get_field_names(self, declared_fields, info): + # fields = super().get_field_names(declared_fields, info) + # excludes = [ + # 'recipients', 'is_periodic', 'interval', 'crontab', + # 'periodic_display', 'assets', 'nodes' + # ] + # fields = [f for f in fields if f not in excludes] + # fields[fields.index('accounts')] = 'username' + # return fields + + +class PushAccountUpdateAssetSerializer(ChangeSecretUpdateAssetSerializer): + class Meta: + model = PushAccountAutomation + fields = ChangeSecretUpdateAssetSerializer.Meta.fields + + +class PushAccountUpdateNodeSerializer(ChangeSecretUpdateNodeSerializer): + class Meta: + model = PushAccountAutomation + fields = ChangeSecretUpdateNodeSerializer.Meta.fields diff --git a/apps/accounts/signal_handlers.py b/apps/accounts/signal_handlers.py new file mode 100644 index 000000000..311c8d83d --- /dev/null +++ b/apps/accounts/signal_handlers.py @@ -0,0 +1,26 @@ +from django.db.models.signals import pre_save, post_save +from django.dispatch import receiver + +from assets.models import Asset +from common.decorator import on_transaction_commit +from common.utils import get_logger +from .automations.push_account.manager import PushAccountManager +from .models import Account + +logger = get_logger(__name__) + + +@receiver(pre_save, sender=Account) +def on_account_pre_create(sender, instance, **kwargs): + # 升级版本号 + instance.version += 1 + # 即使在 root 组织也不怕 + instance.org_id = instance.asset.org_id + + +@receiver(post_save, sender=Asset) +@on_transaction_commit +def on_asset_create(sender, instance, created=False, **kwargs): + if not created: + return + # PushAccountManager.trigger_by_asset_create(instance) diff --git a/apps/accounts/tasks/__init__.py b/apps/accounts/tasks/__init__.py new file mode 100644 index 000000000..055508b24 --- /dev/null +++ b/apps/accounts/tasks/__init__.py @@ -0,0 +1,5 @@ +from .backup_account import * +from .automation import * +from .push_account import * +from .verify_account import * +from .gather_accounts import * diff --git a/apps/accounts/tasks/automation.py b/apps/accounts/tasks/automation.py new file mode 100644 index 000000000..67d4c4a90 --- /dev/null +++ b/apps/accounts/tasks/automation.py @@ -0,0 +1,20 @@ +from celery import shared_task +from django.utils.translation import gettext_lazy as _ + +from orgs.utils import tmp_to_root_org, tmp_to_org +from common.utils import get_logger, get_object_or_none +from accounts.const import AutomationTypes + +logger = get_logger(__file__) + + +@shared_task(queue='ansible', verbose_name=_('Account execute automation')) +def execute_automation(pid, trigger, tp): + model = AutomationTypes.get_type_model(tp) + with tmp_to_root_org(): + instance = get_object_or_none(model, pk=pid) + if not instance: + logger.error("No automation task found: {}".format(pid)) + return + with tmp_to_org(instance.org): + instance.execute(trigger) diff --git a/apps/assets/tasks/backup.py b/apps/accounts/tasks/backup_account.py similarity index 82% rename from apps/assets/tasks/backup.py rename to apps/accounts/tasks/backup_account.py index a82a6abd1..bbcf25d1f 100644 --- a/apps/assets/tasks/backup.py +++ b/apps/accounts/tasks/backup_account.py @@ -3,9 +3,9 @@ from celery import shared_task from django.utils.translation import gettext_lazy as _ +from accounts.models import AccountBackupAutomation from common.utils import get_object_or_none, get_logger from orgs.utils import tmp_to_org, tmp_to_root_org -from assets.models import AccountBackupPlan logger = get_logger(__file__) @@ -13,7 +13,7 @@ logger = get_logger(__file__) @shared_task(verbose_name=_('Execute account backup plan')) def execute_account_backup_plan(pid, trigger): with tmp_to_root_org(): - plan = get_object_or_none(AccountBackupPlan, pk=pid) + plan = get_object_or_none(AccountBackupAutomation, pk=pid) if not plan: logger.error("No account backup route plan found: {}".format(pid)) return diff --git a/apps/accounts/tasks/common.py b/apps/accounts/tasks/common.py new file mode 100644 index 000000000..f45032939 --- /dev/null +++ b/apps/accounts/tasks/common.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# +from assets.tasks.common import generate_data +from common.const.choices import Trigger + + +def automation_execute_start(task_name, tp, child_snapshot=None): + from accounts.models import AutomationExecution + data = generate_data(task_name, tp, child_snapshot) + execution = AutomationExecution.objects.create( + trigger=Trigger.manual, **data + ) + execution.start() diff --git a/apps/assets/tasks/gather_accounts.py b/apps/accounts/tasks/gather_accounts.py similarity index 63% rename from apps/assets/tasks/gather_accounts.py rename to apps/accounts/tasks/gather_accounts.py index 5e20bfe73..dbfbe981e 100644 --- a/apps/assets/tasks/gather_accounts.py +++ b/apps/accounts/tasks/gather_accounts.py @@ -3,9 +3,11 @@ from celery import shared_task from django.utils.translation import gettext_noop from django.utils.translation import gettext_lazy as _ -from orgs.utils import tmp_to_root_org, org_aware_func -from common.utils import get_logger from assets.models import Node +from common.utils import get_logger +from orgs.utils import org_aware_func +from accounts.const import AutomationTypes +from accounts.tasks.common import automation_execute_start __all__ = ['gather_asset_accounts'] logger = get_logger(__name__) @@ -13,16 +15,14 @@ logger = get_logger(__name__) @org_aware_func("nodes") def gather_asset_accounts_util(nodes, task_name): - from assets.models import GatherAccountsAutomation + from accounts.models import GatherAccountsAutomation task_name = GatherAccountsAutomation.generate_unique_name(task_name) - data = { - 'name': task_name, - 'comment': ', '.join([str(i) for i in nodes]) + child_snapshot = { + 'nodes': [str(node.id) for node in nodes], } - instance = GatherAccountsAutomation.objects.create(**data) - instance.nodes.add(*nodes) - instance.execute() + tp = AutomationTypes.verify_account + automation_execute_start(task_name, tp, child_snapshot) @shared_task(queue="ansible", verbose_name=_('Gather asset accounts')) @@ -30,6 +30,5 @@ def gather_asset_accounts(node_ids, task_name=None): if task_name is None: task_name = gettext_noop("Gather assets accounts") - with tmp_to_root_org(): - nodes = Node.objects.filter(id__in=node_ids) + nodes = Node.objects.filter(id__in=node_ids) gather_asset_accounts_util(nodes=nodes, task_name=task_name) diff --git a/apps/accounts/tasks/push_account.py b/apps/accounts/tasks/push_account.py new file mode 100644 index 000000000..4ae09d72b --- /dev/null +++ b/apps/accounts/tasks/push_account.py @@ -0,0 +1,43 @@ +from celery import shared_task +from django.utils.translation import gettext_noop, ugettext_lazy as _ + +from common.utils import get_logger +from orgs.utils import org_aware_func +from accounts.const import AutomationTypes +from accounts.tasks.common import automation_execute_start + +logger = get_logger(__file__) +__all__ = [ + 'push_accounts_to_assets', +] + + +def push_util(account, assets, task_name): + child_snapshot = { + 'secret': account.secret, + 'secret_type': account.secret_type, + 'accounts': [account.username], + 'assets': [str(asset.id) for asset in assets], + } + tp = AutomationTypes.push_account + automation_execute_start(task_name, tp, child_snapshot) + + +@org_aware_func("assets") +def push_accounts_to_assets_util(accounts, assets): + from accounts.models import PushAccountAutomation + + task_name = gettext_noop("Push accounts to assets") + task_name = PushAccountAutomation.generate_unique_name(task_name) + for account in accounts: + push_util(account, assets, task_name) + + +@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) +def push_accounts_to_assets(account_ids, asset_ids): + from assets.models import Asset + from accounts.models import Account + + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) + return push_accounts_to_assets_util(accounts, assets) diff --git a/apps/assets/tasks/verify_account.py b/apps/accounts/tasks/verify_account.py similarity index 59% rename from apps/assets/tasks/verify_account.py rename to apps/accounts/tasks/verify_account.py index 4538f2b2d..0b69262f5 100644 --- a/apps/assets/tasks/verify_account.py +++ b/apps/accounts/tasks/verify_account.py @@ -3,7 +3,9 @@ from django.utils.translation import gettext_noop from django.utils.translation import ugettext as _ from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org +from accounts.tasks.common import automation_execute_start +from accounts.const import AutomationTypes +from orgs.utils import org_aware_func logger = get_logger(__name__) __all__ = [ @@ -13,26 +15,23 @@ __all__ = [ @org_aware_func("assets") def verify_accounts_connectivity_util(accounts, assets, task_name): - from assets.models import VerifyAccountAutomation + from accounts.models import VerifyAccountAutomation task_name = VerifyAccountAutomation.generate_unique_name(task_name) account_usernames = list(accounts.values_list('username', flat=True)) - data = { - 'name': task_name, + child_snapshot = { 'accounts': account_usernames, - 'comment': ', '.join([str(i) for i in assets]) + 'assets': [str(asset.id) for asset in assets], } - instance = VerifyAccountAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() + tp = AutomationTypes.verify_account + automation_execute_start(task_name, tp, child_snapshot) @shared_task(queue="ansible", verbose_name=_('Verify asset account availability')) def verify_accounts_connectivity(account_ids, asset_ids): - from assets.models import Asset, Account - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - accounts = Account.objects.filter(id__in=account_ids) - + from assets.models import Asset + from accounts.models import Account + assets = Asset.objects.filter(id__in=asset_ids) + accounts = Account.objects.filter(id__in=account_ids) task_name = gettext_noop("Verify accounts connectivity") return verify_accounts_connectivity_util(accounts, assets, task_name) diff --git a/apps/accounts/tests.py b/apps/accounts/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/apps/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/accounts/urls.py b/apps/accounts/urls.py new file mode 100644 index 000000000..b5280d8ab --- /dev/null +++ b/apps/accounts/urls.py @@ -0,0 +1,41 @@ +# coding:utf-8 +from django.urls import path +from rest_framework_bulk.routes import BulkRouter + +from . import api + +app_name = 'accounts' + +router = BulkRouter() + +router.register(r'accounts', api.AccountViewSet, 'account') +router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') +router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') +router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') +router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') +router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') +router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') +router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') +router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') +router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation') +router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-execution') +router.register(r'push-account-automations', api.PushAccountAutomationViewSet, 'push-account-automation') +router.register(r'push-account-executions', api.PushAccountExecutionViewSet, 'push-account-execution') +router.register(r'push-account-records', api.PushAccountRecordViewSet, 'push-account-record') + +urlpatterns = [ + path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), + path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(), name='account-secret-history'), + + path('change-secret//asset/remove/', api.ChangSecretRemoveAssetApi.as_view(), name='change-secret-remove-asset'), + path('change-secret//asset/add/', api.ChangSecretAddAssetApi.as_view(), name='change-secret-add-asset'), + path('change-secret//nodes/', api.ChangSecretNodeAddRemoveApi.as_view(), name='change-secret-add-or-remove-node'), + path('change-secret//assets/', api.ChangSecretAssetsListApi.as_view(), name='change-secret-assets'), + + path('push-account//asset/remove/', api.PushAccountRemoveAssetApi.as_view(), name='push-account-remove-asset'), + path('push-accountt//asset/add/', api.PushAccountAddAssetApi.as_view(), name='push-account-add-asset'), + path('push-account//nodes/', api.PushAccountNodeAddRemoveApi.as_view(), name='push-account-add-or-remove-node'), + path('push-account//assets/', api.PushAccountAssetsListApi.as_view(), name='push-account-assets'), +] + +urlpatterns += router.urls diff --git a/apps/accounts/utils.py b/apps/accounts/utils.py new file mode 100644 index 000000000..0a64e54e0 --- /dev/null +++ b/apps/accounts/utils.py @@ -0,0 +1,54 @@ +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers + +from accounts.const import ( + SecretType, DEFAULT_PASSWORD_RULES +) +from common.utils import gen_key_pair, random_string +from common.utils import validate_ssh_private_key, parse_ssh_private_key_str + + +class SecretGenerator: + def __init__(self, secret_strategy, secret_type, password_rules=None): + self.secret_strategy = secret_strategy + self.secret_type = secret_type + self.password_rules = password_rules + + @staticmethod + def generate_ssh_key(): + private_key, public_key = gen_key_pair() + return private_key + + def generate_password(self): + length = int(self.password_rules.get('length', DEFAULT_PASSWORD_RULES['length'])) + return random_string(length, special_char=True) + + def get_secret(self): + if self.secret_type == SecretType.SSH_KEY: + secret = self.generate_ssh_key() + elif self.secret_type == SecretType.PASSWORD: + secret = self.generate_password() + else: + raise ValueError("Secret must be set") + return secret + + +def validate_password_for_ansible(password): + """ 校验 Ansible 不支持的特殊字符 """ + # validate password contains left double curly bracket + # check password not contains `{{` + # Ansible 推送的时候不支持 + if '{{' in password: + raise serializers.ValidationError(_('Password can not contains `{{` ')) + # Ansible Windows 推送的时候不支持 + if "'" in password: + raise serializers.ValidationError(_("Password can not contains `'` ")) + if '"' in password: + raise serializers.ValidationError(_('Password can not contains `"` ')) + + +def validate_ssh_key(ssh_key, passphrase=None): + valid = validate_ssh_private_key(ssh_key, password=passphrase) + if not valid: + raise serializers.ValidationError(_("private key invalid or passphrase error")) + return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/acls/api/command_acl.py b/apps/acls/api/command_acl.py index 80163e717..44a342743 100644 --- a/apps/acls/api/command_acl.py +++ b/apps/acls/api/command_acl.py @@ -35,21 +35,5 @@ class CommandFilterACLViewSet(OrgBulkModelViewSet): 'org_id': serializer.org.id } ticket = serializer.cmd_filter_acl.create_command_review_ticket(**data) - - url_review_status = reverse( - view_name='api-tickets:super-ticket-status', kwargs={'pk': str(ticket.id)} - ) - url_ticket_detail = reverse( - view_name='api-tickets:ticket-detail', kwargs={'pk': str(ticket.id)}, - external=True, api_to_ui=True - ) - resp_data = { - 'check_review_status': {'method': 'GET', 'url': url_review_status}, - 'close_review': {'method': 'DELETE', 'url': url_review_status}, - 'ticket_detail_url': url_ticket_detail, - 'reviewers': [ - str(ticket_assignee.assignee) - for ticket_assignee in ticket.current_step.ticket_assignees.all() - ] - } - return Response(resp_data) + info = ticket.get_extra_info_of_review(user=request.user) + return info diff --git a/apps/acls/api/login_acl.py b/apps/acls/api/login_acl.py index 85e902143..806ffb343 100644 --- a/apps/acls/api/login_acl.py +++ b/apps/acls/api/login_acl.py @@ -1,4 +1,4 @@ -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet from ..models import LoginACL from .. import serializers from ..filters import LoginAclFilter diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py index 386f4480c..a593f0c27 100644 --- a/apps/acls/api/login_asset_check.py +++ b/apps/acls/api/login_asset_check.py @@ -48,7 +48,7 @@ class LoginAssetCheckAPI(CreateAPIView): return response_data def _get_response_data_of_need_review(self, acl) -> dict: - ticket = LoginAssetACL.create_login_asset_confirm_ticket( + ticket = LoginAssetACL.create_login_asset_review_ticket( user=self.serializer.user, asset=self.serializer.asset, account_username=self.serializer.validated_data.get('account_username'), diff --git a/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py b/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py new file mode 100644 index 000000000..b657906f1 --- /dev/null +++ b/apps/acls/migrations/0010_alter_commandfilteracl_command_groups.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('acls', '0009_auto_20221220_1956'), + ] + + operations = [ + migrations.AlterField( + model_name='commandfilteracl', + name='command_groups', + field=models.ManyToManyField(to='acls.CommandGroup', verbose_name='Command group'), + ), + ] diff --git a/apps/acls/models/base.py b/apps/acls/models/base.py index 256241361..7ba48b3c6 100644 --- a/apps/acls/models/base.py +++ b/apps/acls/models/base.py @@ -85,6 +85,9 @@ class BaseACL(JMSBaseModel): ordering = ('priority', 'name') abstract = True + def is_action(self, action): + return self.action == action + class UserAssetAccountBaseACL(BaseACL, OrgModelMixin): # username_group diff --git a/apps/acls/models/command_acl.py b/apps/acls/models/command_acl.py index b02bc09be..9029142f2 100644 --- a/apps/acls/models/command_acl.py +++ b/apps/acls/models/command_acl.py @@ -93,7 +93,7 @@ class CommandGroup(JMSOrgBaseModel): class CommandFilterACL(UserAssetAccountBaseACL): - command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Commands')) + command_groups = models.ManyToManyField(CommandGroup, verbose_name=_('Command group')) class Meta(UserAssetAccountBaseACL.Meta): abstract = False diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py index bdaf9b60c..609491e8d 100644 --- a/apps/acls/models/login_asset_acl.py +++ b/apps/acls/models/login_asset_acl.py @@ -14,7 +14,7 @@ class LoginAssetACL(UserAssetAccountBaseACL): return self.name @classmethod - def create_login_asset_confirm_ticket(cls, user, asset, account_username, assignees, org_id): + def create_login_asset_review_ticket(cls, user, asset, account_username, assignees, org_id): from tickets.const import TicketType from tickets.models import ApplyLoginAssetTicket title = _('Login asset confirm') + ' ({})'.format(user) diff --git a/apps/acls/serializers/base.py b/apps/acls/serializers/base.py index fdf6f8a23..24b6867e7 100644 --- a/apps/acls/serializers/base.py +++ b/apps/acls/serializers/base.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from acls.models.base import ActionChoices -from common.drf.fields import LabeledChoiceField, ObjectRelatedField +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField from orgs.models import Organization from users.models import User @@ -55,10 +55,28 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): users = ACLUsersSerializer(label=_('User')) assets = ACLAssestsSerializer(label=_('Asset')) accounts = ACLAccountsSerializer(label=_('Account')) + users_username_group = serializers.ListField( + source='users.username_group', read_only=True, child=serializers.CharField(), + label=_('User (username)') + ) + assets_name_group = serializers.ListField( + source='assets.name_group', read_only=True, child=serializers.CharField(), + label=_('Asset (name)') + ) + assets_address_group = serializers.ListField( + source='assets.address_group', read_only=True, child=serializers.CharField(), + label=_('Asset (address)') + ) + accounts_username_group = serializers.ListField( + source='accounts.username_group', read_only=True, child=serializers.CharField(), + label=_('Account (username)') + ) reviewers = ObjectRelatedField( queryset=User.objects, many=True, required=False, label=_('Reviewers') ) - reviewers_amount = serializers.IntegerField(read_only=True, source="reviewers.count") + reviewers_amount = serializers.IntegerField( + read_only=True, source="reviewers.count", label=_('Reviewers amount') + ) action = LabeledChoiceField( choices=ActionChoices.choices, default=ActionChoices.reject, label=_("Action") ) @@ -66,6 +84,8 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): class Meta: fields_mini = ["id", "name"] fields_small = fields_mini + [ + 'users_username_group', 'assets_address_group', 'assets_name_group', + 'accounts_username_group', "users", "accounts", "assets", "is_active", "date_created", "date_updated", "priority", "action", "comment", "created_by", "org_id", @@ -73,7 +93,6 @@ class BaseUserAssetAccountACLSerializerMixin(serializers.Serializer): fields_m2m = ["reviewers", "reviewers_amount"] fields = fields_small + fields_m2m extra_kwargs = { - "reviewers": {"allow_null": False, "required": True}, "priority": {"default": 50}, "is_active": {"default": True}, } diff --git a/apps/acls/serializers/command_acl.py b/apps/acls/serializers/command_acl.py index 07f2e39a7..f4729ffd5 100644 --- a/apps/acls/serializers/command_acl.py +++ b/apps/acls/serializers/command_acl.py @@ -1,11 +1,10 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers - from terminal.models import Session from acls.models import CommandGroup, CommandFilterACL from common.utils import lazyproperty, get_object_or_none -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from orgs.utils import tmp_to_root_org from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .base import BaseUserAssetAccountACLSerializerMixin as BaseSerializer @@ -28,10 +27,13 @@ class CommandFilterACLSerializer(BaseSerializer, BulkOrgResourceModelSerializer) command_groups = ObjectRelatedField( queryset=CommandGroup.objects, many=True, required=False, label=_('Command group') ) + command_groups_amount = serializers.IntegerField( + source='command_groups.count', read_only=True, label=_('Command group amount') + ) class Meta(BaseSerializer.Meta): model = CommandFilterACL - fields = BaseSerializer.Meta.fields + ['command_groups'] + fields = BaseSerializer.Meta.fields + ['command_groups', 'command_groups_amount'] class CommandReviewSerializer(serializers.Serializer): diff --git a/apps/acls/serializers/login_acl.py b/apps/acls/serializers/login_acl.py index db89445c8..99b1f1077 100644 --- a/apps/acls/serializers/login_acl.py +++ b/apps/acls/serializers/login_acl.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.drf.fields import ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import BulkModelSerializer, MethodSerializer +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers import BulkModelSerializer, MethodSerializer from jumpserver.utils import has_valid_xpack_license from users.models import User from .rules import RuleSerializer @@ -24,9 +24,9 @@ class LoginACLSerializer(BulkModelSerializer): ) action = LabeledChoiceField(choices=LoginACL.ActionChoices.choices) reviewers_amount = serializers.IntegerField( - read_only=True, source="reviewers.count" + read_only=True, source="reviewers.count", label=_("Reviewers amount") ) - rules = MethodSerializer() + rules = MethodSerializer(label=_('Rule')) class Meta: model = LoginACL @@ -34,15 +34,14 @@ class LoginACLSerializer(BulkModelSerializer): fields_small = fields_mini + [ "priority", "user", "rules", "action", "is_active", "date_created", "date_updated", - "reviewers_amount", "comment", "created_by", + "comment", "created_by", ] fields_fk = ["user"] - fields_m2m = ["reviewers"] + fields_m2m = ["reviewers", "reviewers_amount"] fields = fields_small + fields_fk + fields_m2m extra_kwargs = { "priority": {"default": 50}, "is_active": {"default": True}, - "reviewers": {"allow_null": False, "required": True}, } def __init__(self, *args, **kwargs): diff --git a/apps/applications/migrations/0026_auto_20220817_1716.py b/apps/applications/migrations/0026_auto_20220817_1716.py index 5fbf9c1d6..bb9df0808 100644 --- a/apps/applications/migrations/0026_auto_20220817_1716.py +++ b/apps/applications/migrations/0026_auto_20220817_1716.py @@ -42,7 +42,7 @@ class Migration(migrations.Migration): ('applications', '0025_auto_20220817_1346'), ('perms', '0031_auto_20220816_1600'), ('ops', '0022_auto_20220817_1346'), - ('assets', '0105_auto_20220817_1544'), + ('assets', '0100_auto_20220711_1413'), ('tickets', '0020_auto_20220817_1346'), ] diff --git a/apps/assets/api/__init__.py b/apps/assets/api/__init__.py index 67a935f84..186ab10b7 100644 --- a/apps/assets/api/__init__.py +++ b/apps/assets/api/__init__.py @@ -1,6 +1,4 @@ -from .account import * from .asset import * -from .automations import * from .category import * from .domain import * from .favorite_asset import * diff --git a/apps/assets/api/asset/asset.py b/apps/assets/api/asset/asset.py index 9ba11a872..08c877bcb 100644 --- a/apps/assets/api/asset/asset.py +++ b/apps/assets/api/asset/asset.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- # - import django_filters +from django.db.models import Q +from django.utils.translation import ugettext_lazy as _ from rest_framework.decorators import action from rest_framework.response import Response +from accounts.tasks import push_accounts_to_assets, verify_accounts_connectivity from assets import serializers from assets.filters import IpInFilterBackend, LabelFilterBackend, NodeFilterBackend from assets.models import Asset, Gateway from assets.tasks import ( - push_accounts_to_assets, test_assets_connectivity_manual, - update_assets_hardware_info_manual, verify_accounts_connectivity, + test_assets_connectivity_manual, + update_assets_hardware_info_manual ) +from common.api import SuggestionMixin from common.drf.filters import BaseFilterSet -from common.mixins.api import SuggestionMixin from common.utils import get_logger from orgs.mixins import generics from orgs.mixins.api import OrgBulkModelViewSet @@ -21,10 +23,8 @@ from ..mixin import NodeFilterMixin logger = get_logger(__file__) __all__ = [ - "AssetViewSet", - "AssetTaskCreateApi", - "AssetsTaskCreateApi", - 'AssetFilterSet' + "AssetViewSet", "AssetTaskCreateApi", + "AssetsTaskCreateApi", 'AssetFilterSet' ] @@ -32,11 +32,12 @@ class AssetFilterSet(BaseFilterSet): type = django_filters.CharFilter(field_name="platform__type", lookup_expr="exact") category = django_filters.CharFilter(field_name="platform__category", lookup_expr="exact") platform = django_filters.CharFilter(method='filter_platform') + labels = django_filters.CharFilter(method='filter_labels') class Meta: model = Asset fields = [ - "id", "name", "address", "is_active", + "id", "name", "address", "is_active", "labels", "type", "category", "platform" ] @@ -47,12 +48,20 @@ class AssetFilterSet(BaseFilterSet): else: return queryset.filter(platform__name=value) + @staticmethod + def filter_labels(queryset, name, value): + if ':' in value: + n, v = value.split(':', 1) + queryset = queryset.filter(labels__name=n, labels__value=v) + else: + queryset = queryset.filter(Q(labels__name=value) | Q(labels__value=value)) + return queryset + class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ - model = Asset filterset_class = AssetFilterSet search_fields = ("name", "address") @@ -60,7 +69,6 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ordering = ("name",) serializer_classes = ( ("default", serializers.AssetSerializer), - ("retrieve", serializers.AssetDetailSerializer), ("platform", serializers.PlatformSerializer), ("suggestion", serializers.MiniAssetSerializer), ("gateways", serializers.GatewaySerializer), @@ -72,10 +80,18 @@ class AssetViewSet(SuggestionMixin, NodeFilterMixin, OrgBulkModelViewSet): ) extra_filter_backends = [LabelFilterBackend, IpInFilterBackend, NodeFilterBackend] + def get_serializer_class(self): + cls = super().get_serializer_class() + if self.action == "retrieve": + name = cls.__name__.replace("Serializer", "DetailSerializer") + retrieve_cls = type(name, (serializers.DetailMixin, cls), {}) + return retrieve_cls + return cls + @action(methods=["GET"], detail=True, url_path="platform") def platform(self, *args, **kwargs): - asset = self.get_object() - serializer = self.get_serializer(asset.platform) + asset = super().get_object() + serializer = super().get_serializer(instance=asset.platform) return Response(serializer.data) @action(methods=["GET"], detail=True, url_path="gateways") @@ -120,14 +136,14 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): return super().create(request, *args, **kwargs) def check_permissions(self, request): - action = request.data.get("action") action_perm_require = { "refresh": "assets.refresh_assethardwareinfo", - "push_account": "assets.push_assetsystemuser", + "push_account": "accounts.add_pushaccountexecution", "test": "assets.test_assetconnectivity", - "test_account": "assets.test_assetconnectivity", + "test_account": "assets.test_account", } - perm_required = action_perm_require.get(action) + _action = request.data.get("action") + perm_required = action_perm_require.get(_action) has = self.request.user.has_perm(perm_required) if not has: @@ -136,7 +152,7 @@ class AssetTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): @staticmethod def perform_asset_task(serializer): data = serializer.validated_data - if data["action"] not in ["push_system_user", "test_system_user"]: + if data["action"] not in ["push_account", "test_account"]: return asset = data["asset"] @@ -166,11 +182,11 @@ class AssetsTaskCreateApi(AssetsTaskMixin, generics.CreateAPIView): serializer_class = serializers.AssetsTaskSerializer def check_permissions(self, request): - action = request.data.get("action") action_perm_require = { "refresh": "assets.refresh_assethardwareinfo", } - perm_required = action_perm_require.get(action) + _action = request.data.get("action") + perm_required = action_perm_require.get(_action) has = self.request.user.has_perm(perm_required) if not has: self.permission_denied(request) diff --git a/apps/assets/api/automations/change_secret.py b/apps/assets/api/automations/change_secret.py deleted file mode 100644 index 550a3aad0..000000000 --- a/apps/assets/api/automations/change_secret.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# - -from rest_framework import mixins - -from assets import serializers -from assets.models import ChangeSecretAutomation, ChangeSecretRecord, AutomationExecution -from common.utils import get_object_or_none -from orgs.mixins.api import OrgBulkModelViewSet, OrgGenericViewSet -from .base import AutomationExecutionViewSet - -__all__ = [ - 'ChangeSecretAutomationViewSet', 'ChangeSecretRecordViewSet', - 'ChangSecretExecutionViewSet' -] - - -class ChangeSecretAutomationViewSet(OrgBulkModelViewSet): - model = ChangeSecretAutomation - filter_fields = ('name', 'secret_type', 'secret_strategy') - search_fields = filter_fields - ordering_fields = ('name',) - serializer_class = serializers.ChangeSecretAutomationSerializer - - -class ChangeSecretRecordViewSet(mixins.ListModelMixin, OrgGenericViewSet): - serializer_class = serializers.ChangeSecretRecordSerializer - filter_fields = ['asset', 'execution_id'] - search_fields = ['asset__hostname'] - - def get_queryset(self): - return ChangeSecretRecord.objects.all() - - def filter_queryset(self, queryset): - queryset = super().filter_queryset(queryset) - eid = self.request.GET.get('execution_id') - execution = get_object_or_none(AutomationExecution, pk=eid) - if execution: - queryset = queryset.filter(execution=execution) - queryset = queryset.order_by('-date_started') - return queryset - - -class ChangSecretExecutionViewSet(AutomationExecutionViewSet): - rbac_perms = ( - ("list", "assets.view_changesecretexecution"), - ("retrieve", "assets.view_changesecretexecution"), - ("create", "assets.add_changesecretexecution"), - ) diff --git a/apps/assets/api/category.py b/apps/assets/api/category.py index ae44ddc9b..17deba4d3 100644 --- a/apps/assets/api/category.py +++ b/apps/assets/api/category.py @@ -2,7 +2,7 @@ from rest_framework.mixins import ListModelMixin from rest_framework.decorators import action from rest_framework.response import Response -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from assets.serializers import CategorySerializer, TypeSerializer from assets.const import AllTypes diff --git a/apps/assets/api/domain.py b/apps/assets/api/domain.py index 954f4842c..280a8c40c 100644 --- a/apps/assets/api/domain.py +++ b/apps/assets/api/domain.py @@ -1,13 +1,14 @@ # ~*~ coding: utf-8 ~*~ -from django.views.generic.detail import SingleObjectMixin from django.utils.translation import ugettext as _ -from rest_framework.views import APIView, Response +from django.views.generic.detail import SingleObjectMixin from rest_framework.serializers import ValidationError +from rest_framework.views import APIView, Response from common.utils import get_logger +from assets.tasks import test_assets_connectivity_manual from orgs.mixins.api import OrgBulkModelViewSet -from ..models import Domain, Gateway from .. import serializers +from ..models import Domain, Gateway logger = get_logger(__file__) __all__ = ['DomainViewSet', 'GatewayViewSet', "GatewayTestConnectionApi"] @@ -40,7 +41,7 @@ class GatewayViewSet(OrgBulkModelViewSet): class GatewayTestConnectionApi(SingleObjectMixin, APIView): rbac_perms = { - 'POST': 'assets.test_gateway' + 'POST': 'assets.test_assetconnectivity' } def get_queryset(self): @@ -54,8 +55,5 @@ class GatewayTestConnectionApi(SingleObjectMixin, APIView): local_port = int(local_port) except ValueError: raise ValidationError({'port': _('Number required')}) - ok, e = gateway.test_connective(local_port=local_port) - if ok: - return Response("ok") - else: - return Response({"error": e}, status=400) + task = test_assets_connectivity_manual.delay([gateway.id], local_port) + return Response({'task': task.id}) diff --git a/apps/assets/api/node.py b/apps/assets/api/node.py index b45b2170c..bb81a3b45 100644 --- a/apps/assets/api/node.py +++ b/apps/assets/api/node.py @@ -1,24 +1,24 @@ # ~*~ coding: utf-8 ~*~ -from collections import namedtuple, defaultdict from functools import partial +from collections import namedtuple, defaultdict from django.db.models.signals import m2m_changed from django.utils.translation import ugettext_lazy as _ from rest_framework import status +from rest_framework.response import Response from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 -from rest_framework.response import Response from rest_framework.serializers import ValidationError from assets.models import Asset from common.const.http import POST -from common.const.signals import PRE_REMOVE, POST_REMOVE -from common.exceptions import SomeoneIsDoingThis -from common.mixins.api import SuggestionMixin from common.utils import get_logger +from common.api import SuggestionMixin +from common.exceptions import SomeoneIsDoingThis +from common.const.signals import PRE_REMOVE, POST_REMOVE from orgs.mixins import generics -from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import current_org +from orgs.mixins.api import OrgBulkModelViewSet from .. import serializers from ..models import Node from ..tasks import ( @@ -208,8 +208,9 @@ class NodeTaskCreateApi(generics.CreateAPIView): task = self.refresh_nodes_cache() self.set_serializer_data(serializer, task) return + if action == "refresh": - task = update_node_assets_hardware_info_manual.delay(node) + task = update_node_assets_hardware_info_manual.delay(node.id) else: - task = test_node_assets_connectivity_manual.delay(node) + task = test_node_assets_connectivity_manual.delay(node.id) self.set_serializer_data(serializer, task) diff --git a/apps/assets/api/platform.py b/apps/assets/api/platform.py index dbfcc2e4c..2d68b2350 100644 --- a/apps/assets/api/platform.py +++ b/apps/assets/api/platform.py @@ -1,6 +1,6 @@ from jumpserver.utils import has_valid_xpack_license -from common.drf.api import JMSModelViewSet -from common.drf.serializers import GroupedChoiceSerializer +from common.api import JMSModelViewSet +from common.serializers import GroupedChoiceSerializer from assets.models import Platform from assets.const import AllTypes from assets.serializers import PlatformSerializer diff --git a/apps/assets/api/tree.py b/apps/assets/api/tree.py index 2d07d88b0..dcbc191d8 100644 --- a/apps/assets/api/tree.py +++ b/apps/assets/api/tree.py @@ -1,5 +1,6 @@ # ~*~ coding: utf-8 ~*~ +from django.db.models import Q from rest_framework.generics import get_object_or_404 from rest_framework.response import Response @@ -107,25 +108,38 @@ class NodeChildrenAsTreeApi(SerializeToTreeNodeMixin, NodeChildrenApi): model = Node def filter_queryset(self, queryset): + """ queryset is Node queryset """ if not self.request.GET.get('search'): return queryset queryset = super().filter_queryset(queryset) queryset = self.model.get_ancestor_queryset(queryset) return queryset - def list(self, request, *args, **kwargs): - nodes = self.filter_queryset(self.get_queryset()).order_by('value') - nodes = self.serialize_nodes(nodes, with_asset_amount=True) - assets = self.get_assets_as_node() - data = [*nodes, *assets] - return Response(data=data) - - def get_assets_as_node(self): + def get_queryset_for_assets(self): + query_all = self.request.query_params.get("all", "0") == "all" include_assets = self.request.query_params.get('assets', '0') == '1' if not self.instance or not include_assets: return [] - assets = self.instance.get_assets_for_tree() - return self.serialize_assets(assets, self.instance.key) + if query_all: + assets = self.instance.get_all_assets_for_tree() + else: + assets = self.instance.get_assets_for_tree() + return assets + + def filter_queryset_for_assets(self, assets): + search = self.request.query_params.get('search') + if search: + q = Q(name__icontains=search) | Q(address__icontains=search) + assets = assets.filter(q) + return assets + + def list(self, request, *args, **kwargs): + nodes = self.filter_queryset(self.get_queryset()).order_by('value') + nodes = self.serialize_nodes(nodes, with_asset_amount=True) + assets = self.filter_queryset_for_assets(self.get_queryset_for_assets()) + assets = self.serialize_assets(assets, self.instance.key) + data = [*nodes, *assets] + return Response(data=data) class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): @@ -145,9 +159,11 @@ class CategoryTreeApi(SerializeToTreeNodeMixin, generics.ListAPIView): def list(self, request, *args, **kwargs): include_asset = self.request.query_params.get('assets', '0') == '1' + # 资源数量统计可选项 (asset, account) + count_resource = self.request.query_params.get('count_resource', 'asset') if include_asset and self.request.query_params.get('key'): nodes = self.get_assets() else: - nodes = AllTypes.to_tree_nodes(include_asset) + nodes = AllTypes.to_tree_nodes(include_asset, count_resource=count_resource) return Response(data=nodes) diff --git a/apps/assets/automations/base/manager.py b/apps/assets/automations/base/manager.py index b2f76b43d..50a5160e2 100644 --- a/apps/assets/automations/base/manager.py +++ b/apps/assets/automations/base/manager.py @@ -1,7 +1,6 @@ import os import shutil from collections import defaultdict -from copy import deepcopy from hashlib import md5 from socket import gethostname @@ -11,7 +10,6 @@ from django.utils import timezone from django.utils.translation import gettext as _ from assets.automations.methods import platform_automation_methods -from assets.const import SecretType from common.utils import get_logger, lazyproperty from common.utils import ssh_pubkey_gen, ssh_key_string_to_obj from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback @@ -19,50 +17,6 @@ from ops.ansible import JMSInventory, PlaybookRunner, DefaultCallback logger = get_logger(__name__) -class PushOrVerifyHostCallbackMixin: - execution: callable - host_account_mapper: dict - ignore_account: bool - need_privilege_account: bool - generate_public_key: callable - generate_private_key_path: callable - - def host_callback(self, host, asset=None, account=None, automation=None, path_dir=None, **kwargs): - host = super().host_callback(host, asset=asset, account=account, automation=automation, **kwargs) - if host.get('error'): - return host - - accounts = asset.accounts.all() - if self.need_privilege_account and accounts.count() > 1 and account: - accounts = accounts.exclude(id=account.id) - - if '*' not in self.execution.snapshot['accounts']: - accounts = accounts.filter(username__in=self.execution.snapshot['accounts']) - - inventory_hosts = [] - for account in accounts: - h = deepcopy(host) - h['name'] += '_' + account.username - self.host_account_mapper[h['name']] = account - secret = account.secret - - private_key_path = None - if account.secret_type == SecretType.SSH_KEY: - private_key_path = self.generate_private_key_path(secret, path_dir) - secret = self.generate_public_key(secret) - - h['secret_type'] = account.secret_type - h['account'] = { - 'name': account.name, - 'username': account.username, - 'secret_type': account.secret_type, - 'secret': secret, - 'private_key_path': private_key_path - } - inventory_hosts.append(h) - return inventory_hosts - - class PlaybookCallback(DefaultCallback): def playbook_on_stats(self, event_data, **kwargs): super().playbook_on_stats(event_data, **kwargs) @@ -74,10 +28,9 @@ class BasePlaybookManager: def __init__(self, execution): self.execution = execution - self.automation = execution.automation self.method_id_meta_mapper = { method['id']: method - for method in platform_automation_methods + for method in self.platform_automation_methods if method['method'] == self.__class__.method_type() } # 根据执行方式就行分组, 不同资产的改密、推送等操作可能会使用不同的执行方式 @@ -86,17 +39,22 @@ class BasePlaybookManager: self.method_hosts_mapper = defaultdict(list) self.playbooks = [] + @property + def platform_automation_methods(self): + return platform_automation_methods + @classmethod def method_type(cls): raise NotImplementedError def get_assets_group_by_platform(self): - return self.automation.all_assets_group_by_platform() + return self.execution.all_assets_group_by_platform() @lazyproperty def runtime_dir(self): ansible_dir = settings.ANSIBLE_DIR - dir_name = '{}_{}'.format(self.automation.name.replace(' ', '_'), self.execution.id) + task_name = self.execution.snapshot['name'] + dir_name = '{}_{}'.format(task_name.replace(' ', '_'), self.execution.id) path = os.path.join( ansible_dir, 'automations', self.execution.snapshot['type'], dir_name, timezone.now().strftime('%Y%m%d_%H%M%S') @@ -205,9 +163,7 @@ class BasePlaybookManager: print("Runner failed: {} {}".format(e, self)) def before_runner_start(self, runner): - print("Start run task: ") - print(" inventory: {}".format(runner.inventory)) - print(" playbook: {}".format(runner.playbook)) + pass def run(self, *args, **kwargs): runners = self.get_runners() diff --git a/apps/assets/automations/endpoint.py b/apps/assets/automations/endpoint.py index 2548e9184..99feebc49 100644 --- a/apps/assets/automations/endpoint.py +++ b/apps/assets/automations/endpoint.py @@ -1,23 +1,14 @@ -from .change_secret.manager import ChangeSecretManager -from .gather_facts.manager import GatherFactsManager -from .gather_accounts.manager import GatherAccountsManager -from .verify_account.manager import VerifyAccountManager -from .push_account.manager import PushAccountManager -from .backup_account.manager import AccountBackupManager from .ping.manager import PingManager +from .ping_gateway.manager import PingGatewayManager +from .gather_facts.manager import GatherFactsManager from ..const import AutomationTypes class ExecutionManager: manager_type_mapper = { AutomationTypes.ping: PingManager, - AutomationTypes.push_account: PushAccountManager, + AutomationTypes.ping_gateway: PingGatewayManager, AutomationTypes.gather_facts: GatherFactsManager, - AutomationTypes.change_secret: ChangeSecretManager, - AutomationTypes.verify_account: VerifyAccountManager, - AutomationTypes.gather_accounts: GatherAccountsManager, - # TODO 后期迁移到自动化策略中 - 'backup_account': AccountBackupManager, } def __init__(self, execution): diff --git a/apps/assets/automations/gather_facts/database/mysql/manifest.yml b/apps/assets/automations/gather_facts/database/mysql/manifest.yml index 33109b29b..7149e96e9 100644 --- a/apps/assets/automations/gather_facts/database/mysql/manifest.yml +++ b/apps/assets/automations/gather_facts/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Gather facts from MySQL category: database type: - mysql + - mariadb method: gather_facts diff --git a/apps/assets/automations/methods.py b/apps/assets/automations/methods.py index 811d0c77d..86fcb775c 100644 --- a/apps/assets/automations/methods.py +++ b/apps/assets/automations/methods.py @@ -3,8 +3,6 @@ import yaml import json from functools import partial -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) - def check_platform_method(manifest, manifest_path): required_keys = ['category', 'method', 'name', 'id', 'type'] @@ -19,13 +17,13 @@ def check_platform_method(manifest, manifest_path): def check_platform_methods(methods): ids = [m['id'] for m in methods] for i, _id in enumerate(ids): - if _id in ids[i+1:]: + if _id in ids[i + 1:]: raise ValueError("Duplicate id: {}".format(_id)) -def get_platform_automation_methods(): +def get_platform_automation_methods(path): methods = [] - for root, dirs, files in os.walk(BASE_DIR, topdown=False): + for root, dirs, files in os.walk(path, topdown=False): for name in files: path = os.path.join(root, name) if not path.endswith('manifest.yml'): @@ -48,8 +46,8 @@ def filter_key(manifest, attr, value): return value in manifest_value or 'all' in manifest_value -def filter_platform_methods(category, tp, method=None): - methods = platform_automation_methods +def filter_platform_methods(category, tp, method=None, methods=None): + methods = platform_automation_methods if methods is None else methods if category: methods = filter(partial(filter_key, attr='category', value=category), methods) if tp: @@ -59,8 +57,8 @@ def filter_platform_methods(category, tp, method=None): return methods -platform_automation_methods = get_platform_automation_methods() - +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +platform_automation_methods = get_platform_automation_methods(BASE_DIR) if __name__ == '__main__': print(json.dumps(platform_automation_methods, indent=4)) diff --git a/apps/assets/automations/ping/database/mysql/manifest.yml b/apps/assets/automations/ping/database/mysql/manifest.yml index aded00b1f..044907c6a 100644 --- a/apps/assets/automations/ping/database/mysql/manifest.yml +++ b/apps/assets/automations/ping/database/mysql/manifest.yml @@ -3,4 +3,5 @@ name: Ping MySQL category: database type: - mysql + - mariadb method: ping diff --git a/apps/assets/automations/ping_gateway/__init__.py b/apps/assets/automations/ping_gateway/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/assets/automations/ping_gateway/manager.py b/apps/assets/automations/ping_gateway/manager.py new file mode 100644 index 000000000..7e40cda4e --- /dev/null +++ b/apps/assets/automations/ping_gateway/manager.py @@ -0,0 +1,123 @@ +import socket +import paramiko + +from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ + +from common.utils import get_logger +from assets.const import AutomationTypes, Connectivity +from assets.models import Gateway + +logger = get_logger(__name__) + + +class PingGatewayManager: + + def __init__(self, execution): + self.execution = execution + + @classmethod + def method_type(cls): + return AutomationTypes.ping_gateway + + def execute_task(self, gateway, account): + from accounts.models import Account + local_port = self.execution.snapshot.get('local_port') + local_port = gateway.port if local_port is None else local_port + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + proxy = paramiko.SSHClient() + proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + if not isinstance(account, Account): + err = _('No account') + return False, err + + logger.debug('Test account: {}'.format(account)) + try: + proxy.connect( + gateway.address, + port=gateway.port, + username=account.username, + password=account.secret, + pkey=account.private_key_obj + ) + except( + paramiko.AuthenticationException, + paramiko.BadAuthenticationType, + paramiko.SSHException, + paramiko.ChannelException, + paramiko.ssh_exception.NoValidConnectionsError, + socket.gaierror + ) as e: + err = str(e) + if err.startswith('[Errno None] Unable to connect to port'): + err = _('Unable to connect to port {port} on {address}') + err = err.format(port=gateway.port, address=gateway.address) + elif err == 'Authentication failed.': + err = _('Authentication failed') + elif err == 'Connect failed': + err = _('Connect failed') + return False, err + + try: + sock = proxy.get_transport().open_channel( + 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) + ) + client.connect( + '127.0.0.1', + sock=sock, + timeout=5, + port=local_port, + username=account.username, + password=account.secret, + key_filename=account.private_key_path, + ) + except ( + paramiko.SSHException, + paramiko.ssh_exception.SSHException, + paramiko.ChannelException, + paramiko.AuthenticationException, + TimeoutError + ) as e: + + err = getattr(e, 'text', str(e)) + if err == 'Connect failed': + err = _('Connect failed') + return False, err + finally: + client.close() + return True, None + + @staticmethod + def on_host_success(gateway, account): + logger.info('\033[32m {}\033[0m\n'.format(gateway)) + gateway.set_connectivity(Connectivity.OK) + if not account: + return + account.set_connectivity(Connectivity.OK) + + @staticmethod + def on_host_error(gateway, account, error): + logger.info('\033[31m {} 原因: {} \033[0m\n'.format(gateway, error)) + gateway.set_connectivity(Connectivity.FAILED) + if not account: + return + account.set_connectivity(Connectivity.FAILED) + + def run(self): + asset_ids = self.execution.snapshot['assets'] + gateways = Gateway.objects.filter(id__in=asset_ids) + self.execution.date_start = timezone.now() + logger.info(">>> 开始执行测试网关可连接性任务") + for gateway in gateways: + account = gateway.select_account + ok, e = self.execute_task(gateway, account) + if ok: + self.on_host_success(gateway, account) + else: + self.on_host_error(gateway, account, e) + print('\n') + self.execution.status = 'success' + self.execution.date_finished = timezone.now() + self.execution.save() diff --git a/apps/assets/automations/push_account/manager.py b/apps/assets/automations/push_account/manager.py deleted file mode 100644 index f849f3e6e..000000000 --- a/apps/assets/automations/push_account/manager.py +++ /dev/null @@ -1,17 +0,0 @@ -from common.utils import get_logger -from assets.const import AutomationTypes -from ..base.manager import BasePlaybookManager, PushOrVerifyHostCallbackMixin - -logger = get_logger(__name__) - - -class PushAccountManager(PushOrVerifyHostCallbackMixin, BasePlaybookManager): - need_privilege_account = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.host_account_mapper = {} - - @classmethod - def method_type(cls): - return AutomationTypes.push_account diff --git a/apps/assets/const/__init__.py b/apps/assets/const/__init__.py index bc30f388d..9e3f2cbb1 100644 --- a/apps/assets/const/__init__.py +++ b/apps/assets/const/__init__.py @@ -1,7 +1,6 @@ -from .base import * -from .host import * -from .types import * -from .account import * -from .protocol import * -from .category import * from .automation import * +from .base import * +from .category import * +from .host import * +from .protocol import * +from .types import * diff --git a/apps/assets/const/automation.py b/apps/assets/const/automation.py index 72cd5704e..a811cb50b 100644 --- a/apps/assets/const/automation.py +++ b/apps/assets/const/automation.py @@ -1,47 +1,25 @@ from django.db.models import TextChoices from django.utils.translation import ugettext_lazy as _ -string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~' -DEFAULT_PASSWORD_LENGTH = 30 -DEFAULT_PASSWORD_RULES = { - 'length': DEFAULT_PASSWORD_LENGTH, - 'symbol_set': string_punctuation -} + +class Connectivity(TextChoices): + UNKNOWN = 'unknown', _('Unknown') + OK = 'ok', _('Ok') + FAILED = 'failed', _('Failed') class AutomationTypes(TextChoices): ping = 'ping', _('Ping') + ping_gateway = 'ping_gateway', _('Ping gateway') gather_facts = 'gather_facts', _('Gather facts') - push_account = 'push_account', _('Push account') - change_secret = 'change_secret', _('Change secret') - verify_account = 'verify_account', _('Verify account') - gather_accounts = 'gather_accounts', _('Gather accounts') @classmethod def get_type_model(cls, tp): from assets.models import ( PingAutomation, GatherFactsAutomation, - PushAccountAutomation, ChangeSecretAutomation, - VerifyAccountAutomation, GatherAccountsAutomation, ) type_model_dict = { cls.ping: PingAutomation, cls.gather_facts: GatherFactsAutomation, - cls.push_account: PushAccountAutomation, - cls.change_secret: ChangeSecretAutomation, - cls.verify_account: VerifyAccountAutomation, - cls.gather_accounts: GatherAccountsAutomation, } return type_model_dict.get(tp) - - -class SecretStrategy(TextChoices): - custom = 'specific', _('Specific') - random_one = 'random_one', _('All assets use the same random password') - random_all = 'random_all', _('All assets use different random password') - - -class SSHKeyStrategy(TextChoices): - add = 'add', _('Append SSH KEY') - set = 'set', _('Empty and append SSH KEY') - set_jms = 'set_jms', _('Replace (The key generated by JumpServer) ') diff --git a/apps/assets/const/category.py b/apps/assets/const/category.py index 9e76946f3..e14867d69 100644 --- a/apps/assets/const/category.py +++ b/apps/assets/const/category.py @@ -14,6 +14,12 @@ class Category(ChoicesMixin, models.TextChoices): CLOUD = 'cloud', _("Cloud service") WEB = 'web', _("Web") + @classmethod + def filter_choices(cls, category): + _category = getattr(cls, category.upper(), None) + choices = [(_category.value, _category.label)] if _category else cls.choices + return choices + diff --git a/apps/assets/const/cloud.py b/apps/assets/const/cloud.py index 22240cc77..be2637ddf 100644 --- a/apps/assets/const/cloud.py +++ b/apps/assets/const/cloud.py @@ -25,6 +25,7 @@ class CloudTypes(BaseType): 'gather_facts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, + 'push_account_enabled': False, 'gather_accounts_enabled': False, } } diff --git a/apps/assets/const/database.py b/apps/assets/const/database.py index 53c06bff0..05a0afc64 100644 --- a/apps/assets/const/database.py +++ b/apps/assets/const/database.py @@ -29,11 +29,19 @@ class DatabaseTypes(BaseType): 'ansible_config': { 'ansible_connection': 'local', }, + 'ping_enabled': True, 'gather_facts_enabled': True, 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, - } + 'push_account_enabled': True, + }, + cls.REDIS: { + 'push_account_enabled': False, + }, + cls.CLICKHOUSE: { + 'push_account_enabled': False, + }, } return constrains diff --git a/apps/assets/const/device.py b/apps/assets/const/device.py index cbd9d7b27..a1913994c 100644 --- a/apps/assets/const/device.py +++ b/apps/assets/const/device.py @@ -40,6 +40,7 @@ class DeviceTypes(BaseType): 'gather_accounts_enabled': False, 'verify_account_enabled': False, 'change_secret_enabled': False, + 'push_account_enabled': False } } diff --git a/apps/assets/const/host.py b/apps/assets/const/host.py index 91c6b51be..295337b51 100644 --- a/apps/assets/const/host.py +++ b/apps/assets/const/host.py @@ -54,6 +54,7 @@ class HostTypes(BaseType): 'gather_accounts_enabled': True, 'verify_account_enabled': True, 'change_secret_enabled': True, + 'push_account_enabled': True }, cls.WINDOWS: { 'ansible_config': { @@ -74,9 +75,7 @@ class HostTypes(BaseType): {'name': 'Unix'}, {'name': 'macOS'}, {'name': 'BSD'}, - {'name': 'AIX', 'automation': { - 'change_secret_method': 'push_secret_aix' - }} + {'name': 'AIX'}, ], cls.WINDOWS: [ {'name': 'Windows'}, diff --git a/apps/assets/const/types.py b/apps/assets/const/types.py index 1bf61a583..dcdb03ed3 100644 --- a/apps/assets/const/types.py +++ b/apps/assets/const/types.py @@ -27,6 +27,11 @@ class AllTypes(ChoicesMixin): choices.extend(tp.choices) return choices + @classmethod + def filter_choices(cls, category): + choices = dict(cls.category_types()).get(category, cls).choices + return choices() if callable(choices) else choices + @classmethod def get_constraints(cls, category, tp): types_cls = dict(cls.category_types()).get(category) @@ -44,16 +49,25 @@ class AllTypes(ChoicesMixin): return None return constraints.get('protocols')[0]['name'] + @classmethod + def get_automation_methods(cls): + from assets.automations import platform_automation_methods as asset_methods + from accounts.automations import platform_automation_methods as account_methods + return asset_methods + account_methods + @classmethod def set_automation_methods(cls, category, tp, constraints): from assets.automations import filter_platform_methods automation = constraints.get('automation', {}) automation_methods = {} + platform_automation_methods = cls.get_automation_methods() for item, enabled in automation.items(): if not enabled: continue item_name = item.replace('_enabled', '') - methods = filter_platform_methods(category, tp, item_name) + methods = filter_platform_methods( + category, tp, item_name, methods=platform_automation_methods + ) methods = [{'name': m['name'], 'id': m['id']} for m in methods] automation_methods[item_name + '_methods'] = methods automation.update(automation_methods) @@ -162,11 +176,16 @@ class AllTypes(ChoicesMixin): return node @classmethod - def to_tree_nodes(cls, include_asset): + def to_tree_nodes(cls, include_asset, count_resource='asset'): + from accounts.models import Account from ..models import Asset, Platform - asset_platforms = Asset.objects.all().values_list('platform_id', flat=True) + if count_resource == 'account': + resource_platforms = Account.objects.all().values_list('asset__platform_id', flat=True) + else: + resource_platforms = Asset.objects.all().values_list('platform_id', flat=True) + platform_count = defaultdict(int) - for platform_id in asset_platforms: + for platform_id in resource_platforms: platform_count[platform_id] += 1 category_type_mapper = defaultdict(int) diff --git a/apps/assets/filters.py b/apps/assets/filters.py index c60d492e3..a9cb5a614 100644 --- a/apps/assets/filters.py +++ b/apps/assets/filters.py @@ -8,7 +8,7 @@ from rest_framework.compat import coreapi, coreschema from assets.utils import get_node_from_request, is_query_node_all_assets from common.drf.filters import BaseFilterSet -from .models import Account, Label, Node +from .models import Label, Node class AssetByNodeFilterBackend(filters.BaseFilterBackend): @@ -145,39 +145,3 @@ class IpInFilterBackend(filters.BaseFilterBackend): ) ) ] - - -class AccountFilterSet(BaseFilterSet): - ip = drf_filters.CharFilter(field_name='address', lookup_expr='exact') - hostname = drf_filters.CharFilter(field_name='name', lookup_expr='exact') - username = drf_filters.CharFilter(field_name="username", lookup_expr='exact') - address = drf_filters.CharFilter(field_name="asset__address", lookup_expr='exact') - asset = drf_filters.CharFilter(field_name="asset_id", lookup_expr='exact') - assets = drf_filters.CharFilter(field_name='asset_id', lookup_expr='exact') - nodes = drf_filters.CharFilter(method='filter_nodes') - has_secret = drf_filters.BooleanFilter(method='filter_has_secret') - - @staticmethod - def filter_has_secret(queryset, name, has_secret): - q = Q(secret__isnull=True) | Q(secret='') - if has_secret: - return queryset.exclude(q) - else: - return queryset.filter(q) - - @staticmethod - def filter_nodes(queryset, name, value): - nodes = Node.objects.filter(id=value) - if not nodes: - return queryset - - node_qs = Node.objects.none() - for node in nodes: - node_qs |= node.get_all_children(with_self=True) - node_ids = list(node_qs.values_list('id', flat=True)) - queryset = queryset.filter(asset__nodes__in=node_ids) - return queryset - - class Meta: - model = Account - fields = ['id'] diff --git a/apps/assets/migrations/0093_auto_20220403_1627.py b/apps/assets/migrations/0093_auto_20220403_1627.py index 752b3890a..ee536497e 100644 --- a/apps/assets/migrations/0093_auto_20220403_1627.py +++ b/apps/assets/migrations/0093_auto_20220403_1627.py @@ -113,6 +113,11 @@ class Migration(migrations.Migration): models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.asset')), ('db_name', models.CharField(blank=True, max_length=1024, verbose_name='Database')), + ('allow_invalid_cert', models.BooleanField(default=False, verbose_name='Allow invalid cert')), + ('ca_cert', models.TextField(blank=True, verbose_name='CA cert')), + ('client_cert', models.TextField(blank=True, verbose_name='Client cert')), + ('client_key', models.TextField(blank=True, verbose_name='Client key'),), + ('use_ssl', models.BooleanField(default=False, verbose_name='Use SSL'),), ], options={ 'verbose_name': 'Database', diff --git a/apps/assets/migrations/0095_auto_20220407_1726.py b/apps/assets/migrations/0095_auto_20220407_1726.py index 9d9baf42c..fa0798a20 100644 --- a/apps/assets/migrations/0095_auto_20220407_1726.py +++ b/apps/assets/migrations/0095_auto_20220407_1726.py @@ -37,11 +37,6 @@ class Migration(migrations.Migration): name='domain_enabled', field=models.BooleanField(default=True, verbose_name='Domain enabled'), ), - migrations.AddField( - model_name='platform', - name='protocols_enabled', - field=models.BooleanField(default=True, verbose_name='Protocols enabled'), - ), migrations.AddField( model_name='platform', name='su_enabled', diff --git a/apps/assets/migrations/0096_auto_20220426_1550.py b/apps/assets/migrations/0096_auto_20220426_1550.py index 30c4b76be..274a22331 100644 --- a/apps/assets/migrations/0096_auto_20220426_1550.py +++ b/apps/assets/migrations/0096_auto_20220426_1550.py @@ -33,9 +33,12 @@ class Migration(migrations.Migration): ('gather_facts_enabled', models.BooleanField(default=False, verbose_name='Gather facts enabled')), ('gather_facts_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Gather facts method')), - ('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change password enabled')), + ('change_secret_enabled', models.BooleanField(default=False, verbose_name='Change secret enabled')), ('change_secret_method', - models.TextField(blank=True, max_length=32, null=True, verbose_name='Change password method')), + models.TextField(blank=True, max_length=32, null=True, verbose_name='Change secret method')), + ('push_account_enabled', models.BooleanField(default=False, verbose_name='Push account enabled')), + ('push_account_method', + models.TextField(blank=True, max_length=32, null=True, verbose_name='Push account method')), ('verify_account_enabled', models.BooleanField(default=False, verbose_name='Verify account enabled')), ('verify_account_method', models.TextField(blank=True, max_length=32, null=True, verbose_name='Verify account method')), diff --git a/apps/assets/migrations/0097_auto_20220426_1558.py b/apps/assets/migrations/0097_auto_20220426_1558.py index f563d50d7..ec7f64084 100644 --- a/apps/assets/migrations/0097_auto_20220426_1558.py +++ b/apps/assets/migrations/0097_auto_20220426_1558.py @@ -13,6 +13,27 @@ def update_user_platforms(apps, *args): AllTypes.update_user_create_platforms(platform_cls) +def migrate_macos_platform(apps, schema_editor): + db_alias = schema_editor.connection.alias + asset_model = apps.get_model('assets', 'Asset') + platform_model = apps.get_model('assets', 'Platform') + old_macos = platform_model.objects.using(db_alias).filter( + name='MacOS', type='macos' + ).first() + new_macos = platform_model.objects.using(db_alias).filter( + name='macOS', type='unix' + ).first() + + if not old_macos or not new_macos: + return + + asset_model.objects.using(db_alias).filter( + platform=old_macos + ).update(platform=new_macos) + + platform_model.objects.using(db_alias).filter(id=old_macos.id).delete() + + class Migration(migrations.Migration): dependencies = [ ('assets', '0096_auto_20220426_1550'), @@ -21,4 +42,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(create_internal_platforms), migrations.RunPython(update_user_platforms), + migrations.RunPython(migrate_macos_platform), ] diff --git a/apps/assets/migrations/0099_auto_20220711_1409.py b/apps/assets/migrations/0099_auto_20220711_1409.py index 58f917ac4..c0444048f 100644 --- a/apps/assets/migrations/0099_auto_20220711_1409.py +++ b/apps/assets/migrations/0099_auto_20220711_1409.py @@ -1,13 +1,47 @@ # Generated by Django 3.2.12 on 2022-07-11 08:59 -import uuid +import time -import django.db.models.deletion -import simple_history.models from django.conf import settings from django.db import migrations, models -import common.db.fields + +def migrate_asset_protocols(apps, schema_editor): + asset_model = apps.get_model('assets', 'Asset') + protocol_model = apps.get_model('assets', 'Protocol') + + count = 0 + bulk_size = 1000 + print("\n\tStart migrate asset protocols") + while True: + start = time.time() + assets = asset_model.objects.all()[count:count + bulk_size] + if not assets: + break + count += len(assets) + assets_protocols = [] + + for asset in assets: + old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22' + + if ',' in old_protocols: + _protocols = old_protocols.split(',') + else: + _protocols = old_protocols.split() + + for name_port in _protocols: + name_port_list = name_port.split('/') + if len(name_port_list) != 2: + continue + + name, port = name_port_list + protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset}) + assets_protocols.append(protocol) + + protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True) + print("\t - Create asset protocols: {}-{} using: {:.2f}s".format( + count - len(assets), count, time.time() - start + )) class Migration(migrations.Migration): @@ -17,92 +51,21 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name='HistoricalAccount', - fields=[ - ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('version', models.IntegerField(default=0, verbose_name='Version'),), - ('history_id', models.AutoField(primary_key=True, serialize=False)), - ('history_date', models.DateTimeField(db_index=True)), - ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', - models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('history_user', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', - to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'historical Account', - 'verbose_name_plural': 'historical Accounts', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), - }, - bases=(simple_history.models.HistoricalChanges, models.Model), + migrations.RenameField( + model_name='asset', + old_name='protocols', + new_name='_protocols', ), migrations.CreateModel( - name='Account', + name='Protocol', fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('connectivity', models.CharField(choices=[('unknown', 'Unknown'), ('ok', 'Ok'), ('failed', 'Failed')], - default='unknown', max_length=16, verbose_name='Connectivity')), - ('date_verified', models.DateTimeField(null=True, verbose_name='Date verified')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), - ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), - ('version', models.IntegerField(default=0, verbose_name='Version')), - ('asset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', - to='assets.asset', verbose_name='Asset')), + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='Name')), + ('port', models.IntegerField(verbose_name='Port')), + ('asset', + models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', + verbose_name='Asset')), ], - options={ - 'verbose_name': 'Account', - 'permissions': [('view_accountsecret', 'Can view asset account secret'), - ('change_accountsecret', 'Can change asset account secret'), - ('view_historyaccount', 'Can view asset history account'), - ('view_historyaccountsecret', 'Can view asset history account secret')], - 'unique_together': {('name', 'asset'), ('username', 'asset', 'secret_type')}, - }, - ), - migrations.AddField( - model_name='account', - name='su_from', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='su_to', - to='assets.account', verbose_name='Su from'), - ), - migrations.CreateModel( - name='AccountTemplate', - fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('username', models.CharField(blank=True, db_index=True, max_length=128, verbose_name='Username')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'),), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')), - ('privileged', models.BooleanField(default=False, verbose_name='Privileged')), - ], - options={ - 'verbose_name': 'Account template', - 'unique_together': {('name', 'org_id')}, - }, ), + migrations.RunPython(migrate_asset_protocols), ] diff --git a/apps/assets/migrations/0100_auto_20220711_1413.py b/apps/assets/migrations/0100_auto_20220711_1413.py index f45ff27da..62fa1cbd5 100644 --- a/apps/assets/migrations/0100_auto_20220711_1413.py +++ b/apps/assets/migrations/0100_auto_20220711_1413.py @@ -7,7 +7,7 @@ from assets.models import Platform def migrate_accounts(apps, schema_editor): auth_book_model = apps.get_model('assets', 'AuthBook') - account_model = apps.get_model('assets', 'Account') + account_model = apps.get_model('accounts', 'Account') count = 0 bulk_size = 1000 @@ -15,8 +15,8 @@ def migrate_accounts(apps, schema_editor): while True: start = time.time() auth_books = auth_book_model.objects \ - .prefetch_related('systemuser') \ - .all()[count:count+bulk_size] + .prefetch_related('systemuser') \ + .all()[count:count + bulk_size] if not auth_books: break @@ -72,13 +72,13 @@ def migrate_accounts(apps, schema_editor): account_model.objects.bulk_create(accounts, ignore_conflicts=True) print("\t - Create accounts: {}-{} using: {:.2f}s".format( - count - len(auth_books), count, time.time()-start + count - len(auth_books), count, time.time() - start )) class Migration(migrations.Migration): - dependencies = [ + ('accounts', '0001_initial'), ('assets', '0099_auto_20220711_1409'), ] diff --git a/apps/assets/migrations/0101_auto_20220803_1448.py b/apps/assets/migrations/0101_auto_20220803_1448.py deleted file mode 100644 index 9a07c9593..000000000 --- a/apps/assets/migrations/0101_auto_20220803_1448.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 3.2.14 on 2022-08-03 10:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0100_auto_20220711_1413'), - ] - - operations = [ - migrations.RenameField( - model_name='asset', - old_name='protocols', - new_name='_protocols', - ), - migrations.CreateModel( - name='Protocol', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=32, verbose_name='Name')), - ('port', models.IntegerField(verbose_name='Port')), - ('asset', models.ForeignKey(on_delete=models.deletion.CASCADE, related_name='protocols', to='assets.asset', verbose_name='Asset')), - ], - ), - ] diff --git a/apps/assets/migrations/0103_auto_20220811_1511.py b/apps/assets/migrations/0101_auto_20220811_1511.py similarity index 87% rename from apps/assets/migrations/0103_auto_20220811_1511.py rename to apps/assets/migrations/0101_auto_20220811_1511.py index b10455d2f..9ca39774e 100644 --- a/apps/assets/migrations/0103_auto_20220811_1511.py +++ b/apps/assets/migrations/0101_auto_20220811_1511.py @@ -5,16 +5,16 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0102_auto_20220803_1859'), + ('assets', '0100_auto_20220711_1413'), ] operations = [ migrations.AlterField( model_name='asset', name='platform', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assets', to='assets.platform', verbose_name='Platform'), + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='assets', + to='assets.platform', verbose_name='Platform'), ), migrations.RemoveField( model_name='asset', diff --git a/apps/assets/migrations/0102_auto_20220803_1859.py b/apps/assets/migrations/0102_auto_20220803_1859.py deleted file mode 100644 index afaa63d41..000000000 --- a/apps/assets/migrations/0102_auto_20220803_1859.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 3.2.14 on 2022-08-03 10:59 -import time -from django.db import migrations - - -def migrate_asset_protocols(apps, schema_editor): - asset_model = apps.get_model('assets', 'Asset') - protocol_model = apps.get_model('assets', 'Protocol') - - count = 0 - bulk_size = 1000 - print("\n\tStart migrate asset protocols") - while True: - start = time.time() - assets = asset_model.objects.all()[count:count+bulk_size] - if not assets: - break - count += len(assets) - assets_protocols = [] - - for asset in assets: - old_protocols = asset._protocols or '{}/{}'.format(asset.protocol, asset.port) or 'ssh/22' - - if ',' in old_protocols: - _protocols = old_protocols.split(',') - else: - _protocols = old_protocols.split() - - for name_port in _protocols: - name_port_list = name_port.split('/') - if len(name_port_list) != 2: - continue - - name, port = name_port_list - protocol = protocol_model(**{'name': name, 'port': port, 'asset': asset}) - assets_protocols.append(protocol) - - protocol_model.objects.bulk_create(assets_protocols, ignore_conflicts=True) - print("\t - Create asset protocols: {}-{} using: {:.2f}s".format( - count - len(assets), count, time.time()-start - )) - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0101_auto_20220803_1448'), - ] - - operations = [ - migrations.RunPython(migrate_asset_protocols) - ] diff --git a/apps/assets/migrations/0104_auto_20220816_1022.py b/apps/assets/migrations/0102_auto_20220816_1022.py similarity index 94% rename from apps/assets/migrations/0104_auto_20220816_1022.py rename to apps/assets/migrations/0102_auto_20220816_1022.py index 111e4a479..922fb23e9 100644 --- a/apps/assets/migrations/0104_auto_20220816_1022.py +++ b/apps/assets/migrations/0102_auto_20220816_1022.py @@ -13,7 +13,7 @@ def migrate_command_filter_to_assets(apps, schema_editor): while True: start = time.time() command_filters = command_filter_model.objects.all() \ - .prefetch_related('system_users')[count:count + bulk_size] + .prefetch_related('system_users')[count:count + bulk_size] if not command_filters: break count += len(command_filters) @@ -44,9 +44,8 @@ def migrate_command_filter_apps(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('assets', '0103_auto_20220811_1511'), + ('assets', '0101_auto_20220811_1511'), ] operations = [ diff --git a/apps/assets/migrations/0112_gateway_to_asset.py b/apps/assets/migrations/0103_auto_20220902_1021.py similarity index 95% rename from apps/assets/migrations/0112_gateway_to_asset.py rename to apps/assets/migrations/0103_auto_20220902_1021.py index 67c874761..8d324fa12 100644 --- a/apps/assets/migrations/0112_gateway_to_asset.py +++ b/apps/assets/migrations/0103_auto_20220902_1021.py @@ -47,7 +47,7 @@ def migrate_gateway_to_asset(apps, schema_editor): print('>>> migrate gateway to account') accounts = [] - account_model = apps.get_model('assets', 'Account') + account_model = apps.get_model('accounts', 'Account') for gateway in gateways: password = gateway.password private_key = gateway.private_key @@ -66,7 +66,7 @@ def migrate_gateway_to_asset(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('assets', '0111_alter_automationexecution_status'), + ('assets', '0102_auto_20220816_1022'), ] operations = [ @@ -82,6 +82,7 @@ class Migration(migrations.Migration): 'proxy': True, 'indexes': [], 'constraints': [], + 'verbose_name': 'Gateway' }, bases=('assets.host',), ), diff --git a/apps/assets/migrations/0105_auto_20220817_1544.py b/apps/assets/migrations/0104_auto_20220817_1544.py similarity index 97% rename from apps/assets/migrations/0105_auto_20220817_1544.py rename to apps/assets/migrations/0104_auto_20220817_1544.py index 40084ffdd..864eba8f1 100644 --- a/apps/assets/migrations/0105_auto_20220817_1544.py +++ b/apps/assets/migrations/0104_auto_20220817_1544.py @@ -4,9 +4,8 @@ from django.db import migrations class Migration(migrations.Migration): - dependencies = [ - ('assets', '0104_auto_20220816_1022'), + ('assets', '0103_auto_20220902_1021'), ] operations = [ diff --git a/apps/assets/migrations/0115_auto_20221220_1956.py b/apps/assets/migrations/0105_auto_20221220_1956.py similarity index 55% rename from apps/assets/migrations/0115_auto_20221220_1956.py rename to apps/assets/migrations/0105_auto_20221220_1956.py index 976a8c53b..196a33fb8 100644 --- a/apps/assets/migrations/0115_auto_20221220_1956.py +++ b/apps/assets/migrations/0105_auto_20221220_1956.py @@ -4,27 +4,11 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0114_remove_redundant_macos'), + ('assets', '0104_auto_20220817_1544'), ] operations = [ - migrations.AddField( - model_name='accountbackupplan', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='baseautomation', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='changesecretrecord', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), migrations.AddField( model_name='domain', name='created_by', @@ -50,21 +34,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AddField( - model_name='gathereduser', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AddField( - model_name='gathereduser', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AddField( - model_name='gathereduser', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AddField( model_name='node', name='comment', @@ -90,35 +59,30 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='account', + migrations.AddField( + model_name='label', name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), ), - migrations.AlterField( - model_name='account', + migrations.AddField( + model_name='label', + name='date_updated', + field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), + ), + migrations.AddField( + model_name='label', name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), ), migrations.AlterField( - model_name='accountbackupplan', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), + model_name='platformprotocol', + name='default', + field=models.BooleanField(default=False, verbose_name='Default'), ), migrations.AlterField( - model_name='accountbackupplan', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), ), migrations.AlterField( model_name='asset', @@ -130,26 +94,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='baseautomation', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AlterField( - model_name='baseautomation', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='changesecretrecord', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='changesecretrecord', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AlterField( model_name='domain', name='comment', @@ -160,11 +104,6 @@ class Migration(migrations.Migration): name='created_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), ), - migrations.AlterField( - model_name='gathereduser', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), migrations.AlterField( model_name='label', name='comment', @@ -180,4 +119,9 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), + migrations.AlterField( + model_name='gateway', + name='date_created', + field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), + ), ] diff --git a/apps/assets/migrations/0106_auto_20220916_1556.py b/apps/assets/migrations/0106_auto_20220916_1556.py deleted file mode 100644 index 70db1a2f8..000000000 --- a/apps/assets/migrations/0106_auto_20220916_1556.py +++ /dev/null @@ -1,71 +0,0 @@ -# Generated by Django 3.2.13 on 2022-09-16 07:56 -from functools import reduce -from django.db import migrations, models -from assets.const import AllTypes, HostTypes - - -def migrate_backup_types(apps, schema_editor): - all_types = list(reduce( - lambda x, y: x + y, - [ - [j['value'] for j in i['children']] - for i in AllTypes.grouped_choices_to_objs() - ] - )) - asset_types = [i[0] for i in HostTypes.choices] - app_types = list(set(all_types) - set(asset_types)) - - backup_model = apps.get_model("assets", "AccountBackupPlan") - backup_objs = [] - for instance in backup_model.objects.all(): - types = instance.types - if types == 1: - instance.categories = asset_types - elif types == 2: - instance.categories = app_types - elif types == 255: - instance.categories = all_types - else: - instance.categories = [] - backup_objs.append(instance) - backup_model.objects.bulk_update(backup_objs, ['categories']) - - backup_execution_model = apps.get_model("assets", "AccountBackupPlanExecution") - backup_execution_objs = [] - for instance in backup_execution_model.objects.all(): - types = instance.plan_snapshot.get('types', []) - if 'all' in types: - instance.plan_snapshot['categories'] = all_types - elif 'asset' in types: - instance.plan_snapshot['categories'] = asset_types - elif 'application' in types: - instance.plan_snapshot['categories'] = app_types - else: - instance.categories = [] - instance.plan_snapshot.pop('types', None) - backup_execution_objs.append(instance) - backup_execution_model.objects.bulk_update(backup_execution_objs, ['plan_snapshot']) - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0105_auto_20220817_1544'), - ] - - operations = [ - migrations.AlterField( - model_name='accountbackupplan', - name='types', - field=models.BigIntegerField(), - ), - migrations.AddField( - model_name='accountbackupplan', - name='categories', - field=models.JSONField(default=list), - ), - migrations.RunPython(migrate_backup_types), - migrations.RemoveField( - model_name='accountbackupplan', - name='types', - ), - ] diff --git a/apps/assets/migrations/0106_auto_20221228_1838.py b/apps/assets/migrations/0106_auto_20221228_1838.py new file mode 100644 index 000000000..9637bbd1d --- /dev/null +++ b/apps/assets/migrations/0106_auto_20221228_1838.py @@ -0,0 +1,84 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:38 + +import common.db.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + ('tickets', '0025_auto_20221206_1820'), + ] + + operations = [ + migrations.RemoveField( + model_name='accountbackupplanexecution', + name='plan', + ), + migrations.AlterUniqueTogether( + name='commandfilter', + unique_together=None, + ), + migrations.RemoveField( + model_name='commandfilter', + name='assets', + ), + migrations.RemoveField( + model_name='commandfilter', + name='nodes', + ), + migrations.RemoveField( + model_name='commandfilter', + name='user_groups', + ), + migrations.RemoveField( + model_name='commandfilter', + name='users', + ), + migrations.RemoveField( + model_name='commandfilterrule', + name='filter', + ), + migrations.RemoveField( + model_name='commandfilterrule', + name='reviewers', + ), + migrations.RemoveField( + model_name='gathereduser', + name='asset', + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], + 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), + ('test_assetconnectivity', 'Can test asset connectivity'), + ('push_assetaccount', 'Can push account to asset'), + ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), + ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + migrations.AlterUniqueTogether( + name='accountbackupplan', + unique_together=None, + ), + migrations.RemoveField( + model_name='accountbackupplan', + name='recipients', + ), + migrations.DeleteModel( + name='AccountBackupPlanExecution', + ), + migrations.DeleteModel( + name='CommandFilter', + ), + migrations.DeleteModel( + name='CommandFilterRule', + ), + migrations.DeleteModel( + name='GatheredUser', + ), + migrations.DeleteModel( + name='AccountBackupPlan', + ), + ] diff --git a/apps/assets/migrations/0107_auto_20221019_1115.py b/apps/assets/migrations/0107_auto_20221019_1115.py deleted file mode 100644 index 7951f56f1..000000000 --- a/apps/assets/migrations/0107_auto_20221019_1115.py +++ /dev/null @@ -1,175 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-19 03:15 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import common.db.fields - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0106_auto_20220916_1556'), - ] - - operations = [ - migrations.CreateModel( - name='AutomationExecution', - fields=[ - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='pending', max_length=16)), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, - verbose_name='Automation snapshot')), - ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], - default='manual', max_length=128, verbose_name='Trigger mode')), - ], - options={ - 'verbose_name': 'Automation task execution', - }, - ), - migrations.CreateModel( - name='BaseAutomation', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('accounts', models.JSONField(default=list, verbose_name='Accounts')), - ('type', models.CharField(max_length=16, verbose_name='Type')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('comment', models.TextField(blank=True, verbose_name='Comment')), - ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), - ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), - ], - options={ - 'verbose_name': 'Automation task', - 'unique_together': {('org_id', 'name')}, - }, - ), - migrations.AddField( - model_name='label', - name='created_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by'), - ), - migrations.AddField( - model_name='label', - name='date_updated', - field=models.DateTimeField(auto_now=True, verbose_name='Date updated'), - ), - migrations.AddField( - model_name='label', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='platformprotocol', - name='default', - field=models.BooleanField(default=False, verbose_name='Default'), - ), - migrations.CreateModel( - name='GatherFactsAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Gather asset facts', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='PushAccountAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Push asset account', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='VerifyAccountAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Verify asset account', - }, - bases=('assets.baseautomation',), - ), - migrations.CreateModel( - name='ChangeSecretRecord', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('old_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Old secret')), - ('new_secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('date_started', models.DateTimeField(blank=True, null=True, verbose_name='Date started')), - ('date_finished', models.DateTimeField(blank=True, null=True, verbose_name='Date finished')), - ('status', models.CharField(default='pending', max_length=16)), - ('error', models.TextField(blank=True, null=True, verbose_name='Error')), - ('account', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.account')), - ('execution', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='assets.automationexecution')), - ], - options={ - 'verbose_name': 'Change secret record', - }, - ), - migrations.AddField( - model_name='automationexecution', - name='automation', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', - to='assets.baseautomation', verbose_name='Automation task'), - ), - migrations.CreateModel( - name='ChangeSecretAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ('secret_type', models.CharField( - choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), - ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type')), - ('secret_strategy', models.CharField( - choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), - ('random_all', 'All assets use different random password')], default='specific', - max_length=16, verbose_name='Secret strategy')), - ('secret', common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret')), - ('password_rules', models.JSONField(default=dict, verbose_name='Password rules')), - ('ssh_key_change_strategy', models.CharField( - choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), - ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, - verbose_name='SSH key change strategy')), - ('recipients', - models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='Recipient')), - ], - options={ - 'verbose_name': 'Change secret automation', - }, - bases=('assets.baseautomation',), - ), - ] diff --git a/apps/assets/migrations/0107_automation.py b/apps/assets/migrations/0107_automation.py new file mode 100644 index 000000000..5711481e7 --- /dev/null +++ b/apps/assets/migrations/0107_automation.py @@ -0,0 +1,91 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +import common.db.fields +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0106_auto_20221228_1838'), + ] + + operations = [ + migrations.CreateModel( + name='BaseAutomation', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('accounts', models.JSONField(default=list, verbose_name='Accounts')), + ('type', models.CharField(max_length=16, verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('assets', models.ManyToManyField(blank=True, to='assets.Asset', verbose_name='Assets')), + ('nodes', models.ManyToManyField(blank=True, to='assets.Node', verbose_name='Nodes')), + ], + options={ + 'verbose_name': 'Automation task', + 'unique_together': {('org_id', 'name')}, + }, + ), + migrations.CreateModel( + name='AutomationExecution', + fields=[ + ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('snapshot', common.db.fields.EncryptJsonDictTextField(blank=True, default=dict, null=True, verbose_name='Automation snapshot')), + ('trigger', models.CharField(choices=[('manual', 'Manual trigger'), ('timing', 'Timing trigger')], default='manual', max_length=128, verbose_name='Trigger mode')), + ('automation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task')), + ], + options={ + 'verbose_name': 'Automation task execution', + 'ordering': ('-date_start',), + }, + ), + migrations.CreateModel( + name='AssetBaseAutomation', + fields=[ + ], + options={ + 'verbose_name': 'Asset automation task', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('assets.baseautomation',), + ), + migrations.CreateModel( + name='GatherFactsAutomation', + fields=[ + ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Gather asset facts', + }, + bases=('assets.assetbaseautomation',), + ), + migrations.CreateModel( + name='PingAutomation', + fields=[ + ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), + ], + options={ + 'verbose_name': 'Ping asset', + }, + bases=('assets.assetbaseautomation',), + ), + ] diff --git a/apps/assets/migrations/0108_alter_automationexecution_automation.py b/apps/assets/migrations/0108_alter_automationexecution_automation.py new file mode 100644 index 000000000..7fa692b2b --- /dev/null +++ b/apps/assets/migrations/0108_alter_automationexecution_automation.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.16 on 2023-01-05 06:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0107_automation'), + ] + + operations = [ + migrations.AlterField( + model_name='automationexecution', + name='automation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='assets.baseautomation', verbose_name='Automation task'), + ), + ] diff --git a/apps/assets/migrations/0108_auto_20221027_1053.py b/apps/assets/migrations/0108_auto_20221027_1053.py deleted file mode 100644 index 0aa1b5c7a..000000000 --- a/apps/assets/migrations/0108_auto_20221027_1053.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-27 02:53 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0107_auto_20221019_1115'), - ] - - operations = [ - migrations.CreateModel( - name='GatherAccountsAutomation', - fields=[ - ('baseautomation_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Gather asset accounts', - }, - bases=('assets.baseautomation',), - ), - migrations.AlterField( - model_name='baseautomation', - name='type', - field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Create account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), - ), - migrations.CreateModel( - name='PingAutomation', - fields=[ - ('baseautomation_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.baseautomation')), - ], - options={ - 'verbose_name': 'Ping asset', - }, - bases=('assets.baseautomation',), - ), - migrations.AlterModelOptions( - name='asset', - options={'ordering': ['name'], - 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), - ('test_assetconnectivity', 'Can test asset connectivity'), - ('push_assetaccount', 'Can push account to asset'), - ('match_asset', 'Can match asset'), ('add_assettonode', 'Add asset to node'), - ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, - ), - ] diff --git a/apps/assets/migrations/0109_alter_baseautomation_unique_together.py b/apps/assets/migrations/0109_alter_baseautomation_unique_together.py new file mode 100644 index 000000000..a1d141585 --- /dev/null +++ b/apps/assets/migrations/0109_alter_baseautomation_unique_together.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2023-01-06 07:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0108_alter_automationexecution_automation'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='baseautomation', + unique_together={('org_id', 'name', 'type')}, + ), + migrations.AlterModelOptions( + name='asset', + options={'ordering': ['name'], + 'permissions': [('refresh_assethardwareinfo', 'Can refresh asset hardware info'), + ('test_assetconnectivity', 'Can test asset connectivity'), + ('push_assetaccount', 'Can push account to asset'), + ('test_account', 'Can verify account'), ('match_asset', 'Can match asset'), + ('add_assettonode', 'Add asset to node'), + ('move_assettonode', 'Move asset to node')], 'verbose_name': 'Asset'}, + ), + ] diff --git a/apps/assets/migrations/0109_auto_20221102_2017.py b/apps/assets/migrations/0109_auto_20221102_2017.py deleted file mode 100644 index a9dcc4446..000000000 --- a/apps/assets/migrations/0109_auto_20221102_2017.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-02 12:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0108_auto_20221027_1053'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active'), - ), - migrations.AddField( - model_name='account', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='accounttemplate', - name='is_active', - field=models.BooleanField(default=True, verbose_name='Is active'), - ), - migrations.AddField( - model_name='accounttemplate', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AddField( - model_name='gateway', - name='updated_by', - field=models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='account', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - migrations.AlterField( - model_name='accounttemplate', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - migrations.AlterField( - model_name='gateway', - name='date_created', - field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created'), - ), - ] diff --git a/apps/assets/migrations/0110_alter_favoriteasset_options.py b/apps/assets/migrations/0110_alter_favoriteasset_options.py new file mode 100644 index 000000000..0aec8be61 --- /dev/null +++ b/apps/assets/migrations/0110_alter_favoriteasset_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0109_alter_baseautomation_unique_together'), + ] + + operations = [ + migrations.AlterModelOptions( + name='favoriteasset', + options={'verbose_name': 'Favorite Asset'}, + ), + ] diff --git a/apps/assets/migrations/0110_changesecretrecord_asset.py b/apps/assets/migrations/0110_changesecretrecord_asset.py deleted file mode 100644 index 76a3f9872..000000000 --- a/apps/assets/migrations/0110_changesecretrecord_asset.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-03 13:57 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0109_auto_20221102_2017'), - ] - - operations = [ - migrations.RenameField( - model_name='accountbackupplan', - old_name='categories', - new_name='types', - ), - migrations.AddField( - model_name='changesecretrecord', - name='asset', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='assets.asset'), - ), - ] diff --git a/apps/assets/migrations/0111_alter_automationexecution_status.py b/apps/assets/migrations/0111_alter_automationexecution_status.py deleted file mode 100644 index 5ccaf638f..000000000 --- a/apps/assets/migrations/0111_alter_automationexecution_status.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-11 11:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0110_changesecretrecord_asset'), - ] - - operations = [ - migrations.AlterField( - model_name='automationexecution', - name='status', - field=models.CharField(default='pending', max_length=16, verbose_name='Status'), - ), - ] diff --git a/apps/assets/migrations/0113_auto_20221122_2015.py b/apps/assets/migrations/0113_auto_20221122_2015.py deleted file mode 100644 index 9488b5499..000000000 --- a/apps/assets/migrations/0113_auto_20221122_2015.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-28 10:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0112_gateway_to_asset'), - ] - - operations = [ - migrations.AlterModelOptions( - name='accounttemplate', - options={'permissions': [('view_accounttemplatesecret', 'Can view asset account template secret'), - ('change_accounttemplatesecret', 'Can change asset account template secret')], - 'verbose_name': 'Account template'}, - ), - migrations.AddField( - model_name='database', - name='allow_invalid_cert', - field=models.BooleanField(default=False, verbose_name='Allow invalid cert'), - ), - migrations.AddField( - model_name='database', - name='ca_cert', - field=models.TextField(blank=True, verbose_name='CA cert'), - ), - migrations.AddField( - model_name='database', - name='client_cert', - field=models.TextField(blank=True, verbose_name='Client cert'), - ), - migrations.AddField( - model_name='database', - name='client_key', - field=models.TextField(blank=True, verbose_name='Client key'), - ), - migrations.AddField( - model_name='database', - name='use_ssl', - field=models.BooleanField(default=False, verbose_name='Use SSL'), - ), - ] diff --git a/apps/assets/migrations/0114_remove_redundant_macos.py b/apps/assets/migrations/0114_remove_redundant_macos.py deleted file mode 100644 index d24a3e74a..000000000 --- a/apps/assets/migrations/0114_remove_redundant_macos.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-15 07:08 - -from django.db import migrations - - -def migrate_del_macos(apps, schema_editor): - db_alias = schema_editor.connection.alias - asset_model = apps.get_model('assets', 'Asset') - platform_model = apps.get_model('assets', 'Platform') - old_macos = platform_model.objects.using(db_alias).filter( - name='MacOS', type='macos' - ).first() - new_macos = platform_model.objects.using(db_alias).filter( - name='macOS', type='unix' - ).first() - - if not old_macos or not new_macos: - return - - asset_model.objects.using(db_alias).filter( - platform=old_macos - ).update(platform=new_macos) - - platform_model.objects.using(db_alias).filter(id=old_macos.id).delete() - - -class Migration(migrations.Migration): - dependencies = [ - ('assets', '0113_auto_20221122_2015'), - ] - - operations = [ - migrations.RunPython(migrate_del_macos), - ] diff --git a/apps/assets/migrations/0116_alter_automationexecution_options.py b/apps/assets/migrations/0116_alter_automationexecution_options.py deleted file mode 100644 index 75ae53efd..000000000 --- a/apps/assets/migrations/0116_alter_automationexecution_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-22 11:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0115_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterModelOptions( - name='automationexecution', - options={'permissions': [('view_changesecretexecution', 'Can view change secret execution'), ('add_changesecretexection', 'Can add change secret execution'), ('view_gatheraccountsexecution', 'Can view gather accounts execution'), ('add_gatheraccountsexecution', 'Can add gather accounts execution')], 'verbose_name': 'Automation task execution'}, - ), - ] diff --git a/apps/assets/migrations/0117_alter_gateway_options.py b/apps/assets/migrations/0117_alter_gateway_options.py deleted file mode 100644 index 826535b3f..000000000 --- a/apps/assets/migrations/0117_alter_gateway_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0116_alter_automationexecution_options'), - ] - - operations = [ - migrations.AlterModelOptions( - name='gateway', - options={'verbose_name': 'Gateway'}, - ), - ] diff --git a/apps/assets/migrations/0118_auto_20221227_1504.py b/apps/assets/migrations/0118_auto_20221227_1504.py deleted file mode 100644 index 47452733a..000000000 --- a/apps/assets/migrations/0118_auto_20221227_1504.py +++ /dev/null @@ -1,50 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-27 07:04 - -import common.db.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0117_alter_gateway_options'), - ] - - operations = [ - migrations.AddField( - model_name='pushaccountautomation', - name='password_rules', - field=models.JSONField(default=dict, verbose_name='Password rules'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret', - field=common.db.fields.EncryptTextField(blank=True, null=True, verbose_name='Secret'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret_strategy', - field=models.CharField(choices=[('specific', 'Specific'), ('random_one', 'All assets use the same random password'), ('random_all', 'All assets use different random password')], default='specific', max_length=16, verbose_name='Secret strategy'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='secret_type', - field=models.CharField(choices=[('password', 'Password'), ('ssh_key', 'SSH key'), ('access_key', 'Access key'), ('token', 'Token')], default='password', max_length=16, verbose_name='Secret type'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='ssh_key_change_strategy', - field=models.CharField(choices=[('add', 'Append SSH KEY'), ('set', 'Empty and append SSH KEY'), ('set_jms', 'Replace (The key generated by JumpServer) ')], default='add', max_length=16, verbose_name='SSH key change strategy'), - ), - migrations.AddField( - model_name='pushaccountautomation', - name='username', - field=models.CharField(default='', max_length=128, verbose_name='Username'), - preserve_default=False, - ), - migrations.AlterField( - model_name='baseautomation', - name='type', - field=models.CharField(choices=[('ping', 'Ping'), ('gather_facts', 'Gather facts'), ('push_account', 'Push account'), ('change_secret', 'Change secret'), ('verify_account', 'Verify account'), ('gather_accounts', 'Gather accounts')], max_length=16, verbose_name='Type'), - ), - ] diff --git a/apps/assets/migrations/0119_auto_20221227_1740.py b/apps/assets/migrations/0119_auto_20221227_1740.py deleted file mode 100644 index 63048163d..000000000 --- a/apps/assets/migrations/0119_auto_20221227_1740.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-27 09:40 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0118_auto_20221227_1504'), - ] - - operations = [ - migrations.AddField( - model_name='account', - name='source', - field=models.CharField(default='local', max_length=30, verbose_name='Source'), - ), - migrations.DeleteModel( - name='GatheredUser', - ), - ] diff --git a/apps/assets/models/__init__.py b/apps/assets/models/__init__.py index 78e6e579d..5eeaf2626 100644 --- a/apps/assets/models/__init__.py +++ b/apps/assets/models/__init__.py @@ -6,13 +6,9 @@ from .group import * from .gateway import * from .domain import * from .node import * -from .utils import * from .favorite_asset import * -from .account import * -from .backup import * from .automations import * -from ._user import * # 废弃以下 # from ._authbook import * -from .cmd_filter import * - +# from .cmd_filter import * +from ._user import * diff --git a/apps/assets/models/asset/common.py b/apps/assets/models/asset/common.py index 6ef10b019..afe905ac3 100644 --- a/apps/assets/models/asset/common.py +++ b/apps/assets/models/asset/common.py @@ -2,12 +2,14 @@ # -*- coding: utf-8 -*- # +import json import logging from collections import defaultdict from django.db import models from django.utils.translation import ugettext_lazy as _ +from assets import const from common.utils import lazyproperty from orgs.mixins.models import OrgManager, JMSOrgBaseModel from ..base import AbsConnectivity @@ -98,6 +100,9 @@ class Protocol(models.Model): class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): + Category = const.Category + Type = const.AllTypes + name = models.CharField(max_length=128, verbose_name=_('Name')) address = models.CharField(max_length=128, verbose_name=_('IP'), db_index=True) platform = models.ForeignKey(Platform, on_delete=models.PROTECT, verbose_name=_("Platform"), related_name='assets') @@ -116,11 +121,53 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): @property def specific(self): - if not hasattr(self, self.category): + instance = getattr(self, self.category, None) + if not instance: return {} - instance = getattr(self, self.category) - private_fields = [i.name for i in instance._meta.local_fields if i.name != 'asset_ptr'] - return {i: getattr(instance, i) for i in private_fields} + specific_fields = self.get_specific_fields(instance) + info = {} + for i in specific_fields: + v = getattr(instance, i.name) + if isinstance(i, models.JSONField) and not isinstance(v, (list, dict)): + v = json.loads(v) + info[i.name] = v + return info + + @property + def spec_info(self): + instance = getattr(self, self.category, None) + if not instance: + return [] + specific_fields = self.get_specific_fields(instance) + info = [ + { + 'label': i.verbose_name, + 'name': i.name, + 'value': getattr(instance, i.name) + } + for i in specific_fields + ] + return info + + @lazyproperty + def enabled_info(self): + platform = self.platform + automation = self.platform.automation + return { + 'su_enabled': platform.su_enabled, + 'ping_enabled': automation.ping_enabled, + 'domain_enabled': platform.domain_enabled, + 'ansible_enabled': automation.ansible_enabled, + 'gather_facts_enabled': automation.gather_facts_enabled, + 'change_secret_enabled': automation.change_secret_enabled, + 'verify_account_enabled': automation.verify_account_enabled, + 'gather_accounts_enabled': automation.gather_accounts_enabled, + } + + @staticmethod + def get_specific_fields(instance): + specific_fields = [i for i in instance._meta.local_fields if i.name != 'asset_ptr'] + return specific_fields def get_target_ip(self): return self.address @@ -177,6 +224,18 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): def category(self): return self.platform.category + def is_category(self, category): + return self.category == category + + def is_type(self, tp): + return self.type == tp + + @lazyproperty + def gateway(self): + if self.domain_id: + return self.domain.select_gateway() + return None + def as_node(self): from assets.models import Node fake_node = Node() @@ -224,6 +283,7 @@ class Asset(NodesRelationMixin, AbsConnectivity, JMSOrgBaseModel): ('refresh_assethardwareinfo', _('Can refresh asset hardware info')), ('test_assetconnectivity', _('Can test asset connectivity')), ('push_assetaccount', _('Can push account to asset')), + ('test_account', _('Can verify account')), ('match_asset', _('Can match asset')), ('add_assettonode', _('Add asset to node')), ('move_assettonode', _('Move asset to node')), diff --git a/apps/assets/models/asset/database.py b/apps/assets/models/asset/database.py index 4772a6b08..2c033de9e 100644 --- a/apps/assets/models/asset/database.py +++ b/apps/assets/models/asset/database.py @@ -15,20 +15,17 @@ class Database(Asset): def __str__(self): return '{}({}://{}/{})'.format(self.name, self.type, self.address, self.db_name) - @property - def ip(self): - return self.address - @property def specific(self): return { 'db_name': self.db_name, 'use_ssl': self.use_ssl, - 'ca_cert': self.ca_cert, - 'client_cert': self.client_cert, - 'client_key': self.client_key, 'allow_invalid_cert': self.allow_invalid_cert, } + @property + def ip(self): + return self.address + class Meta: verbose_name = _("Database") diff --git a/apps/assets/models/automations/__init__.py b/apps/assets/models/automations/__init__.py index abf23ed7e..f6665cfbb 100644 --- a/apps/assets/models/automations/__init__.py +++ b/apps/assets/models/automations/__init__.py @@ -1,7 +1,3 @@ -from .ping import * from .base import * -from .push_account import * from .gather_facts import * -from .change_secret import * -from .verify_account import * -from .gather_accounts import * +from .ping import * diff --git a/apps/assets/models/automations/base.py b/apps/assets/models/automations/base.py index 6504e5898..e888fdf26 100644 --- a/apps/assets/models/automations/base.py +++ b/apps/assets/models/automations/base.py @@ -4,12 +4,12 @@ from celery import current_task from django.db import models from django.utils.translation import ugettext_lazy as _ -from assets.const import AutomationTypes -from assets.models import Node, Asset +from assets.models.node import Node +from assets.models.asset import Asset from assets.tasks import execute_automation +from ops.mixin import PeriodTaskModelMixin from common.const.choices import Trigger from common.db.fields import EncryptJsonDictTextField -from ops.mixin import PeriodTaskModelMixin from orgs.mixins.models import OrgModelMixin, JMSOrgBaseModel @@ -17,12 +17,16 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): accounts = models.JSONField(default=list, verbose_name=_("Accounts")) nodes = models.ManyToManyField('assets.Node', blank=True, verbose_name=_("Nodes")) assets = models.ManyToManyField('assets.Asset', blank=True, verbose_name=_("Assets")) - type = models.CharField(max_length=16, choices=AutomationTypes.choices, verbose_name=_('Type')) + type = models.CharField(max_length=16, verbose_name=_('Type')) is_active = models.BooleanField(default=True, verbose_name=_("Is active")) def __str__(self): return self.name + '@' + str(self.created_by) + class Meta: + unique_together = [('org_id', 'name', 'type')] + verbose_name = _("Automation task") + @classmethod def generate_unique_name(cls, name): while True: @@ -57,35 +61,45 @@ class BaseAutomation(PeriodTaskModelMixin, JMSOrgBaseModel): return { 'name': self.name, 'type': self.type, - 'org_id': str(self.org_id), 'comment': self.comment, 'accounts': self.accounts, + 'org_id': str(self.org_id), 'nodes': self.get_many_to_many_ids('nodes'), 'assets': self.get_many_to_many_ids('assets'), } + @property + def execution_model(self): + return AutomationExecution + + @property + def executed_amount(self): + return self.executions.count() + def execute(self, trigger=Trigger.manual): try: eid = current_task.request.id except AttributeError: eid = str(uuid.uuid4()) - execution = self.executions.model.objects.create( + execution = self.execution_model.objects.create( id=eid, trigger=trigger, automation=self, snapshot=self.to_attr_json(), ) return execution.start() + +class AssetBaseAutomation(BaseAutomation): class Meta: - unique_together = [('org_id', 'name')] - verbose_name = _("Automation task") + proxy = True + verbose_name = _("Asset automation task") class AutomationExecution(OrgModelMixin): id = models.UUIDField(default=uuid.uuid4, primary_key=True) automation = models.ForeignKey( 'BaseAutomation', related_name='executions', on_delete=models.CASCADE, - verbose_name=_('Automation task') + verbose_name=_('Automation task'), null=True ) status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) date_created = models.DateTimeField(auto_now_add=True, verbose_name=_('Date created')) @@ -100,24 +114,31 @@ class AutomationExecution(OrgModelMixin): ) class Meta: + ordering = ('-date_start',) verbose_name = _('Automation task execution') - permissions = [ - ('view_changesecretexecution', _('Can view change secret execution')), - ('add_changesecretexection', _('Can add change secret execution')), - ('view_gatheraccountsexecution', _('Can view gather accounts execution')), - ('add_gatheraccountsexecution', _('Can add gather accounts execution')), - ] @property def manager_type(self): return self.snapshot['type'] + def get_all_assets(self): + node_ids = self.snapshot['nodes'] + asset_ids = self.snapshot['assets'] + nodes = Node.objects.filter(id__in=node_ids) + node_asset_ids = Node.get_nodes_all_assets(*nodes).values_list('id', flat=True) + asset_ids = set(list(asset_ids) + list(node_asset_ids)) + return Asset.objects.filter(id__in=asset_ids) + + def all_assets_group_by_platform(self): + assets = self.get_all_assets().prefetch_related('platform') + return assets.group_by_platform() + @property def recipients(self): recipients = self.snapshot.get('recipients') if not recipients: - return [] - return recipients.values() + return {} + return recipients def start(self): from assets.automations.endpoint import ExecutionManager diff --git a/apps/assets/models/automations/gather_facts.py b/apps/assets/models/automations/gather_facts.py index 1641c9f81..cf11ea41b 100644 --- a/apps/assets/models/automations/gather_facts.py +++ b/apps/assets/models/automations/gather_facts.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from assets.const import AutomationTypes -from .base import BaseAutomation +from .base import AssetBaseAutomation __all__ = ['GatherFactsAutomation'] -class GatherFactsAutomation(BaseAutomation): +class GatherFactsAutomation(AssetBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.gather_facts super().save(*args, **kwargs) diff --git a/apps/assets/models/automations/ping.py b/apps/assets/models/automations/ping.py index b327bc4ea..a8df4e7c8 100644 --- a/apps/assets/models/automations/ping.py +++ b/apps/assets/models/automations/ping.py @@ -1,12 +1,12 @@ from django.utils.translation import ugettext_lazy as _ from assets.const import AutomationTypes -from .base import BaseAutomation +from .base import AssetBaseAutomation __all__ = ['PingAutomation'] -class PingAutomation(BaseAutomation): +class PingAutomation(AssetBaseAutomation): def save(self, *args, **kwargs): self.type = AutomationTypes.ping super().save(*args, **kwargs) diff --git a/apps/assets/models/automations/push_account.py b/apps/assets/models/automations/push_account.py deleted file mode 100644 index 33972eb58..000000000 --- a/apps/assets/models/automations/push_account.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import models -from django.utils.translation import ugettext_lazy as _ - -from assets.const import AutomationTypes -from .base import BaseAutomation -from .change_secret import ChangeSecretMixin - -__all__ = ['PushAccountAutomation'] - - -class PushAccountAutomation(BaseAutomation, ChangeSecretMixin): - accounts = None - username = models.CharField(max_length=128, verbose_name=_('Username')) - - def save(self, *args, **kwargs): - self.type = AutomationTypes.push_account - super().save(*args, **kwargs) - - class Meta: - verbose_name = _("Push asset account") diff --git a/apps/assets/models/base.py b/apps/assets/models/base.py index 8c8c08555..8f5ed713f 100644 --- a/apps/assets/models/base.py +++ b/apps/assets/models/base.py @@ -1,21 +1,14 @@ # -*- coding: utf-8 -*- # -import os -from hashlib import md5 -import sshpubkeys -from django.conf import settings from django.db import models from django.utils import timezone from django.utils.translation import ugettext_lazy as _ -from assets.const import Connectivity, SecretType -from common.db import fields +from assets.const import Connectivity from common.utils import ( - ssh_key_string_to_obj, ssh_key_gen, get_logger, - random_string, lazyproperty, parse_ssh_public_key_str + get_logger ) -from orgs.mixins.models import JMSOrgBaseModel logger = get_logger(__file__) @@ -48,130 +41,3 @@ class AbsConnectivity(models.Model): class Meta: abstract = True - - -class BaseAccountQuerySet(models.QuerySet): - def active(self): - return self.filter(is_active=True) - - -class BaseAccountManager(models.Manager): - def active(self): - return self.get_queryset().active() - - -class BaseAccount(JMSOrgBaseModel): - name = models.CharField(max_length=128, verbose_name=_("Name")) - username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), db_index=True) - secret_type = models.CharField( - max_length=16, choices=SecretType.choices, default=SecretType.PASSWORD, verbose_name=_('Secret type') - ) - secret = fields.EncryptTextField(blank=True, null=True, verbose_name=_('Secret')) - privileged = models.BooleanField(verbose_name=_("Privileged"), default=False) - is_active = models.BooleanField(default=True, verbose_name=_("Is active")) - - objects = BaseAccountManager.from_queryset(BaseAccountQuerySet)() - - @property - def has_secret(self): - return bool(self.secret) - - @property - def has_username(self): - return bool(self.username) - - @property - def specific(self): - data = {} - if self.secret_type != SecretType.SSH_KEY: - return data - data['ssh_key_fingerprint'] = self.ssh_key_fingerprint - return data - - @property - def private_key(self): - if self.secret_type == SecretType.SSH_KEY: - return self.secret - return None - - @private_key.setter - def private_key(self, value): - self.secret = value - self.secret_type = SecretType.SSH_KEY - - @lazyproperty - def public_key(self): - if self.secret_type == SecretType.SSH_KEY and self.private_key: - return parse_ssh_public_key_str(self.private_key) - return None - - @property - def ssh_key_fingerprint(self): - if self.public_key: - public_key = self.public_key - elif self.private_key: - try: - public_key = parse_ssh_public_key_str(self.private_key) - except IOError as e: - return str(e) - else: - return '' - - public_key_obj = sshpubkeys.SSHKey(public_key) - fingerprint = public_key_obj.hash_md5() - return fingerprint - - @property - def private_key_obj(self): - if self.private_key: - key_obj = ssh_key_string_to_obj(self.private_key) - return key_obj - else: - return None - - @property - def private_key_path(self): - if not self.secret_type != SecretType.SSH_KEY or not self.secret: - return None - project_dir = settings.PROJECT_DIR - tmp_dir = os.path.join(project_dir, 'tmp') - key_name = '.' + md5(self.private_key.encode('utf-8')).hexdigest() - key_path = os.path.join(tmp_dir, key_name) - if not os.path.exists(key_path): - self.private_key_obj.write_private_key_file(key_path) - os.chmod(key_path, 0o400) - return key_path - - def get_private_key(self): - if not self.private_key: - return None - return self.private_key - - @property - def public_key_obj(self): - if self.public_key: - try: - return sshpubkeys.SSHKey(self.public_key) - except TabError: - pass - return None - - @staticmethod - def gen_password(length=36): - return random_string(length, special_char=True) - - @staticmethod - def gen_key(username): - private_key, public_key = ssh_key_gen(username=username) - return private_key, public_key - - def _to_secret_json(self): - """Push system user use it""" - return { - 'name': self.name, - 'username': self.username, - 'public_key': self.public_key, - } - - class Meta: - abstract = True diff --git a/apps/assets/models/domain.py b/apps/assets/models/domain.py index 4e2cfc71d..dfb0e32fe 100644 --- a/apps/assets/models/domain.py +++ b/apps/assets/models/domain.py @@ -5,7 +5,7 @@ import random from django.db import models from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty +from common.utils import get_logger from orgs.mixins.models import JMSOrgBaseModel from .gateway import Gateway @@ -31,17 +31,21 @@ class Domain(JMSOrgBaseModel): def random_gateway(self): gateways = [gw for gw in self.active_gateways if gw.is_connective] if not gateways: - gateways = self.active_gateways logger.warn(f'Gateway all bad. domain={self}, gateway_num={len(gateways)}.') + gateways = self.active_gateways + if not gateways: + logger.warn(f'Not active gateway. domain={self}') + return None return random.choice(gateways) - @lazyproperty + @property def active_gateways(self): return self.gateways.filter(is_active=True) - @lazyproperty + @property def gateways(self): - return self.get_gateway_queryset().filter(domain=self) + queryset = self.get_gateway_queryset().filter(domain=self) + return queryset @classmethod def get_gateway_queryset(cls): diff --git a/apps/assets/models/favorite_asset.py b/apps/assets/models/favorite_asset.py index 8fbaeed64..86d64bbe1 100644 --- a/apps/assets/models/favorite_asset.py +++ b/apps/assets/models/favorite_asset.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.db import models +from django.utils.translation import ugettext_lazy as _ from common.db.models import JMSBaseModel @@ -13,7 +14,11 @@ class FavoriteAsset(JMSBaseModel): class Meta: unique_together = ('user', 'asset') + verbose_name = _("Favorite Asset") @classmethod def get_user_favorite_asset_ids(cls, user): return cls.objects.filter(user=user).values_list('asset', flat=True) + + def __str__(self): + return '%s' % self.asset diff --git a/apps/assets/models/gateway.py b/apps/assets/models/gateway.py index f94ae826b..d9d4d891b 100644 --- a/apps/assets/models/gateway.py +++ b/apps/assets/models/gateway.py @@ -1,16 +1,11 @@ # -*- coding: utf-8 -*- # -import socket -import paramiko - from django.utils.translation import ugettext_lazy as _ -from common.utils import get_logger, lazyproperty from orgs.mixins.models import OrgManager - -from assets.const import GATEWAY_NAME, Connectivity from assets.models.platform import Platform -from assets.models.account import Account +from assets.const import GATEWAY_NAME +from common.utils import get_logger, lazyproperty from .asset.host import Host @@ -52,71 +47,22 @@ class Gateway(Host): account = self.accounts.active().order_by('-privileged', '-date_updated').first() return account - def test_connective(self, local_port=None): - local_port = self.port if local_port is None else local_port - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - proxy = paramiko.SSHClient() - proxy.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - if not isinstance(self.select_account, Account): - err = _('No account') - return False, err + @lazyproperty + def username(self): + account = self.select_account + return account.username if account else None - logger.debug('Test account: {}'.format(self.select_account)) - try: - proxy.connect( - self.address, - port=self.port, - username=self.select_account.username, - password=self.select_account.secret, - pkey=self.select_account.private_key_obj - ) - except( - paramiko.AuthenticationException, - paramiko.BadAuthenticationType, - paramiko.SSHException, - paramiko.ChannelException, - paramiko.ssh_exception.NoValidConnectionsError, - socket.gaierror - ) as e: - err = str(e) - if err.startswith('[Errno None] Unable to connect to port'): - err = _('Unable to connect to port {port} on {address}') - err = err.format(port=self.port, address=self.address) - elif err == 'Authentication failed.': - err = _('Authentication failed') - elif err == 'Connect failed': - err = _('Connect failed') - self.set_connectivity(Connectivity.FAILED) - return False, err + @lazyproperty + def password(self): + account = self.select_account + return account.password if account else None - try: - sock = proxy.get_transport().open_channel( - 'direct-tcpip', ('127.0.0.1', local_port), ('127.0.0.1', 0) - ) - client.connect( - '127.0.0.1', - sock=sock, - timeout=5, - port=local_port, - username=self.select_account.username, - password=self.select_account.secret, - key_filename=self.select_account.private_key_path, - ) - except ( - paramiko.SSHException, - paramiko.ssh_exception.SSHException, - paramiko.ChannelException, - paramiko.AuthenticationException, - TimeoutError - ) as e: + @lazyproperty + def private_key(self): + account = self.select_account + return account.private_key if account else None - err = getattr(e, 'text', str(e)) - if err == 'Connect failed': - err = _('Connect failed') - self.set_connectivity(Connectivity.FAILED) - return False, err - finally: - client.close() - self.set_connectivity(Connectivity.OK) - return True, None + @lazyproperty + def private_key_path(self): + account = self.select_account + return account.private_key_path if account else None diff --git a/apps/assets/models/node.py b/apps/assets/models/node.py index 626f931d0..5d1e633f5 100644 --- a/apps/assets/models/node.py +++ b/apps/assets/models/node.py @@ -436,6 +436,12 @@ class NodeAssetsMixin(NodeAllAssetsMappingMixin): "org_id", "is_active" ).prefetch_related('platform') + def get_all_assets_for_tree(self): + return self.get_all_assets().only( + "id", "name", "address", "platform_id", + "org_id", "is_active" + ).prefetch_related('platform') + def get_valid_assets(self): return self.get_assets().valid() diff --git a/apps/assets/models/platform.py b/apps/assets/models/platform.py index c013dd2d6..1bb560e7e 100644 --- a/apps/assets/models/platform.py +++ b/apps/assets/models/platform.py @@ -44,9 +44,14 @@ class PlatformAutomation(models.Model): ping_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Ping method")) gather_facts_enabled = models.BooleanField(default=False, verbose_name=_("Gather facts enabled")) gather_facts_method = models.TextField(max_length=32, blank=True, null=True, verbose_name=_("Gather facts method")) - change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change password enabled")) + change_secret_enabled = models.BooleanField(default=False, verbose_name=_("Change secret enabled")) change_secret_method = models.TextField( - max_length=32, blank=True, null=True, verbose_name=_("Change password method")) + max_length=32, blank=True, null=True, verbose_name=_("Change secret method") + ) + push_account_enabled = models.BooleanField(default=False, verbose_name=_("Push account enabled")) + push_account_method = models.TextField( + max_length=32, blank=True, null=True, verbose_name=_("Push account method") + ) verify_account_enabled = models.BooleanField(default=False, verbose_name=_("Verify account enabled")) verify_account_method = models.TextField( max_length=32, blank=True, null=True, verbose_name=_("Verify account method")) @@ -77,7 +82,6 @@ class Platform(models.Model): default=CharsetChoices.utf8, choices=CharsetChoices.choices, max_length=8, verbose_name=_("Charset") ) domain_enabled = models.BooleanField(default=True, verbose_name=_("Domain enabled")) - protocols_enabled = models.BooleanField(default=True, verbose_name=_("Protocols enabled")) # 账号有关的 su_enabled = models.BooleanField(default=False, verbose_name=_("Su enabled")) su_method = models.CharField(max_length=32, blank=True, null=True, verbose_name=_("Su method")) diff --git a/apps/assets/models/utils.py b/apps/assets/models/utils.py index 891c5b9d6..a5e4da1da 100644 --- a/apps/assets/models/utils.py +++ b/apps/assets/models/utils.py @@ -18,64 +18,3 @@ def private_key_validator(value): _('%(value)s is not an even number'), params={'value': value}, ) - - -def update_internal_platforms(platform_model): - platforms = [ - {'name': 'Linux', 'category': 'host', 'type': 'linux'}, - {'name': 'BSD', 'category': 'host', 'type': 'unix'}, - {'name': 'Unix', 'category': 'host', 'type': 'unix'}, - {'name': 'MacOS', 'category': 'host', 'type': 'unix'}, - {'name': 'Windows', 'category': 'host', 'type': 'unix'}, - { - 'name': 'AIX', 'category': 'host', 'type': 'unix', - 'change_secret_method': 'change_secret_aix', - }, - {'name': 'Windows', 'category': 'host', 'type': 'windows'}, - { - 'name': 'Windows-TLS', 'category': 'host', 'type': 'windows', - 'protocols': [ - {'name': 'rdp', 'port': 3389, 'setting': {'security': 'tls'}}, - {'name': 'ssh', 'port': 22}, - ] - }, - { - 'name': 'Windows-RDP', 'category': 'host', 'type': 'windows', - 'protocols': [ - {'name': 'rdp', 'port': 3389, 'setting': {'security': 'rdp'}}, - {'name': 'ssh', 'port': 22}, - ] - }, - # 数据库 - {'name': 'MySQL', 'category': 'database', 'type': 'mysql'}, - {'name': 'PostgreSQL', 'category': 'database', 'type': 'postgresql'}, - {'name': 'Oracle', 'category': 'database', 'type': 'oracle'}, - {'name': 'SQLServer', 'category': 'database', 'type': 'sqlserver'}, - {'name': 'MongoDB', 'category': 'database', 'type': 'mongodb'}, - {'name': 'Redis', 'category': 'database', 'type': 'redis'}, - - # 网络设备 - {'name': 'Generic', 'category': 'device', 'type': 'general'}, - {'name': 'Huawei', 'category': 'device', 'type': 'general'}, - {'name': 'Cisco', 'category': 'device', 'type': 'general'}, - {'name': 'H3C', 'category': 'device', 'type': 'general'}, - - # Web - {'name': 'Website', 'category': 'web', 'type': 'general'}, - - # Cloud - {'name': 'Kubernetes', 'category': 'cloud', 'type': 'k8s'}, - {'name': 'VMware vSphere', 'category': 'cloud', 'type': 'private'}, - ] - - platforms = platform_model.objects.all() - - updated = [] - for p in platforms: - attrs = platform_ops_map.get((p.category, p.type), {}) - if not attrs: - continue - for k, v in attrs.items(): - setattr(p, k, v) - updated.append(p) - platform_model.objects.bulk_update(updated, list(default_ok.keys())) diff --git a/apps/assets/serializers/__init__.py b/apps/assets/serializers/__init__.py index f70a94bbf..cbf21454d 100644 --- a/apps/assets/serializers/__init__.py +++ b/apps/assets/serializers/__init__.py @@ -7,7 +7,6 @@ from .node import * from .gateway import * from .domain import * from .favorite_asset import * -from .account import * from .platform import * from .cagegory import * from .automations import * diff --git a/apps/assets/serializers/account/base.py b/apps/assets/serializers/account/base.py deleted file mode 100644 index becfd4df9..000000000 --- a/apps/assets/serializers/account/base.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - -from assets.models import BaseAccount -from assets.serializers.base import AuthValidateMixin -from orgs.mixins.serializers import BulkOrgResourceModelSerializer - -__all__ = ['BaseAccountSerializer'] - - -class BaseAccountSerializer(AuthValidateMixin, BulkOrgResourceModelSerializer): - has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True) - - class Meta: - model = BaseAccount - fields_mini = ['id', 'name', 'username'] - fields_small = fields_mini + [ - 'secret_type', 'secret', 'has_secret', 'passphrase', - 'privileged', 'is_active', 'specific', - ] - fields_other = ['created_by', 'date_created', 'date_updated', 'comment'] - fields = fields_small + fields_other - read_only_fields = [ - 'has_secret', 'specific', - 'date_verified', 'created_by', 'date_created', - ] - extra_kwargs = { - 'specific': {'label': _('Specific')}, - } diff --git a/apps/assets/serializers/asset/common.py b/apps/assets/serializers/asset/common.py index e2a84fe5a..30adcbe78 100644 --- a/apps/assets/serializers/asset/common.py +++ b/apps/assets/serializers/asset/common.py @@ -6,24 +6,25 @@ from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField, ObjectRelatedField -from common.drf.serializers import WritableNestedModelSerializer -from orgs.mixins.serializers import BulkOrgResourceSerializerMixin -from ..account import AccountSerializer +from accounts.models import Account, AccountTemplate +from common.serializers import WritableNestedModelSerializer, SecretReadableMixin, CommonModelSerializer +from common.serializers.fields import LabeledChoiceField +from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ...const import Category, AllTypes -from ...models import Asset, Node, Platform, Label, Domain, Account, Protocol +from ...models import Asset, Node, Platform, Label, Protocol __all__ = [ 'AssetSerializer', 'AssetSimpleSerializer', 'MiniAssetSerializer', 'AssetTaskSerializer', 'AssetsTaskSerializer', 'AssetProtocolsSerializer', - 'AssetDetailSerializer', + 'AssetDetailSerializer', 'DetailMixin', 'AssetAccountSerializer', + 'AccountSecretSerializer' ] class AssetProtocolsSerializer(serializers.ModelSerializer): class Meta: model = Protocol - fields = ['id', 'name', 'port'] + fields = ['name', 'port'] class AssetLabelSerializer(serializers.ModelSerializer): @@ -45,10 +46,14 @@ class AssetPlatformSerializer(serializers.ModelSerializer): } -class AssetAccountSerializer(AccountSerializer): +class AssetAccountSerializer(CommonModelSerializer): add_org_fields = False + push_now = serializers.BooleanField( + default=False, label=_("Push now"), write_only=True + ) - class Meta(AccountSerializer.Meta): + class Meta: + model = Account fields_mini = [ 'id', 'name', 'username', 'privileged', 'version', 'secret_type', @@ -57,28 +62,79 @@ class AssetAccountSerializer(AccountSerializer): 'secret', 'push_now' ] fields = fields_mini + fields_write_only + extra_kwargs = { + 'secret': {'write_only': True}, + } + + def validate_name(self, value): + if not value: + value = self.initial_data.get('username') + return value + + @staticmethod + def validate_template(value): + try: + return AccountTemplate.objects.get(id=value) + except AccountTemplate.DoesNotExist: + raise serializers.ValidationError(_('Account template not found')) + + @staticmethod + def replace_attrs(account_template: AccountTemplate, attrs: dict): + exclude_fields = [ + '_state', 'org_id', 'id', 'date_created', + 'date_updated' + ] + template_attrs = { + k: v for k, v in account_template.__dict__.items() + if k not in exclude_fields + } + for k, v in template_attrs.items(): + attrs.setdefault(k, v) + + def validate(self, attrs): + account_template = attrs.pop('template', None) + if account_template: + self.replace_attrs(account_template, attrs) + self.push_now = attrs.pop('push_now', False) + return super().validate(attrs) + + def create(self, validated_data): + from accounts.tasks import push_accounts_to_assets + instance = super().create(validated_data) + if self.push_now: + push_accounts_to_assets.delay([instance.id], [instance.asset_id]) + return instance -class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSerializer): +class AccountSecretSerializer(SecretReadableMixin, CommonModelSerializer): + class Meta: + model = Account + fields = [ + 'name', 'username', 'privileged', 'secret_type', 'secret', + ] + extra_kwargs = { + 'secret': {'write_only': False}, + } + + +class AssetSerializer(BulkOrgResourceModelSerializer, WritableNestedModelSerializer): category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) - domain = ObjectRelatedField(required=False, queryset=Domain.objects, label=_('Domain'), allow_null=True) - platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) - nodes = ObjectRelatedField(many=True, required=False, queryset=Node.objects, label=_('Nodes')) - labels = AssetLabelSerializer(many=True, required=False, label=_('Labels')) + labels = AssetLabelSerializer(many=True, required=False, label=_('Label')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) - accounts = AssetAccountSerializer(many=True, required=False, label=_('Account')) + accounts = AssetAccountSerializer(many=True, required=False, write_only=True, label=_('Account')) + enabled_info = serializers.DictField(read_only=True, label=_('Enabled info')) class Meta: model = Asset fields_mini = ['id', 'name', 'address'] fields_small = fields_mini + ['is_active', 'comment'] - fields_fk = ['domain', 'platform', 'platform'] + fields_fk = ['domain', 'platform'] fields_m2m = [ - 'nodes', 'labels', 'protocols', 'accounts', 'nodes_display', + 'nodes', 'labels', 'protocols', 'nodes_display', 'accounts' ] read_only_fields = [ - 'category', 'type', 'info', + 'category', 'type', 'info', 'enabled_info', 'connectivity', 'date_verified', 'created_by', 'date_created' ] @@ -89,16 +145,31 @@ class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSeriali 'nodes_display': {'label': _('Node path')}, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._init_field_choices() + + def _init_field_choices(self): + request = self.context.get('request') + if not request: + return + category = request.path.strip('/').split('/')[-1].rstrip('s') + field_category = self.fields.get('category') + field_category._choices = Category.filter_choices(category) + field_type = self.fields.get('type') + field_type._choices = AllTypes.filter_choices(category) + @classmethod def setup_eager_loading(cls, queryset): """ Perform necessary eager loading of data. """ - queryset = queryset.prefetch_related('domain', 'platform', 'protocols') \ + queryset = queryset.prefetch_related('domain', 'platform') \ .annotate(category=F("platform__category")) \ .annotate(type=F("platform__type")) - queryset = queryset.prefetch_related('nodes', 'labels', 'accounts') + queryset = queryset.prefetch_related('nodes', 'labels', 'protocols') return queryset - def perform_nodes_display_create(self, instance, nodes_display): + @staticmethod + def perform_nodes_display_create(instance, nodes_display): if not nodes_display: return nodes_to_set = [] @@ -166,28 +237,20 @@ class AssetSerializer(BulkOrgResourceSerializerMixin, WritableNestedModelSeriali return instance -class AssetDetailSerializer(AssetSerializer): +class DetailMixin(serializers.Serializer): accounts = AssetAccountSerializer(many=True, required=False, label=_('Accounts')) - enabled_info = serializers.SerializerMethodField() - class Meta(AssetSerializer.Meta): - fields = AssetSerializer.Meta.fields + ['accounts', 'enabled_info', 'info', 'specific'] - @staticmethod - def get_enabled_info(obj): - platform = obj.platform - automation = platform.automation - return { - 'su_enabled': platform.su_enabled, - 'ping_enabled': automation.ping_enabled, - 'domain_enabled': platform.domain_enabled, - 'ansible_enabled': automation.ansible_enabled, - 'protocols_enabled': platform.protocols_enabled, - 'gather_facts_enabled': automation.gather_facts_enabled, - 'change_secret_enabled': automation.change_secret_enabled, - 'verify_account_enabled': automation.verify_account_enabled, - 'gather_accounts_enabled': automation.gather_accounts_enabled, - } + def get_field_names(self, declared_fields, info): + names = super().get_field_names(declared_fields, info) + names.extend([ + 'accounts', 'info', 'specific', 'spec_info' + ]) + return names + + +class AssetDetailSerializer(DetailMixin, AssetSerializer): + pass class MiniAssetSerializer(serializers.ModelSerializer): diff --git a/apps/assets/serializers/asset/database.py b/apps/assets/serializers/asset/database.py index d168d2ffe..c4ad0171d 100644 --- a/apps/assets/serializers/asset/database.py +++ b/apps/assets/serializers/asset/database.py @@ -1,11 +1,22 @@ - from assets.models import Database from .common import AssetSerializer +from ..gateway import GatewayWithAccountSecretSerializer -__all__ = ['DatabaseSerializer'] +__all__ = ['DatabaseSerializer', 'DatabaseWithGatewaySerializer'] class DatabaseSerializer(AssetSerializer): class Meta(AssetSerializer.Meta): model = Database - fields = AssetSerializer.Meta.fields + ['db_name'] + extra_fields = [ + 'db_name', 'use_ssl', 'ca_cert', 'client_cert', + 'client_key', 'allow_invalid_cert' + ] + fields = AssetSerializer.Meta.fields + extra_fields + + +class DatabaseWithGatewaySerializer(DatabaseSerializer): + gateway = GatewayWithAccountSecretSerializer() + + class Meta(DatabaseSerializer.Meta): + fields = DatabaseSerializer.Meta.fields + ['gateway'] diff --git a/apps/assets/serializers/automations/__init__.py b/apps/assets/serializers/automations/__init__.py index e4daeda95..9b5ed21c9 100644 --- a/apps/assets/serializers/automations/__init__.py +++ b/apps/assets/serializers/automations/__init__.py @@ -1,3 +1 @@ from .base import * -from .change_secret import * -from .gather_accounts import * diff --git a/apps/assets/serializers/automations/base.py b/apps/assets/serializers/automations/base.py index 3ff3f6686..fa20c4c28 100644 --- a/apps/assets/serializers/automations/base.py +++ b/apps/assets/serializers/automations/base.py @@ -2,11 +2,11 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers from ops.mixin import PeriodTaskSerializerMixin -from assets.const import AutomationTypes from assets.models import Asset, Node, BaseAutomation, AutomationExecution from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.utils import get_logger -from common.drf.fields import ObjectRelatedField +from common.const.choices import Trigger +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField logger = get_logger(__file__) @@ -24,10 +24,10 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe read_only_fields = [ 'date_created', 'date_updated', 'created_by', 'periodic_display' ] - fields = read_only_fields + [ - 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', - 'type', 'accounts', 'nodes', 'assets', 'is_active' - ] + fields = [ + 'id', 'name', 'is_periodic', 'interval', 'crontab', 'comment', + 'type', 'accounts', 'nodes', 'assets', 'is_active' + ] + read_only_fields extra_kwargs = { 'name': {'required': True}, 'type': {'read_only': True}, @@ -37,15 +37,14 @@ class BaseAutomationSerializer(PeriodTaskSerializerMixin, BulkOrgResourceModelSe class AutomationExecutionSerializer(serializers.ModelSerializer): snapshot = serializers.SerializerMethodField(label=_('Automation snapshot')) - type = serializers.ChoiceField(choices=AutomationTypes.choices, write_only=True, label=_('Type')) - trigger_display = serializers.ReadOnlyField(source='get_trigger_display', label=_('Trigger mode')) + trigger = LabeledChoiceField(choices=Trigger.choices, label=_("Trigger mode")) class Meta: model = AutomationExecution read_only_fields = [ - 'trigger_display', 'date_start', 'date_finished', 'snapshot', 'status' + 'trigger', 'date_start', 'date_finished', 'snapshot', 'status' ] - fields = ['id', 'automation', 'trigger', 'type'] + read_only_fields + fields = ['id', 'automation', 'trigger'] + read_only_fields @staticmethod def get_snapshot(obj): @@ -57,7 +56,6 @@ class AutomationExecutionSerializer(serializers.ModelSerializer): 'accounts': obj.snapshot['accounts'], 'node_amount': len(obj.snapshot['nodes']), 'asset_amount': len(obj.snapshot['assets']), - 'type_display': getattr(AutomationTypes, tp).label, } return snapshot diff --git a/apps/assets/serializers/base.py b/apps/assets/serializers/base.py index 9641ce786..3d98261b1 100644 --- a/apps/assets/serializers/base.py +++ b/apps/assets/serializers/base.py @@ -1,57 +1,3 @@ # -*- coding: utf-8 -*- # -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers -from assets.const import SecretType -from common.drf.fields import EncryptedField, LabeledChoiceField -from .utils import validate_password_for_ansible, validate_ssh_key - - -class AuthValidateMixin(serializers.Serializer): - secret_type = LabeledChoiceField( - choices=SecretType.choices, required=True, label=_('Secret type') - ) - secret = EncryptedField( - label=_('Secret'), required=False, max_length=40960, allow_blank=True, - allow_null=True, write_only=True, - ) - passphrase = serializers.CharField( - allow_blank=True, allow_null=True, required=False, max_length=512, - write_only=True, label=_('Key password') - ) - - @property - def initial_secret_type(self): - secret_type = self.initial_data.get('secret_type') - return secret_type - - def validate_secret(self, secret): - if not secret: - return '' - secret_type = self.initial_secret_type - if secret_type == SecretType.PASSWORD: - validate_password_for_ansible(secret) - return secret - elif secret_type == SecretType.SSH_KEY: - passphrase = self.initial_data.get('passphrase') - passphrase = passphrase if passphrase else None - return validate_ssh_key(secret, passphrase) - else: - return secret - - @staticmethod - def clean_auth_fields(validated_data): - for field in ('secret',): - value = validated_data.get(field) - if value is None: - validated_data.pop(field, None) - validated_data.pop('passphrase', None) - - def create(self, validated_data): - self.clean_auth_fields(validated_data) - return super().create(validated_data) - - def update(self, instance, validated_data): - self.clean_auth_fields(validated_data) - return super().update(instance, validated_data) diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 8fc711146..6692ea03b 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- # from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.fields import ObjectRelatedField -from ..serializers import GatewaySerializer +from .gateway import GatewayWithAccountSecretSerializer from ..models import Domain, Asset - __all__ = ['DomainSerializer', 'DomainWithGatewaySerializer'] @@ -26,11 +26,17 @@ class DomainSerializer(BulkOrgResourceModelSerializer): fields_m2m = ['assets', 'gateways'] read_only_fields = ['date_created'] fields = fields_small + fields_m2m + read_only_fields - extra_kwargs = {} + + def to_representation(self, instance): + data = super().to_representation(instance) + assets = data['assets'] + gateway_ids = [str(i['id']) for i in data['gateways']] + data['assets'] = [i for i in assets if str(i['id']) not in gateway_ids] + return data -class DomainWithGatewaySerializer(BulkOrgResourceModelSerializer): - gateways = GatewaySerializer(many=True, read_only=True) +class DomainWithGatewaySerializer(serializers.ModelSerializer): + gateways = GatewayWithAccountSecretSerializer(many=True, read_only=True) class Meta: model = Domain diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index cc0647943..909c13506 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,10 +4,9 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.drf.serializers import BulkSerializerMixin +from common.serializers import BulkSerializerMixin from ..models import FavoriteAsset - __all__ = ['FavoriteAssetSerializer'] diff --git a/apps/assets/serializers/gateway.py b/apps/assets/serializers/gateway.py index 8dab64957..012348719 100644 --- a/apps/assets/serializers/gateway.py +++ b/apps/assets/serializers/gateway.py @@ -1,11 +1,33 @@ # -*- coding: utf-8 -*- # -from ..serializers import HostSerializer -from ..models import Gateway +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers -__all__ = ['GatewaySerializer'] +from .asset import HostSerializer +from .asset.common import AccountSecretSerializer +from ..models import Gateway, Asset + +__all__ = ['GatewaySerializer', 'GatewayWithAccountSecretSerializer'] class GatewaySerializer(HostSerializer): class Meta(HostSerializer.Meta): model = Gateway + + def validate_name(self, value): + queryset = Asset.objects.filter(name=value) + if self.instance: + queryset = queryset.exclude(id=self.instance.id) + has = queryset.exists() + if has: + raise serializers.ValidationError( + _('This field must be unique.') + ) + return value + + +class GatewayWithAccountSecretSerializer(GatewaySerializer): + account = AccountSecretSerializer(required=False, label=_('Account'), source='select_account') + + class Meta(GatewaySerializer.Meta): + fields = GatewaySerializer.Meta.fields + ['account'] diff --git a/apps/assets/serializers/platform.py b/apps/assets/serializers/platform.py index ab72cecc9..6e622aaf9 100644 --- a/apps/assets/serializers/platform.py +++ b/apps/assets/serializers/platform.py @@ -1,8 +1,8 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField -from common.drf.serializers import WritableNestedModelSerializer +from common.serializers.fields import LabeledChoiceField +from common.serializers import WritableNestedModelSerializer from ..const import Category, AllTypes from ..models import Platform, PlatformProtocol, PlatformAutomation @@ -44,6 +44,7 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): "id", "ansible_enabled", "ansible_config", "ping_enabled", "ping_method", + "push_account_enabled", "push_account_method", "gather_facts_enabled", "gather_facts_method", "change_secret_enabled", "change_secret_method", "verify_account_enabled", "verify_account_method", @@ -58,6 +59,8 @@ class PlatformAutomationSerializer(serializers.ModelSerializer): "verify_account_method": {"label": "校验账号方式"}, "change_secret_enabled": {"label": "启用账号改密"}, "change_secret_method": {"label": "账号改密方式"}, + "push_account_enabled": {"label": "启用推送账号"}, + "push_account_method": {"label": "推送账号方式"}, "gather_accounts_enabled": {"label": "启用账号收集"}, "gather_accounts_method": {"label": "收集账号方式"}, } @@ -100,18 +103,24 @@ class PlatformSerializer(WritableNestedModelSerializer): "category", "type", "charset", ] fields = fields_small + [ - "protocols_enabled", "protocols", + "protocols", "domain_enabled", "su_enabled", "su_method", "automation", "comment", ] extra_kwargs = { "su_enabled": {"label": "启用切换账号"}, - "protocols_enabled": {"label": "启用协议"}, "domain_enabled": {"label": "启用网域"}, "domain_default": {"label": "默认网域"}, } + @classmethod + def setup_eager_loading(cls, queryset): + queryset = queryset.prefetch_related( + 'protocols', 'automation' + ) + return queryset + class PlatformOpsMethodSerializer(serializers.Serializer): id = serializers.CharField(read_only=True) diff --git a/apps/assets/serializers/utils.py b/apps/assets/serializers/utils.py index 770710843..139597f9c 100644 --- a/apps/assets/serializers/utils.py +++ b/apps/assets/serializers/utils.py @@ -1,25 +1,2 @@ -from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers - -from common.utils import validate_ssh_private_key, parse_ssh_private_key_str -def validate_password_for_ansible(password): - """ 校验 Ansible 不支持的特殊字符 """ - # validate password contains left double curly bracket - # check password not contains `{{` - # Ansible 推送的时候不支持 - if '{{' in password: - raise serializers.ValidationError(_('Password can not contains `{{` ')) - # Ansible Windows 推送的时候不支持 - if "'" in password: - raise serializers.ValidationError(_("Password can not contains `'` ")) - if '"' in password: - raise serializers.ValidationError(_('Password can not contains `"` ')) - - -def validate_ssh_key(ssh_key, passphrase=None): - valid = validate_ssh_private_key(ssh_key, password=passphrase) - if not valid: - raise serializers.ValidationError(_("private key invalid or passphrase error")) - return parse_ssh_private_key_str(ssh_key, passphrase) diff --git a/apps/assets/signal_handlers/__init__.py b/apps/assets/signal_handlers/__init__.py index b337df001..aab861fd3 100644 --- a/apps/assets/signal_handlers/__init__.py +++ b/apps/assets/signal_handlers/__init__.py @@ -1,4 +1,3 @@ from .asset import * -from .account import * from .node_assets_amount import * from .node_assets_mapping import * diff --git a/apps/assets/signal_handlers/account.py b/apps/assets/signal_handlers/account.py deleted file mode 100644 index 8020e4087..000000000 --- a/apps/assets/signal_handlers/account.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.dispatch import receiver -from django.db.models.signals import pre_save - -from common.utils import get_logger -from ..models import Account - -logger = get_logger(__name__) - - -@receiver(pre_save, sender=Account) -def on_account_pre_create(sender, instance, **kwargs): - # 升级版本号 - instance.version += 1 - # 即使在 root 组织也不怕 - instance.org_id = instance.asset.org_id diff --git a/apps/assets/signal_handlers/asset.py b/apps/assets/signal_handlers/asset.py index 86e6a0a9a..55cd0ede2 100644 --- a/apps/assets/signal_handlers/asset.py +++ b/apps/assets/signal_handlers/asset.py @@ -1,32 +1,16 @@ # -*- coding: utf-8 -*- # -from django.db.models.signals import ( - post_save, m2m_changed, pre_delete, post_delete, pre_save -) +from django.db.models.signals import post_save, m2m_changed, pre_delete, post_delete, pre_save from django.dispatch import receiver +from assets.models import Asset, Node, Cloud, Device, Host, Web, Database from common.const.signals import POST_ADD, POST_REMOVE, PRE_REMOVE -from common.utils import get_logger from common.decorator import on_transaction_commit -from assets.models import Asset, Node -from assets.tasks import ( - update_assets_hardware_info_util, - test_asset_connectivity_util, -) +from common.utils import get_logger logger = get_logger(__file__) -def update_asset_hardware_info_on_created(asset): - logger.debug("Update asset `{}` hardware info".format(asset)) - update_assets_hardware_info_util.delay([asset]) - - -def test_asset_conn_on_created(asset): - logger.debug("Test asset `{}` connectivity".format(asset)) - test_asset_connectivity_util.delay([asset]) - - @receiver(pre_save, sender=Node) def on_node_pre_save(sender, instance: Node, **kwargs): instance.parent_key = instance.compute_parent_key() @@ -34,22 +18,23 @@ def on_node_pre_save(sender, instance: Node, **kwargs): @receiver(post_save, sender=Asset) @on_transaction_commit -def on_asset_created_or_update(sender, instance=None, created=False, **kwargs): +def on_asset_create(sender, instance=None, created=False, **kwargs): """ 当资产创建时,更新硬件信息,更新可连接性 确保资产必须属于一个节点 """ - if created: - logger.info("Asset create signal recv: {}".format(instance)) + if not created: + return + logger.info("Asset create signal recv: {}".format(instance)) - # 获取资产硬件信息 - update_asset_hardware_info_on_created(instance) - test_asset_conn_on_created(instance) + # 获取资产硬件信息 + # update_assets_fact_util.delay([instance]) + # test_asset_connectivity_util.delay([instance]) - # 确保资产存在一个节点 - has_node = instance.nodes.all().exists() - if not has_node: - instance.nodes.add(Node.org_root()) + # 确保资产存在一个节点 + has_node = instance.nodes.all().exists() + if not has_node: + instance.nodes.add(Node.org_root()) @receiver(m2m_changed, sender=Asset.nodes.through) @@ -109,6 +94,7 @@ RELATED_NODE_IDS = '_related_node_ids' @receiver(pre_delete, sender=Asset) def on_asset_delete(instance: Asset, using, **kwargs): + logger.debug("Asset pre delete signal recv: {}".format(instance)) node_ids = set(Node.objects.filter( assets=instance ).distinct().values_list('id', flat=True)) @@ -121,9 +107,19 @@ def on_asset_delete(instance: Asset, using, **kwargs): @receiver(post_delete, sender=Asset) def on_asset_post_delete(instance: Asset, using, **kwargs): + logger.debug("Asset delete signal recv: {}".format(instance)) node_ids = getattr(instance, RELATED_NODE_IDS, None) if node_ids: m2m_changed.send( sender=Asset.nodes.through, instance=instance, reverse=False, model=Node, pk_set=node_ids, using=using, action=POST_REMOVE ) + + +def resend_to_asset_signals(sender, signal, **kwargs): + signal.send(sender=Asset, **kwargs) + + +for model in (Host, Database, Device, Web, Cloud): + for s in (pre_save, post_save): + s.connect(resend_to_asset_signals, sender=model) diff --git a/apps/assets/tasks/__init__.py b/apps/assets/tasks/__init__.py index 060f4d2d9..cf2ccd2cd 100644 --- a/apps/assets/tasks/__init__.py +++ b/apps/assets/tasks/__init__.py @@ -3,10 +3,6 @@ from .ping import * from .utils import * from .common import * -from .backup import * from .automation import * from .gather_facts import * from .nodes_amount import * -from .push_account import * -from .verify_account import * -from .gather_accounts import * diff --git a/apps/assets/tasks/automation.py b/apps/assets/tasks/automation.py index 60f01836f..582fe7ea2 100644 --- a/apps/assets/tasks/automation.py +++ b/apps/assets/tasks/automation.py @@ -8,7 +8,7 @@ from assets.const import AutomationTypes logger = get_logger(__file__) -@shared_task(queue='ansible', verbose_name=_('Execute automation')) +@shared_task(queue='ansible', verbose_name=_('Asset execute automation')) def execute_automation(pid, trigger, tp): model = AutomationTypes.get_type_model(tp) with tmp_to_root_org(): diff --git a/apps/assets/tasks/common.py b/apps/assets/tasks/common.py index ec51c5a2b..9bafbcde8 100644 --- a/apps/assets/tasks/common.py +++ b/apps/assets/tasks/common.py @@ -1,2 +1,46 @@ # -*- coding: utf-8 -*- # +import uuid +from celery import current_task +from django.db.utils import IntegrityError +from orgs.utils import current_org + +from common.const.choices import Trigger + + +def generate_data(task_name, tp, child_snapshot=None): + child_snapshot = child_snapshot or {} + from assets.models import BaseAutomation + try: + eid = current_task.request.id + except AttributeError: + eid = str(uuid.uuid4()) + + data = { + 'type': tp, + 'name': task_name, + 'org_id': str(current_org.id) + } + + automation_instance = BaseAutomation() + snapshot = automation_instance.to_attr_json() + snapshot.update(data) + snapshot.update(child_snapshot) + return {'id': eid, 'snapshot': snapshot} + + +def automation_execute_start(task_name, tp, child_snapshot=None): + from assets.models import AutomationExecution + data = generate_data(task_name, tp, child_snapshot) + + while True: + try: + _id = data['id'] + AutomationExecution.objects.get(id=_id) + data['id'] = str(uuid.uuid4()) + except AutomationExecution.DoesNotExist: + break + execution = AutomationExecution.objects.create( + trigger=Trigger.manual, **data + ) + execution.start() diff --git a/apps/assets/tasks/gather_facts.py b/apps/assets/tasks/gather_facts.py index b3196abf5..ad678b92a 100644 --- a/apps/assets/tasks/gather_facts.py +++ b/apps/assets/tasks/gather_facts.py @@ -1,60 +1,66 @@ # -*- coding: utf-8 -*- # from celery import shared_task -from django.utils.translation import gettext_noop -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_noop, gettext_lazy as _ from common.utils import get_logger +from assets.const import AutomationTypes from orgs.utils import org_aware_func, tmp_to_root_org +from .common import automation_execute_start + logger = get_logger(__file__) __all__ = [ - 'update_assets_hardware_info_util', + 'update_assets_fact_util', 'update_node_assets_hardware_info_manual', 'update_assets_hardware_info_manual', ] -@org_aware_func('assets') -def update_assets_hardware_info_util(assets=None, nodes=None, task_name=None): +def update_fact_util(assets=None, nodes=None, task_name=None): from assets.models import GatherFactsAutomation - if not assets and not nodes: - logger.info("No assets or nodes to update hardware info") - return - if task_name is None: task_name = gettext_noop("Update some assets hardware info. ") task_name = GatherFactsAutomation.generate_unique_name(task_name) - comment = '' - if assets: - comment += 'asset:' + ', '.join([str(i) for i in assets]) + '\n' - if nodes: - comment += 'node:' + ', '.join([str(i) for i in nodes]) - data = {'name': task_name, 'comment': comment} - instance = GatherFactsAutomation.objects.create(**data) + nodes = nodes or [] + assets = assets or [] + child_snapshot = { + 'assets': [str(asset.id) for asset in assets], + 'nodes': [str(node.id) for node in nodes], + } + tp = AutomationTypes.gather_facts + automation_execute_start(task_name, tp, child_snapshot) - if assets: - instance.assets.add(*assets) - if nodes: - instance.nodes.add(*nodes) - instance.execute() + +@org_aware_func('assets') +def update_assets_fact_util(assets=None, task_name=None): + if assets is None: + logger.info("No assets to update hardware info") + return + + update_fact_util(assets=assets, task_name=task_name) + + +@org_aware_func('nodes') +def update_nodes_fact_util(nodes=None, task_name=None): + if nodes is None: + logger.info("No nodes to update hardware info") + return + update_fact_util(nodes=nodes, task_name=task_name) @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets')) def update_assets_hardware_info_manual(asset_ids): from assets.models import Asset - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) + assets = Asset.objects.filter(id__in=asset_ids) task_name = gettext_noop("Update assets hardware info: ") - update_assets_hardware_info_util(assets=assets, task_name=task_name) + update_assets_fact_util(assets=assets, task_name=task_name) @shared_task(queue="ansible", verbose_name=_('Manually update the hardware information of assets under a node')) def update_node_assets_hardware_info_manual(node_id): from assets.models import Node - with tmp_to_root_org(): - node = Node.objects.get(id=node_id) - + node = Node.objects.get(id=node_id) task_name = gettext_noop("Update node asset hardware information: ") - update_assets_hardware_info_util(nodes=[node], task_name=task_name) + update_nodes_fact_util(nodes=[node], task_name=task_name) diff --git a/apps/assets/tasks/ping.py b/apps/assets/tasks/ping.py index 817f64b64..c0b53a9c8 100644 --- a/apps/assets/tasks/ping.py +++ b/apps/assets/tasks/ping.py @@ -1,12 +1,15 @@ # ~*~ coding: utf-8 ~*~ from celery import shared_task -from django.utils.translation import gettext_noop -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import gettext_noop, gettext_lazy as _ from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org +from assets.const import AutomationTypes, GATEWAY_NAME +from orgs.utils import org_aware_func + +from .common import automation_execute_start logger = get_logger(__file__) + __all__ = [ 'test_asset_connectivity_util', 'test_assets_connectivity_manual', @@ -14,38 +17,48 @@ __all__ = [ ] +def test_connectivity_util(assets, tp, task_name=None, local_port=None): + if not assets: + return + + if local_port is None: + child_snapshot = {} + else: + child_snapshot = {'local_port': local_port} + + child_snapshot['assets'] = [str(asset.id) for asset in assets] + automation_execute_start(task_name, tp, child_snapshot) + + @org_aware_func('assets') -def test_asset_connectivity_util(assets, task_name=None): +def test_asset_connectivity_util(assets, task_name=None, local_port=None): from assets.models import PingAutomation if task_name is None: task_name = gettext_noop("Test assets connectivity ") task_name = PingAutomation.generate_unique_name(task_name) - data = { - 'name': task_name, - 'comment': ', '.join([str(i) for i in assets]) - } - instance = PingAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() + + gateway_assets = assets.filter(platform__name=GATEWAY_NAME) + test_connectivity_util( + gateway_assets, AutomationTypes.ping_gateway, task_name, local_port + ) + + non_gateway_assets = assets.exclude(platform__name=GATEWAY_NAME) + test_connectivity_util(non_gateway_assets, AutomationTypes.ping, task_name) -@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset')) -def test_assets_connectivity_manual(asset_ids): +@shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of a asset')) +def test_assets_connectivity_manual(asset_ids, local_port=None): from assets.models import Asset - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - + assets = Asset.objects.filter(id__in=asset_ids) task_name = gettext_noop("Test assets connectivity ") - test_asset_connectivity_util(assets, task_name=task_name) + test_asset_connectivity_util(assets, task_name, local_port) @shared_task(queue="ansible", verbose_name=_('Manually test the connectivity of assets under a node')) -def test_node_assets_connectivity_manual(node_id): +def test_node_assets_connectivity_manual(node_id, local_port=None): from assets.models import Node - with tmp_to_root_org(): - node = Node.objects.get(id=node_id) - + node = Node.objects.get(id=node_id) task_name = gettext_noop("Test if the assets under the node are connectable ") assets = node.get_all_assets() - test_asset_connectivity_util(assets, task_name=task_name) + test_asset_connectivity_util(assets, task_name, local_port) diff --git a/apps/assets/tasks/push_account.py b/apps/assets/tasks/push_account.py deleted file mode 100644 index 7c596c8f2..000000000 --- a/apps/assets/tasks/push_account.py +++ /dev/null @@ -1,41 +0,0 @@ -from celery import shared_task -from django.utils.translation import gettext_noop - -from common.utils import get_logger -from orgs.utils import org_aware_func, tmp_to_root_org -from django.utils.translation import ugettext_lazy as _ - -logger = get_logger(__file__) -__all__ = [ - 'push_accounts_to_assets', -] - - -@org_aware_func("assets") -def push_accounts_to_assets_util(accounts, assets, username=None): - from assets.models import PushAccountAutomation - task_name = gettext_noop("Push accounts to assets") - task_name = PushAccountAutomation.generate_unique_name(task_name) - if username is None: - account_usernames = list(accounts.values_list('username', flat=True)) - else: - account_usernames = [username] - - data = { - 'name': task_name, - 'accounts': account_usernames, - 'comment': ', '.join([str(i) for i in assets]) - } - instance = PushAccountAutomation.objects.create(**data) - instance.assets.add(*assets) - instance.execute() - - -@shared_task(queue="ansible", verbose_name=_('Push accounts to assets')) -def push_accounts_to_assets(account_ids, asset_ids, username=None): - from assets.models import Asset, Account - with tmp_to_root_org(): - assets = Asset.objects.filter(id__in=asset_ids) - accounts = Account.objects.filter(id__in=account_ids) - - return push_accounts_to_assets_util(accounts, assets, username) diff --git a/apps/assets/urls/api_urls.py b/apps/assets/urls/api_urls.py index dd42c44eb..a9a385eb9 100644 --- a/apps/assets/urls/api_urls.py +++ b/apps/assets/urls/api_urls.py @@ -14,24 +14,12 @@ router.register(r'devices', api.DeviceViewSet, 'device') router.register(r'databases', api.DatabaseViewSet, 'database') router.register(r'webs', api.WebViewSet, 'web') router.register(r'clouds', api.CloudViewSet, 'cloud') -router.register(r'accounts', api.AccountViewSet, 'account') -router.register(r'account-templates', api.AccountTemplateViewSet, 'account-template') -router.register(r'account-template-secrets', api.AccountTemplateSecretsViewSet, 'account-template-secret') -router.register(r'account-secrets', api.AccountSecretsViewSet, 'account-secret') router.register(r'platforms', api.AssetPlatformViewSet, 'platform') router.register(r'labels', api.LabelViewSet, 'label') router.register(r'nodes', api.NodeViewSet, 'node') router.register(r'domains', api.DomainViewSet, 'domain') router.register(r'gateways', api.GatewayViewSet, 'gateway') router.register(r'favorite-assets', api.FavoriteAssetViewSet, 'favorite-asset') -router.register(r'account-backup-plans', api.AccountBackupPlanViewSet, 'account-backup') -router.register(r'account-backup-plan-executions', api.AccountBackupPlanExecutionViewSet, 'account-backup-execution') - -router.register(r'change-secret-automations', api.ChangeSecretAutomationViewSet, 'change-secret-automation') -router.register(r'change-secret-executions', api.ChangSecretExecutionViewSet, 'change-secret-execution') -router.register(r'gather-account-executions', api.GatherAccountsExecutionViewSet, 'gather-account-execution') -router.register(r'change-secret-records', api.ChangeSecretRecordViewSet, 'change-secret-record') -router.register(r'gather-account-automations', api.GatherAccountsAutomationViewSet, 'gather-account-automation') urlpatterns = [ # path('assets//gateways/', api.AssetGatewayListApi.as_view(), name='asset-gateway-list'), @@ -45,10 +33,6 @@ urlpatterns = [ path('assets//perm-user-groups//permissions/', api.AssetPermUserGroupPermissionsListApi.as_view(), name='asset-perm-user-group-permission-list'), - path('accounts/tasks/', api.AccountTaskCreateAPI.as_view(), name='account-task-create'), - path('account-secrets//histories/', api.AccountHistoriesSecretAPI.as_view(), - name='account-secret-history'), - path('nodes/category/tree/', api.CategoryTreeApi.as_view(), name='asset-category-tree'), path('nodes/children/tree/', api.NodeChildrenAsTreeApi.as_view(), name='node-children-tree'), path('nodes//children/', api.NodeChildrenApi.as_view(), name='node-children'), @@ -61,11 +45,6 @@ urlpatterns = [ path('nodes//tasks/', api.NodeTaskCreateApi.as_view(), name='node-task-create'), path('gateways//test-connective/', api.GatewayTestConnectionApi.as_view(), name='test-gateway-connective'), - - path('automation//asset/remove/', api.AutomationRemoveAssetApi.as_view(), name='automation-remove-asset'), - path('automation//asset/add/', api.AutomationAddAssetApi.as_view(), name='automation-add-asset'), - path('automation//nodes/', api.AutomationNodeAddRemoveApi.as_view(), name='automation-add-or-remove-node'), - path('automation//assets/', api.AutomationAssetsListApi.as_view(), name='automation-assets'), ] urlpatterns += router.urls diff --git a/apps/assets/utils/node.py b/apps/assets/utils/node.py index 5d22421c5..deaa7d32b 100644 --- a/apps/assets/utils/node.py +++ b/apps/assets/utils/node.py @@ -2,7 +2,7 @@ # from collections import defaultdict from common.utils import get_logger, dict_get_any, is_uuid, get_object_or_none, timeit -from common.http import is_true +from common.utils.http import is_true from common.struct import Stack from common.db.models import output_as_string from orgs.utils import ensure_in_real_or_default_org, current_org diff --git a/apps/audits/api.py b/apps/audits/api.py index b060a0eb1..383b0d45b 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -8,16 +8,17 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.mixins import ListModelMixin, CreateModelMixin, RetrieveModelMixin from ops.models.job import JobAuditLog -from common.api import CommonGenericViewSet +from common.api import JMSGenericViewSet from common.drf.filters import DatetimeRangeFilter from common.plugins.es import QuerySet as ESQuerySet -from orgs.utils import current_org +from orgs.utils import current_org, tmp_to_root_org from orgs.mixins.api import OrgGenericViewSet, OrgBulkModelViewSet from .backends import TYPE_ENGINE_MAPPING from .models import FTPLog, UserLoginLog, OperateLog, PasswordChangeLog from .serializers import FTPLogSerializer, UserLoginLogSerializer, JobAuditLogSerializer from .serializers import ( - OperateLogSerializer, OperateLogActionDetailSerializer, PasswordChangeLogSerializer + OperateLogSerializer, OperateLogActionDetailSerializer, + PasswordChangeLogSerializer, ActivitiesOperatorLogSerializer, ) @@ -50,7 +51,7 @@ class UserLoginCommonMixin: search_fields = ['username', 'ip', 'city'] -class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, CommonGenericViewSet): +class UserLoginLogViewSet(UserLoginCommonMixin, ListModelMixin, JMSGenericViewSet): @staticmethod def get_org_members(): @@ -75,6 +76,19 @@ class MyLoginLogAPIView(UserLoginCommonMixin, generics.ListAPIView): return qs +class ResourceActivityAPIView(generics.ListAPIView): + serializer_class = ActivitiesOperatorLogSerializer + rbac_perms = { + 'GET': 'audits.view_operatelog', + } + + def get_queryset(self): + resource_id = self.request.query_params.get('resource_id') + with tmp_to_root_org(): + queryset = OperateLog.objects.filter(resource_id=resource_id)[:30] + return queryset + + class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): model = OperateLog serializer_class = OperateLogSerializer @@ -92,7 +106,7 @@ class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): return super().get_serializer_class() def get_queryset(self): - qs = OperateLog.objects.all() + qs = OperateLog.objects.filter(is_activity=False) es_config = settings.OPERATE_LOG_ELASTICSEARCH_CONFIG if es_config: engine_mod = import_module(TYPE_ENGINE_MAPPING['es']) @@ -103,7 +117,7 @@ class OperateLogViewSet(RetrieveModelMixin, ListModelMixin, OrgGenericViewSet): return qs -class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet): +class PasswordChangeLogViewSet(ListModelMixin, JMSGenericViewSet): queryset = PasswordChangeLog.objects.all() serializer_class = PasswordChangeLogSerializer extra_filter_backends = [DatetimeRangeFilter] diff --git a/apps/audits/backends/db.py b/apps/audits/backends/db.py index 14222eb13..a3d941f8e 100644 --- a/apps/audits/backends/db.py +++ b/apps/audits/backends/db.py @@ -17,22 +17,26 @@ class OperateLogStore(object): return True def save(self, **kwargs): + before_limit, after_limit = None, None log_id = kwargs.get('id', '') before = kwargs.get('before') or {} after = kwargs.get('after') or {} if len(str(before)) > self.max_length: - before = {_('Tips'): self.max_length_tip_msg} + before_limit = {str(_('Tips')): self.max_length_tip_msg} if len(str(after)) > self.max_length: - after = {_('Tips'): self.max_length_tip_msg} + after_limit = {str(_('Tips')): self.max_length_tip_msg} op_log = self.model.objects.filter(pk=log_id).first() if op_log is not None: - raw_after = op_log.after or {} - raw_before = op_log.before or {} - raw_before.update(before) - raw_after.update(after) - op_log.before = raw_before - op_log.after = raw_after - op_log.save() + op_log_before = op_log.before or {} + op_log_after = op_log.after or {} + if not before_limit: + before.update(op_log_before) + if not after_limit: + after.update(op_log_after) else: - self.model.objects.create(**kwargs) + op_log = self.model(**kwargs) + + op_log.before = before_limit if before_limit else before + op_log.after = after_limit if after_limit else after + op_log.save() diff --git a/apps/audits/const.py b/apps/audits/const.py index 6987de94f..1dc37de47 100644 --- a/apps/audits/const.py +++ b/apps/audits/const.py @@ -5,37 +5,7 @@ from django.db.models import TextChoices, IntegerChoices DEFAULT_CITY = _("Unknown") -MODELS_NEED_RECORD = ( - # users - 'User', 'UserGroup', - # authentication - 'AccessKey', 'TempToken', - "User", - "UserGroup", - # acls - "LoginACL", - "LoginAssetACL", - "LoginConfirmSetting", - # assets - 'Asset', 'Node', 'Domain', 'Gateway', 'CommandFilterRule', - 'CommandFilter', 'Platform', 'Label', - # account - 'Account', - # orgs - "Organization", - # settings - "Setting", - # perms - 'AssetPermission', - # notifications - 'SystemMsgSubscription', 'UserMsgSubscription', - # Terminal - 'Terminal', 'Endpoint', 'EndpointRule', 'CommandStorage', 'ReplayStorage', - # rbac - 'Role', 'SystemRole', 'OrgRole', 'RoleBinding', 'OrgRoleBinding', 'SystemRoleBinding', - # xpack - 'License', 'Account', 'SyncInstanceTask', 'Interface', -) +MODELS_NEED_RECORD = set() class OperateChoices(TextChoices): @@ -53,6 +23,10 @@ class ActionChoices(TextChoices): update = "update", _("Update") delete = "delete", _("Delete") create = "create", _("Create") + # Activities action + connect = "connect", _("Connect") + login = "login", _("Login") + change_auth = "change_password", _("Change password") class LoginTypeChoices(TextChoices): diff --git a/apps/audits/handler.py b/apps/audits/handler.py index 8df3c1f37..13ea6f16a 100644 --- a/apps/audits/handler.py +++ b/apps/audits/handler.py @@ -4,6 +4,7 @@ from django.db import transaction from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ +from users.models import User from common.utils import get_request_ip, get_logger from common.utils.timezone import as_current_tz from common.utils.encode import Singleton @@ -14,6 +15,7 @@ from audits.models import OperateLog from orgs.utils import get_current_org_id from .backends import get_operate_log_storage +from .const import ActionChoices logger = get_logger(__name__) @@ -81,7 +83,7 @@ class OperatorLogHandler(metaclass=Singleton): def get_instance_dict_from_cache(self, instance_id): if instance_id is None: - return None + return None, None key = '%s_%s' % (self.CACHE_KEY, instance_id) cache_instance = cache.get(key, {}) @@ -148,9 +150,49 @@ class OperatorLogHandler(metaclass=Singleton): after = self.__data_processing(after) return before, after + @staticmethod + def _get_Session_params(resource, **kwargs): + # 更新会话的日志不在Activity中体现, + # 否则会话结束,录像文件结束操作的会话记录都会体现出来 + params = {} + action = kwargs.get('data', {}).get('action', 'create') + if action == ActionChoices.create: + params = { + 'action': ActionChoices.connect, + 'resource_id': str(resource.asset_id), + 'user': resource.user + } + return params + + @staticmethod + def _get_ChangeSecretRecord_params(resource, **kwargs): + return { + 'action': ActionChoices.change_auth, + 'resource_id': str(resource.account_id), + } + + @staticmethod + def _get_UserLoginLog_params(resource, **kwargs): + username = resource.username + user_id = User.objects.filter(username=username).\ + values_list('id', flat=True)[0] + return { + 'action': ActionChoices.login, + 'resource_id': str(user_id), + } + + def _activity_handle(self, data, object_name, resource): + param_func = getattr(self, '_get_%s_params' % object_name, None) + if param_func is not None: + params = param_func(resource, data=data) + data['is_activity'] = True + data.update(params) + return data + def create_or_update_operate_log( self, action, resource_type, resource=None, - force=False, log_id=None, before=None, after=None + force=False, log_id=None, before=None, after=None, + object_name=None ): user = current_request.user if current_request else None if not user or not user.is_authenticated: @@ -167,8 +209,9 @@ class OperatorLogHandler(metaclass=Singleton): 'id': log_id, "user": str(user), 'action': action, 'resource_type': str(resource_type), 'resource': resource_display, 'remote_addr': remote_addr, 'before': before, 'after': after, - 'org_id': get_current_org_id(), + 'org_id': get_current_org_id(), 'resource_id': str(resource.id) } + data = self._activity_handle(data, object_name, resource=resource) with transaction.atomic(): if self.log_client.ping(timeout=1): client = self.log_client diff --git a/apps/audits/migrations/0016_auto_20221111_1919.py b/apps/audits/migrations/0016_auto_20221111_1919.py index fb546c83f..226588e7b 100644 --- a/apps/audits/migrations/0016_auto_20221111_1919.py +++ b/apps/audits/migrations/0016_auto_20221111_1919.py @@ -20,9 +20,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='operatelog', name='action', - field=models.CharField( - choices=[('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), ('create', 'Create')], - max_length=16, verbose_name='Action'), + field=models.CharField(choices=[ + ('view', 'View'), ('update', 'Update'), ('delete', 'Delete'), + ('create', 'Create'), ('connect', 'Connect'), ('login', 'Login'), + ('change_password', 'Change password') + ], max_length=16, verbose_name='Action'), ), migrations.AlterField( model_name='userloginlog', diff --git a/apps/audits/migrations/0018_operatelog_resource_id.py b/apps/audits/migrations/0018_operatelog_resource_id.py new file mode 100644 index 000000000..44953cae3 --- /dev/null +++ b/apps/audits/migrations/0018_operatelog_resource_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.14 on 2023-01-03 08:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0017_auto_20221220_1757'), + ] + + operations = [ + migrations.AddField( + model_name='operatelog', + name='resource_id', + field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Resource'), + ), + ] diff --git a/apps/audits/migrations/0019_alter_operatelog_options.py b/apps/audits/migrations/0019_alter_operatelog_options.py new file mode 100644 index 000000000..c6ebcb76c --- /dev/null +++ b/apps/audits/migrations/0019_alter_operatelog_options.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('audits', '0018_operatelog_resource_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='operatelog', + options={'ordering': ('-datetime',), 'verbose_name': 'Operate log'}, + ), + ] diff --git a/apps/terminal/migrations/0063_applet_builtin.py b/apps/audits/migrations/0020_auto_20230112_1034.py similarity index 56% rename from apps/terminal/migrations/0063_applet_builtin.py rename to apps/audits/migrations/0020_auto_20230112_1034.py index 1d991e180..d3dd634fa 100644 --- a/apps/terminal/migrations/0063_applet_builtin.py +++ b/apps/audits/migrations/0020_auto_20230112_1034.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.14 on 2022-12-20 07:24 +# Generated by Django 3.2.14 on 2023-01-12 02:34 from django.db import migrations, models @@ -6,13 +6,13 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('terminal', '0062_auto_20221216_1529'), + ('audits', '0019_alter_operatelog_options'), ] operations = [ migrations.AddField( - model_name='applet', - name='builtin', - field=models.BooleanField(default=False, verbose_name='Builtin'), + model_name='operatelog', + name='is_activity', + field=models.BooleanField(default=False, verbose_name='Is Activity'), ), ] diff --git a/apps/audits/models.py b/apps/audits/models.py index 5f11fca9f..370a9513f 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -52,10 +52,15 @@ class OperateLog(OrgModelMixin): ) resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type")) resource = models.CharField(max_length=128, verbose_name=_("Resource")) + resource_id = models.CharField( + max_length=36, blank=True, default='', db_index=True, + verbose_name=_("Resource") + ) remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True) datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True) before = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) after = models.JSONField(default=dict, encoder=ModelJSONFieldEncoder, null=True) + is_activity = models.BooleanField(default=False, verbose_name=(_('Is Activity'))) def __str__(self): return "<{}> {} <{}>".format(self.user, self.action, self.resource) @@ -86,6 +91,7 @@ class OperateLog(OrgModelMixin): class Meta: verbose_name = _("Operate log") + ordering = ('-datetime',) class PasswordChangeLog(models.Model): diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index f5aee2f65..8fd8a10dc 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -3,7 +3,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField +from common.utils.timezone import as_current_tz from ops.models.job import JobAuditLog from ops.serializers.job import JobExecutionSerializer from terminal.models import Session @@ -22,6 +23,7 @@ class JobAuditLogSerializer(JobExecutionSerializer): "id", "material", "time_cost", 'date_start', 'date_finished', 'date_created', 'is_finished', 'is_success', 'created_by', + 'task_id' ] fields = read_only_fields + [] @@ -94,3 +96,18 @@ class SessionAuditSerializer(serializers.ModelSerializer): class Meta: model = Session fields = "__all__" + + +class ActivitiesOperatorLogSerializer(serializers.Serializer): + timestamp = serializers.SerializerMethodField() + content = serializers.SerializerMethodField() + + @staticmethod + def get_timestamp(obj): + return as_current_tz(obj.datetime).strftime('%Y-%m-%d %H:%M:%S') + + @staticmethod + def get_content(obj): + action = obj.action.replace('_', ' ').capitalize() + ctn = _('User {} {} this resource.').format(obj.user, _(action)) + return ctn diff --git a/apps/audits/signal_handlers.py b/apps/audits/signal_handlers.py index 546fea60a..a7649bbe5 100644 --- a/apps/audits/signal_handlers.py +++ b/apps/audits/signal_handlers.py @@ -2,6 +2,7 @@ # import uuid +from django.apps import apps from django.conf import settings from django.contrib.auth import BACKEND_SESSION_KEY from django.db import transaction @@ -21,6 +22,7 @@ from audits.utils import model_to_dict_for_operate_log as model_to_dict from authentication.signals import post_auth_failed, post_auth_success from authentication.utils import check_different_city_login_if_need from common.const.signals import POST_ADD, POST_REMOVE, POST_CLEAR, SKIP_SIGNAL +from common.signals import django_ready from common.utils import get_request_ip, get_logger, get_syslogger from common.utils.encode import data_to_json from jumpserver.utils import current_request @@ -164,9 +166,10 @@ def on_object_created_or_update(sender, instance=None, created=False, update_fie log_id, before, after = get_instance_current_with_cache_diff(current_instance) resource_type = sender._meta.verbose_name + object_name = sender._meta.object_name create_or_update_operate_log( - action, resource_type, resource=instance, - log_id=log_id, before=before, after=after + action, resource_type, resource=instance, log_id=log_id, + before=before, after=after, object_name=object_name ) @@ -278,3 +281,34 @@ def on_user_auth_failed(sender, username, request, reason='', **kwargs): data = generate_data(username, request) data.update({'reason': reason[:128], 'status': False}) write_login_log(**data) + + +@receiver(django_ready) +def on_django_start_set_operate_log_monitor_models(sender, **kwargs): + exclude_label = { + 'django_cas_ng', 'captcha', 'admin', 'jms_oidc_rp', + 'django_celery_beat', 'contenttypes', 'sessions', 'auth' + } + exclude_object_name = { + 'UserPasswordHistory', 'ContentType', + 'SiteMessage', 'SiteMessageUsers', + 'PlatformAutomation', 'PlatformProtocol', 'Protocol', + 'HistoricalAccount', 'GatheredUser', 'ApprovalRule', + 'BaseAutomation', 'CeleryTask', 'Command', 'JobAuditLog', + 'ConnectionToken', 'SessionJoinRecord', + 'HistoricalJob', 'Status', 'TicketStep', 'Ticket', + 'UserAssetGrantedTreeNodeRelation', 'TicketAssignee', + 'SuperTicket', 'SuperConnectionToken', 'PermNode', + 'PermedAsset', 'PermedAccount', 'MenuPermission', + 'Permission', 'TicketSession', 'ApplyLoginTicket', + 'ApplyCommandTicket', 'ApplyLoginAssetTicket', + 'FTPLog', 'OperateLog', 'PasswordChangeLog' + } + for i, app in enumerate(apps.get_models(), 1): + app_label = app._meta.app_label + app_object_name = app._meta.object_name + if app_label in exclude_label or \ + app_object_name in exclude_object_name or \ + app_object_name.endswith('Execution'): + continue + MODELS_NEED_RECORD.add(app_object_name) diff --git a/apps/audits/urls/api_urls.py b/apps/audits/urls/api_urls.py index fa8ee63fc..96b89e52b 100644 --- a/apps/audits/urls/api_urls.py +++ b/apps/audits/urls/api_urls.py @@ -4,7 +4,6 @@ from __future__ import unicode_literals from django.urls.conf import re_path, path from rest_framework.routers import DefaultRouter -from common import api as capi from .. import api app_name = "audits" @@ -18,10 +17,7 @@ router.register(r'job-logs', api.JobAuditViewSet, 'job-log') urlpatterns = [ path('my-login-logs/', api.MyLoginLogAPIView.as_view(), name='my-login-log'), -] - -old_version_urlpatterns = [ - re_path('(?Pftp-log)/.*', capi.redirect_plural_name_api) + path('activities/', api.ResourceActivityAPIView.as_view(), name='resource-activities'), ] urlpatterns += router.urls diff --git a/apps/audits/utils.py b/apps/audits/utils.py index e62a40577..6f8f9e730 100644 --- a/apps/audits/utils.py +++ b/apps/audits/utils.py @@ -56,33 +56,56 @@ def get_resource_display(resource): return resource_display +def _get_instance_field_value( + instance, include_model_fields, + model_need_continue_fields, exclude_fields=None +): + data = {} + opts = getattr(instance, '_meta', None) + if opts is not None: + for f in chain(opts.concrete_fields, opts.private_fields): + if not include_model_fields and not getattr(f, 'primary_key', False): + continue + + if isinstance(f, (models.FileField, models.ImageField)): + continue + + if getattr(f, 'attname', None) in model_need_continue_fields: + continue + + value = getattr(instance, f.name) or getattr(instance, f.attname) + if not isinstance(value, bool) and not value: + continue + + if getattr(f, 'primary_key', False): + f.verbose_name = 'id' + elif isinstance(value, list): + value = [str(v) for v in value] + elif isinstance(f, models.OneToOneField) and isinstance(value, models.Model): + nested_data = _get_instance_field_value( + value, include_model_fields, model_need_continue_fields, ('id',) + ) + for k, v in nested_data.items(): + if exclude_fields and k in exclude_fields: + continue + data.setdefault(k, v) + continue + data.setdefault(str(f.verbose_name), value) + return data + + def model_to_dict_for_operate_log( instance, include_model_fields=True, include_related_fields=True ): model_need_continue_fields = ['date_updated'] m2m_need_continue_fields = ['history_passwords'] - opts = instance._meta - data = {} - for f in chain(opts.concrete_fields, opts.private_fields): - if isinstance(f, (models.FileField, models.ImageField)): - continue - if getattr(f, 'attname', None) in model_need_continue_fields: - continue - - value = getattr(instance, f.name) or getattr(instance, f.attname) - if not isinstance(value, bool) and not value: - continue - - if getattr(f, 'primary_key', False): - f.verbose_name = 'id' - elif isinstance(value, list): - value = [str(v) for v in value] - - if include_model_fields or getattr(f, 'primary_key', False): - data[str(f.verbose_name)] = value + data = _get_instance_field_value( + instance, include_model_fields, model_need_continue_fields + ) if include_related_fields: + opts = instance._meta for f in chain(opts.many_to_many, opts.related_objects): value = [] if instance.pk is not None: @@ -97,7 +120,7 @@ def model_to_dict_for_operate_log( continue try: field_key = getattr(f, 'verbose_name', None) or f.related_model._meta.verbose_name - data[str(field_key)] = value + data.setdefault(str(field_key), value) except: pass return data diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 782346fc5..ab2783b97 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -6,6 +6,7 @@ import urllib.parse from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied @@ -13,10 +14,11 @@ from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import ValidationError -from common.drf.api import JMSModelViewSet -from common.http import is_true +from common.api import JMSModelViewSet +from common.utils.http import is_true from common.utils import random_string from common.utils.django import get_request_os +from common.exceptions import JMSException from orgs.mixins.api import RootOrgViewMixin from perms.models import ActionChoices from terminal.connect_methods import NativeClient, ConnectMethodUtil @@ -158,7 +160,9 @@ class RDPFileClientProtocolURLMixin: def get_smart_endpoint(self, protocol, asset=None): target_ip = asset.get_target_ip() if asset else '' - endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request) + endpoint = EndpointRule.match_endpoint( + target_instance=asset, target_ip=target_ip, protocol=protocol, request=self.request + ) return endpoint @@ -231,8 +235,6 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView return super().perform_create(serializer) def validate_serializer(self, serializer): - from perms.utils.account import PermAccountUtil - data = serializer.validated_data user = self.get_user(serializer) asset = data.get('asset') @@ -241,23 +243,51 @@ class ConnectionTokenViewSet(ExtraActionApiMixin, RootOrgViewMixin, JMSModelView data['user'] = user data['value'] = random_string(16) - util = PermAccountUtil() - permed_account = util.validate_permission(user, asset, account_name) - - if not permed_account or not permed_account.actions: - msg = 'user `{}` not has asset `{}` permission for account `{}`'.format( - user, asset, account_name - ) - raise PermissionDenied(msg) - - if permed_account.date_expired < timezone.now(): - raise PermissionDenied('Expired') - - if permed_account.has_secret: + account = self._validate_perm(user, asset, account_name) + if account.has_secret: data['input_secret'] = '' - if permed_account.username != '@INPUT': + if account.username != '@INPUT': data['input_username'] = '' - return permed_account + + ticket = self._validate_acl(user, asset, account) + if ticket: + data['from_ticket'] = ticket + data['is_active'] = False + + return account + + @staticmethod + def _validate_perm(user, asset, account_name): + from perms.utils.account import PermAccountUtil + account = PermAccountUtil().validate_permission(user, asset, account_name) + if not account or not account.actions: + msg = _('Account not found') + raise JMSException(code='perm_account_invalid', detail=msg) + if account.date_expired < timezone.now(): + msg = _('Permission Expired') + raise JMSException(code='perm_expired', detail=msg) + return account + + def _validate_acl(self, user, asset, account): + from acls.models import LoginAssetACL + acl = LoginAssetACL.filter_queryset(user, asset, account).valid().first() + if not acl: + return + if acl.is_action(acl.ActionChoices.accept): + return + if acl.is_action(acl.ActionChoices.reject): + msg = _('ACL action is reject') + raise JMSException(code='acl_reject', detail=msg) + if acl.is_action(acl.ActionChoices.review): + if not self.request.query_params.get('create_ticket'): + msg = _('ACL action is review') + raise JMSException(code='acl_review', detail=msg) + + ticket = LoginAssetACL.create_login_asset_review_ticket( + user=user, asset=asset, account_username=account.username, + assignees=acl.reviewers.all(), org_id=asset.org_id + ) + return ticket class SuperConnectionTokenViewSet(ConnectionTokenViewSet): diff --git a/apps/authentication/api/dingtalk.py b/apps/authentication/api/dingtalk.py index 817b5276e..ee2bdae2e 100644 --- a/apps/authentication/api/dingtalk.py +++ b/apps/authentication/api/dingtalk.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/api/feishu.py b/apps/authentication/api/feishu.py index 878ec6e2d..5a6d3721e 100644 --- a/apps/authentication/api/feishu.py +++ b/apps/authentication/api/feishu.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/api/sso.py b/apps/authentication/api/sso.py index 88d9707db..c3b2d688c 100644 --- a/apps/authentication/api/sso.py +++ b/apps/authentication/api/sso.py @@ -11,8 +11,8 @@ from rest_framework.permissions import AllowAny from common.utils.timezone import utc_now from common.const.http import POST, GET -from common.drf.api import JMSGenericViewSet -from common.drf.serializers import EmptySerializer +from common.api import JMSGenericViewSet +from common.serializers import EmptySerializer from common.permissions import OnlySuperUser from common.utils import reverse from users.models import User diff --git a/apps/authentication/api/temp_token.py b/apps/authentication/api/temp_token.py index 2fa5791e3..bf8aaa413 100644 --- a/apps/authentication/api/temp_token.py +++ b/apps/authentication/api/temp_token.py @@ -3,7 +3,7 @@ from rest_framework.response import Response from rest_framework.decorators import action from rbac.permissions import RBACPermission -from common.drf.api import JMSModelViewSet +from common.api import JMSModelViewSet from ..models import TempToken from ..serializers import TempTokenSerializer diff --git a/apps/authentication/api/wecom.py b/apps/authentication/api/wecom.py index cdde00bc9..2779cc9eb 100644 --- a/apps/authentication/api/wecom.py +++ b/apps/authentication/api/wecom.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from users.models import User from common.utils import get_logger from common.permissions import UserConfirmation -from common.mixins.api import RoleUserMixin, RoleAdminMixin +from common.api import RoleUserMixin, RoleAdminMixin from authentication.const import ConfirmType from authentication import errors diff --git a/apps/authentication/migrations/0017_auto_20230105_1743.py b/apps/authentication/migrations/0017_auto_20230105_1743.py new file mode 100644 index 000000000..f4f9bff69 --- /dev/null +++ b/apps/authentication/migrations/0017_auto_20230105_1743.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.14 on 2023-01-05 09:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tickets', '0028_remove_app_tickets'), + ('authentication', '0016_auto_20221220_1956'), + ] + + operations = [ + migrations.AddField( + model_name='connectiontoken', + name='from_ticket', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='connection_token', to='tickets.applyloginassetticket', verbose_name='From ticket'), + ), + migrations.AddField( + model_name='connectiontoken', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + ] diff --git a/apps/authentication/models/connection_token.py b/apps/authentication/models/connection_token.py index bb7d282b1..385294122 100644 --- a/apps/authentication/models/connection_token.py +++ b/apps/authentication/models/connection_token.py @@ -11,7 +11,7 @@ from rest_framework.exceptions import PermissionDenied from assets.const import Protocol from common.db.fields import EncryptCharField -from common.utils import lazyproperty, pretty_string, bulk_get +from common.utils import lazyproperty, pretty_string, bulk_get, reverse from common.utils.timezone import as_current_tz from orgs.mixins.models import JMSOrgBaseModel from terminal.models import Applet @@ -39,6 +39,12 @@ class ConnectionToken(JMSOrgBaseModel): user_display = models.CharField(max_length=128, default='', verbose_name=_("User display")) asset_display = models.CharField(max_length=128, default='', verbose_name=_("Asset display")) date_expired = models.DateTimeField(default=date_expired_default, verbose_name=_("Date expired")) + from_ticket = models.OneToOneField( + 'tickets.ApplyLoginAssetTicket', related_name='connection_token', + on_delete=models.SET_NULL, null=True, blank=True, + verbose_name=_('From ticket') + ) + is_active = models.BooleanField(default=True, verbose_name=_("Active")) class Meta: ordering = ('-date_expired',) @@ -90,6 +96,9 @@ class ConnectionToken(JMSOrgBaseModel): return self.permed_account.date_expired.timestamp() def is_valid(self): + if not self.is_active: + error = _('Connection token inactive') + raise PermissionDenied(error) if self.is_expired: error = _('Connection token expired at: {}').format(as_current_tz(self.date_expired)) raise PermissionDenied(error) @@ -192,7 +201,7 @@ class ConnectionToken(JMSOrgBaseModel): @lazyproperty def account_object(self): - from assets.models import Account + from accounts.models import Account if not self.asset: return None diff --git a/apps/authentication/serializers/confirm.py b/apps/authentication/serializers/confirm.py index 9c0da463c..5d747c656 100644 --- a/apps/authentication/serializers/confirm.py +++ b/apps/authentication/serializers/confirm.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from ..const import ConfirmType, MFAType diff --git a/apps/authentication/serializers/connect_token_secret.py b/apps/authentication/serializers/connect_token_secret.py index ac9345ae5..4585111ea 100644 --- a/apps/authentication/serializers/connect_token_secret.py +++ b/apps/authentication/serializers/connect_token_secret.py @@ -1,12 +1,13 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from accounts.const import SecretType +from accounts.models import Account from acls.models import CommandGroup, CommandFilterACL -from assets.const import SecretType -from assets.models import Asset, Account, Platform, Gateway, Domain +from assets.models import Asset, Platform, Gateway, Domain from assets.serializers import PlatformSerializer, AssetProtocolsSerializer -from common.drf.fields import LabeledChoiceField -from common.drf.fields import ObjectRelatedField +from common.serializers.fields import LabeledChoiceField +from common.serializers.fields import ObjectRelatedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from perms.serializers.permission import ActionChoicesField from users.models import User @@ -51,16 +52,15 @@ class _ConnectionTokenAccountSerializer(serializers.ModelSerializer): class Meta: model = Account fields = [ - 'name', 'username', 'secret_type', 'secret', 'su_from', + 'name', 'username', 'secret_type', 'secret', 'su_from', 'privileged' ] class _ConnectionTokenGatewaySerializer(serializers.ModelSerializer): """ Gateway """ - account = ObjectRelatedField( - required=False, source='select_account', queryset=Account.objects, - attrs=('id', 'name', 'username', 'secret', 'secret_type') + account = _SimpleAccountSerializer( + required=False, source='select_account', read_only=True ) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) @@ -135,6 +135,7 @@ class ConnectionTokenSecretSerializer(OrgResourceModelSerializerMixin): 'id', 'value', 'user', 'asset', 'account', 'platform', 'command_filter_acls', 'protocol', 'domain', 'gateway', 'actions', 'expire_at', + 'from_ticket', 'expire_now', 'connect_method', ] extra_kwargs = { diff --git a/apps/authentication/serializers/connection_token.py b/apps/authentication/serializers/connection_token.py index 2fd9ae16e..c2805c78d 100644 --- a/apps/authentication/serializers/connection_token.py +++ b/apps/authentication/serializers/connection_token.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from ..models import ConnectionToken @@ -13,8 +13,9 @@ __all__ = [ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): expire_time = serializers.IntegerField(read_only=True, label=_('Expired time')) input_secret = EncryptedField( - label=_("Input secret"), max_length=40960, required=False, allow_blank=True + label=_("Input secret"), max_length=40960, required=False, allow_blank=True ) + from_ticket_info = serializers.SerializerMethodField(label=_("Ticket info")) class Meta: model = ConnectionToken @@ -22,6 +23,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): fields_small = fields_mini + [ 'user', 'asset', 'account', 'input_username', 'input_secret', 'connect_method', 'protocol', 'actions', + 'is_active', 'from_ticket', 'from_ticket_info', 'date_expired', 'date_created', 'date_updated', 'created_by', 'updated_by', 'org_id', 'org_name', ] @@ -32,14 +34,25 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin): ] fields = fields_small + read_only_fields extra_kwargs = { + 'from_ticket': {'read_only': True}, 'value': {'read_only': True}, } - def get_user(self, attrs): + def get_request_user(self): request = self.context.get('request') user = request.user if request else None return user + def get_user(self, attrs): + return self.get_request_user() + + def get_from_ticket_info(self, instance): + if not instance.from_ticket: + return {} + user = self.get_request_user() + info = instance.from_ticket.get_extra_info_of_review(user=user) + return info + class SuperConnectionTokenSerializer(ConnectionTokenSerializer): class Meta(ConnectionTokenSerializer.Meta): diff --git a/apps/authentication/serializers/password_mfa.py b/apps/authentication/serializers/password_mfa.py index 7684c3f88..d83260748 100644 --- a/apps/authentication/serializers/password_mfa.py +++ b/apps/authentication/serializers/password_mfa.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'MFAChallengeSerializer', 'MFASelectTypeSerializer', diff --git a/apps/authentication/views/dingtalk.py b/apps/authentication/views/dingtalk.py index 340ece19c..e0e76f966 100644 --- a/apps/authentication/views/dingtalk.py +++ b/apps/authentication/views/dingtalk.py @@ -13,7 +13,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.dingtalk import URL, DingTalk from common.utils import FlashMessageUtil, get_logger @@ -27,7 +27,6 @@ from .mixins import METAMixin logger = get_logger(__file__) - DINGTALK_STATE_SESSION_KEY = '_dingtalk_state' @@ -201,7 +200,7 @@ class DingTalkEnableStartView(UserVerifyPasswordView): class DingTalkQRLoginView(DingTalkQRMixin, METAMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') @@ -259,7 +258,7 @@ class DingTalkQRLoginCallbackView(AuthMixin, DingTalkQRMixin, View): class DingTalkOAuthLoginView(DingTalkOAuthMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') redirect_uri = reverse('authentication:dingtalk-oauth-login-callback', external=True) diff --git a/apps/authentication/views/feishu.py b/apps/authentication/views/feishu.py index 4fdf6f846..16e111bad 100644 --- a/apps/authentication/views/feishu.py +++ b/apps/authentication/views/feishu.py @@ -13,7 +13,7 @@ from authentication import errors from authentication.const import ConfirmType from authentication.mixins import AuthMixin from authentication.notifications import OAuthBindMessage -from common.mixins.views import PermissionsMixin, UserConfirmRequiredExceptionMixin +from common.views.mixins import PermissionsMixin, UserConfirmRequiredExceptionMixin from common.permissions import UserConfirmation from common.sdk.im.feishu import URL, FeiShu from common.utils import FlashMessageUtil, get_logger @@ -25,7 +25,6 @@ from users.views import UserVerifyPasswordView logger = get_logger(__file__) - FEISHU_STATE_SESSION_KEY = '_feishu_state' @@ -166,7 +165,7 @@ class FeiShuEnableStartView(UserVerifyPasswordView): class FeiShuQRLoginView(FeiShuQRMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') redirect_uri = reverse('authentication:feishu-qr-login-callback', external=True) redirect_uri += '?' + urlencode({ diff --git a/apps/authentication/views/wecom.py b/apps/authentication/views/wecom.py index b896aa19e..c764c2138 100644 --- a/apps/authentication/views/wecom.py +++ b/apps/authentication/views/wecom.py @@ -15,7 +15,7 @@ from common.utils.random import random_string from common.utils.django import reverse, get_object_or_none from common.sdk.im.wecom import URL from common.sdk.im.wecom import WeCom -from common.mixins.views import UserConfirmRequiredExceptionMixin, PermissionsMixin +from common.views.mixins import UserConfirmRequiredExceptionMixin, PermissionsMixin from common.utils.common import get_request_ip from common.permissions import UserConfirmation from authentication import errors @@ -26,7 +26,6 @@ from .mixins import METAMixin logger = get_logger(__file__) - WECOM_STATE_SESSION_KEY = '_wecom_state' @@ -196,7 +195,7 @@ class WeComEnableStartView(UserVerifyPasswordView): class WeComQRLoginView(WeComQRMixin, METAMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') or reverse('index') next_url = self.get_next_url_from_meta() or reverse('index') redirect_uri = reverse('authentication:wecom-qr-login-callback', external=True) @@ -253,7 +252,7 @@ class WeComQRLoginCallbackView(AuthMixin, WeComQRMixin, View): class WeComOAuthLoginView(WeComOAuthMixin, View): permission_classes = (AllowAny,) - def get(self, request: HttpRequest): + def get(self, request: HttpRequest): redirect_url = request.GET.get('redirect_url') redirect_uri = reverse('authentication:wecom-oauth-login-callback', external=True) diff --git a/apps/common/mixins/api/__init__.py b/apps/common/api/__init__.py similarity index 75% rename from apps/common/mixins/api/__init__.py rename to apps/common/api/__init__.py index a5827ffef..7f2c4dbc9 100644 --- a/apps/common/mixins/api/__init__.py +++ b/apps/common/api/__init__.py @@ -1,7 +1,8 @@ -from .common import * from .action import * -from .patch import * +from .common import * from .filter import * +from .generic import * +from .mixin import * +from .patch import * from .permission import * -from .queryset import * from .serializer import * diff --git a/apps/common/mixins/api/action.py b/apps/common/api/action.py similarity index 100% rename from apps/common/mixins/api/action.py rename to apps/common/api/action.py diff --git a/apps/common/api.py b/apps/common/api/common.py similarity index 87% rename from apps/common/api.py rename to apps/common/api/common.py index 5bf20f027..8dfbc1ca9 100644 --- a/apps/common/api.py +++ b/apps/common/api/common.py @@ -9,16 +9,14 @@ from django.views.decorators.csrf import csrf_exempt from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import generics, serializers -from rest_framework.viewsets import GenericViewSet from common.permissions import IsValidUser -from .http import HttpResponseTemporaryRedirect -from .const import KEY_CACHE_RESOURCE_IDS -from .utils import get_logger -from .mixins import CommonApiMixin +from common.views.http import HttpResponseTemporaryRedirect +from common.utils import get_logger +from common.const import KEY_CACHE_RESOURCE_IDS __all__ = [ - 'LogTailApi', 'ResourcesIDCacheApi', 'CommonGenericViewSet' + 'LogTailApi', 'ResourcesIDCacheApi' ] logger = get_logger(__file__) @@ -102,10 +100,6 @@ class ResourcesIDCacheApi(APIView): def redirect_plural_name_api(request, *args, **kwargs): resource = kwargs.get("resource", "") org_full_path = request.get_full_path() - full_path = org_full_path.replace(resource, resource+"s", 1) + full_path = org_full_path.replace(resource, resource + "s", 1) logger.debug("Redirect {} => {}".format(org_full_path, full_path)) return HttpResponseTemporaryRedirect(full_path) - - -class CommonGenericViewSet(CommonApiMixin, GenericViewSet): - pass diff --git a/apps/common/mixins/api/filter.py b/apps/common/api/filter.py similarity index 100% rename from apps/common/mixins/api/filter.py rename to apps/common/api/filter.py diff --git a/apps/common/api/generic.py b/apps/common/api/generic.py new file mode 100644 index 000000000..6a02f125e --- /dev/null +++ b/apps/common/api/generic.py @@ -0,0 +1,21 @@ +from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework_bulk import BulkModelViewSet + +from .mixin import CommonApiMixin, RelationMixin +from .permission import AllowBulkDestroyMixin + + +class JMSGenericViewSet(CommonApiMixin, GenericViewSet): + pass + + +class JMSModelViewSet(CommonApiMixin, ModelViewSet): + pass + + +class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet): + pass + + +class JMSBulkRelationModelViewSet(CommonApiMixin, RelationMixin, AllowBulkDestroyMixin, BulkModelViewSet): + pass diff --git a/apps/common/mixins/api/common.py b/apps/common/api/mixin.py similarity index 84% rename from apps/common/mixins/api/common.py rename to apps/common/api/mixin.py index 20e2eabc7..9517227a0 100644 --- a/apps/common/mixins/api/common.py +++ b/apps/common/api/mixin.py @@ -1,16 +1,14 @@ # -*- coding: utf-8 -*- # -from typing import Callable -from rest_framework.response import Response from collections import defaultdict +from typing import Callable from django.db.models.signals import m2m_changed +from rest_framework.response import Response -from .serializer import SerializerMixin -from .filter import ExtraFilterFieldsMixin from .action import RenderToJsonMixin -from .queryset import QuerySetMixin - +from .filter import ExtraFilterFieldsMixin +from .serializer import SerializerMixin __all__ = [ 'CommonApiMixin', 'PaginatedResponseMixin', 'RelationMixin', @@ -82,11 +80,21 @@ class RelationMixin: self.send_m2m_changed_signal(instance, 'post_remove') +class QuerySetMixin: + action: str + get_serializer_class: Callable + get_queryset: Callable + + def get_queryset(self): + queryset = super().get_queryset() + if hasattr(self, 'action') and (self.action == 'list' or self.action == 'metadata'): + serializer_class = self.get_serializer_class() + if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): + queryset = serializer_class.setup_eager_loading(queryset) + return queryset + + class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin, QuerySetMixin, RenderToJsonMixin, PaginatedResponseMixin): pass - - - - diff --git a/apps/common/mixins/api/patch.py b/apps/common/api/patch.py similarity index 100% rename from apps/common/mixins/api/patch.py rename to apps/common/api/patch.py diff --git a/apps/common/mixins/api/permission.py b/apps/common/api/permission.py similarity index 90% rename from apps/common/mixins/api/permission.py rename to apps/common/api/permission.py index 3a57658c3..19f63824f 100644 --- a/apps/common/mixins/api/permission.py +++ b/apps/common/api/permission.py @@ -5,7 +5,6 @@ from rest_framework.request import Request from common.utils import lazyproperty - __all__ = ['AllowBulkDestroyMixin', 'RoleAdminMixin', 'RoleUserMixin'] @@ -15,7 +14,8 @@ class AllowBulkDestroyMixin: 我们规定,批量删除的情况必须用 `id` 指定要删除的数据。 """ query = str(filtered.query) - return '`id` IN (' in query or '`id` =' in query + can = '`id` IN (' in query or '`id` =' in query or 'ptr_id` IN (' in query + return can class RoleAdminMixin: diff --git a/apps/common/mixins/api/serializer.py b/apps/common/api/serializer.py similarity index 100% rename from apps/common/mixins/api/serializer.py rename to apps/common/api/serializer.py diff --git a/apps/common/compat.py b/apps/common/compat.py deleted file mode 100644 index f2e757625..000000000 --- a/apps/common/compat.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# - -""" -兼容Python版本 -""" - -import sys - -is_py2 = (sys.version_info[0] == 2) -is_py3 = (sys.version_info[0] == 3) - - -try: - import simplejson as json -except (ImportError, SyntaxError): - import json - - -if is_py2: - - def to_bytes(data): - """若输入为unicode, 则转为utf-8编码的bytes;其他则原样返回。""" - if isinstance(data, unicode): - return data.encode('utf-8') - else: - return data - - def to_string(data): - """把输入转换为str对象""" - return to_bytes(data) - - def to_unicode(data): - """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" - if isinstance(data, bytes): - return data.decode('utf-8') - else: - return data - - def stringify(input): - if isinstance(input, dict): - return dict([(stringify(key), stringify(value)) for key,value in input.iteritems()]) - elif isinstance(input, list): - return [stringify(element) for element in input] - elif isinstance(input, unicode): - return input.encode('utf-8') - else: - return input - - builtin_str = str - bytes = str - str = unicode - - -elif is_py3: - - def to_bytes(data): - """若输入为str(即unicode),则转为utf-8编码的bytes;其他则原样返回""" - if isinstance(data, str): - return data.encode(encoding='utf-8') - else: - return data - - def to_string(data): - """若输入为bytes,则认为是utf-8编码,并返回str""" - if isinstance(data, bytes): - return data.decode('utf-8') - else: - return data - - def to_unicode(data): - """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" - return to_string(data) - - def stringify(input): - return input - - builtin_str = str - bytes = bytes - str = str - diff --git a/apps/common/db/fields.py b/apps/common/db/fields.py index d4a58e618..d66eb229c 100644 --- a/apps/common/db/fields.py +++ b/apps/common/db/fields.py @@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _ from common.local import add_encrypted_field_set from common.utils import signer, crypto +from .validators import PortRangeValidator __all__ = [ "JsonMixin", @@ -27,7 +28,9 @@ __all__ = [ "EncryptJsonDictTextField", "EncryptJsonDictCharField", "PortField", + "PortRangeField", "BitChoices", + "TreeChoices", ] @@ -216,15 +219,15 @@ class PortField(models.IntegerField): super().__init__(*args, **kwargs) -class BitChoices(models.IntegerChoices): +class TreeChoices(models.Choices): + @classmethod + def is_tree(cls): + return True + @classmethod def branches(cls): return [i for i in cls] - @classmethod - def is_tree(cls): - return False - @classmethod def tree(cls): if not cls.is_tree(): @@ -234,7 +237,7 @@ class BitChoices(models.IntegerChoices): @classmethod def render_node(cls, node): - if isinstance(node, BitChoices): + if isinstance(node, models.Choices): return { "value": node.name, "label": node.label, @@ -247,9 +250,26 @@ class BitChoices(models.IntegerChoices): "children": [cls.render_node(child) for child in children], } + @classmethod + def all(cls): + return [i[0] for i in cls.choices] + + +class BitChoices(models.IntegerChoices, TreeChoices): + @classmethod + def is_tree(cls): + return False + @classmethod def all(cls): value = 0 for c in cls: value |= c.value return value + + +class PortRangeField(models.CharField): + def __init__(self, **kwargs): + kwargs['max_length'] = 16 + super().__init__(**kwargs) + self.validators.append(PortRangeValidator()) diff --git a/apps/common/db/managers.py b/apps/common/db/managers.py new file mode 100644 index 000000000..053178071 --- /dev/null +++ b/apps/common/db/managers.py @@ -0,0 +1,13 @@ +from django.db import models + + +class DebugQueryManager(models.Manager): + def get_queryset(self): + import traceback + lines = traceback.format_stack() + print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") + for line in lines[-10:-1]: + print(line) + print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") + queryset = super().get_queryset() + return queryset diff --git a/apps/common/mixins/models.py b/apps/common/db/mixins.py similarity index 76% rename from apps/common/mixins/models.py rename to apps/common/db/mixins.py index f6705f53c..43c5f8a65 100644 --- a/apps/common/mixins/models.py +++ b/apps/common/db/mixins.py @@ -41,15 +41,3 @@ class NoDeleteModelMixin(models.Model): self.is_discard = True self.discard_time = timezone.now() return self.save() - - -class DebugQueryManager(models.Manager): - def get_queryset(self): - import traceback - lines = traceback.format_stack() - print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") - for line in lines[-10:-1]: - print(line) - print("<<<<<<<<<<<<<<<<<<<<<<<<<<<<") - queryset = super().get_queryset() - return queryset diff --git a/apps/common/db/models.py b/apps/common/db/models.py index ae726b85d..4d292827a 100644 --- a/apps/common/db/models.py +++ b/apps/common/db/models.py @@ -21,47 +21,6 @@ from django.utils.translation import ugettext_lazy as _ from ..const.signals import SKIP_SIGNAL -class BitOperationChoice: - NONE = 0 - NAME_MAP: dict - DB_CHOICES: tuple - NAME_MAP_REVERSE: dict - - @classmethod - def value_to_choices(cls, value): - if isinstance(value, list): - return value - value = int(value) - choices = [cls.NAME_MAP[i] for i, j in cls.DB_CHOICES if value & i == i] - return choices - - @classmethod - def value_to_choices_display(cls, value): - choices = cls.value_to_choices(value) - return [str(dict(cls.choices())[i]) for i in choices] - - @classmethod - def choices_to_value(cls, value): - if not isinstance(value, list): - return cls.NONE - db_value = [ - cls.NAME_MAP_REVERSE[v] for v in value - if v in cls.NAME_MAP_REVERSE.keys() - ] - if not db_value: - return cls.NONE - - def to_choices(x, y): - return x | y - - result = reduce(to_choices, db_value) - return result - - @classmethod - def choices(cls): - return [(cls.NAME_MAP[i], j) for i, j in cls.DB_CHOICES] - - class ChoicesMixin: _value2label_map_: dict diff --git a/apps/common/db/validators.py b/apps/common/db/validators.py new file mode 100644 index 000000000..9f8db0278 --- /dev/null +++ b/apps/common/db/validators.py @@ -0,0 +1,22 @@ +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + + +class PortRangeValidator: + def __init__(self, start=1, end=65535): + self.start = start + self.end = end + self.error_message = _("Invalid port range, should be like and within {}-{}").format(start, end) + + def __call__(self, data): + try: + _range = data.split('-') + if len(_range) != 2: + raise ValueError('') + _range = [int(i) for i in _range] + if _range[0] > _range[1]: + raise ValueError('') + if _range[0] < self.start or _range[1] > self.end: + raise ValueError('') + except ValueError: + raise ValidationError(self.error_message) diff --git a/apps/common/drf/api.py b/apps/common/drf/api.py deleted file mode 100644 index 23567aa32..000000000 --- a/apps/common/drf/api.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet, ViewSet -from rest_framework_bulk import BulkModelViewSet - -from ..mixins.api import ( - RelationMixin, AllowBulkDestroyMixin, CommonApiMixin -) - - -class JMSGenericViewSet(CommonApiMixin, GenericViewSet): - pass - - -class JMSViewSet(CommonApiMixin, ViewSet): - pass - - -class JMSModelViewSet(CommonApiMixin, ModelViewSet): - pass - - -class JMSReadOnlyModelViewSet(CommonApiMixin, ReadOnlyModelViewSet): - pass - - -class JMSBulkModelViewSet(CommonApiMixin, AllowBulkDestroyMixin, BulkModelViewSet): - pass - - -class JMSBulkRelationModelViewSet(CommonApiMixin, - RelationMixin, - AllowBulkDestroyMixin, - BulkModelViewSet): - pass diff --git a/apps/common/drf/exc_handlers.py b/apps/common/drf/exc_handlers.py index b99a53547..f832042a7 100644 --- a/apps/common/drf/exc_handlers.py +++ b/apps/common/drf/exc_handlers.py @@ -1,13 +1,14 @@ +from logging import getLogger + from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist +from django.db.models.deletion import ProtectedError from django.http import Http404 from django.utils.translation import gettext -from django.db.models.deletion import ProtectedError from rest_framework import exceptions -from rest_framework.views import set_rollback from rest_framework.response import Response +from rest_framework.views import set_rollback from common.exceptions import JMSObjectDoesNotExist, ReferencedByOthers -from logging import getLogger logger = getLogger('drf_exception') unexpected_exception_logger = getLogger('unexpected_exception') @@ -27,7 +28,7 @@ def extract_object_name(exc, index=0): def common_exception_handler(exc, context): - logger.exception('') + # logger.exception('') if isinstance(exc, Http404): exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1)) diff --git a/apps/common/drf/metadata.py b/apps/common/drf/metadata.py index a4b5bf7ad..731daf878 100644 --- a/apps/common/drf/metadata.py +++ b/apps/common/drf/metadata.py @@ -14,7 +14,7 @@ from rest_framework.fields import empty from rest_framework.metadata import SimpleMetadata from rest_framework.request import clone_request -from common.drf.fields import TreeChoicesMixin +from common.serializers.fields import TreeChoicesField class SimpleMetadataWithFilters(SimpleMetadata): @@ -22,14 +22,9 @@ class SimpleMetadataWithFilters(SimpleMetadata): methods = {"PUT", "POST", "GET", "PATCH"} attrs = [ - "read_only", - "label", - "help_text", - "min_length", - "max_length", - "min_value", - "max_value", - "write_only", + "read_only", "label", "help_text", + "min_length", "max_length", "min_value", + "max_value", "write_only", ] def determine_actions(self, request, view): @@ -71,6 +66,8 @@ class SimpleMetadataWithFilters(SimpleMetadata): class_name = field.__class__.__name__ if class_name == "LabeledChoiceField": tp = "labeled_choice" + elif class_name == "JSONField": + tp = 'json' elif class_name == "ObjectRelatedField": tp = "object_related_field" elif class_name == "ManyRelatedField": @@ -119,7 +116,7 @@ class SimpleMetadataWithFilters(SimpleMetadata): elif getattr(field, "fields", None): field_info["children"] = self.get_serializer_info(field) - if isinstance(field, TreeChoicesMixin): + if isinstance(field, TreeChoicesField): self.set_tree_field(field, field_info) elif isinstance(field, serializers.ChoiceField): self.set_choices_field(field, field_info) diff --git a/apps/common/management/commands/services/services/celery_base.py b/apps/common/management/commands/services/services/celery_base.py index 92fa99cba..7d69139b2 100644 --- a/apps/common/management/commands/services/services/celery_base.py +++ b/apps/common/management/commands/services/services/celery_base.py @@ -1,5 +1,5 @@ -from ..hands import * from .base import BaseService +from ..hands import * class CeleryBaseService(BaseService): @@ -12,9 +12,12 @@ class CeleryBaseService(BaseService): @property def cmd(self): print('\n- Start Celery as Distributed Task Queue: {}'.format(self.queue.capitalize())) - + ansible_config_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'ansible.cfg') + ansible_modules_path = os.path.join(settings.APPS_DIR, 'ops', 'ansible', 'modules') os.environ.setdefault('PYTHONOPTIMIZE', '1') os.environ.setdefault('ANSIBLE_FORCE_COLOR', 'True') + os.environ.setdefault('ANSIBLE_CONFIG', ansible_config_path) + os.environ.setdefault('ANSIBLE_LIBRARY', ansible_modules_path) if os.getuid() == 0: os.environ.setdefault('C_FORCE_ROOT', '1') diff --git a/apps/common/mixins/__init__.py b/apps/common/mixins/__init__.py deleted file mode 100644 index b2a7ec7e4..000000000 --- a/apps/common/mixins/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# -*- coding: utf-8 -*- -# -from .models import * -from .api import * -from .views import * diff --git a/apps/common/mixins/api/queryset.py b/apps/common/mixins/api/queryset.py deleted file mode 100644 index 4f56e8a51..000000000 --- a/apps/common/mixins/api/queryset.py +++ /dev/null @@ -1,14 +0,0 @@ -# -*- coding: utf-8 -*- -# - -__all__ = ['QuerySetMixin'] - - -class QuerySetMixin: - def get_queryset(self): - queryset = super().get_queryset() - serializer_class = self.get_serializer_class() - - if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): - queryset = serializer_class.setup_eager_loading(queryset) - return queryset diff --git a/apps/common/drf/serializers/__init__.py b/apps/common/serializers/__init__.py similarity index 100% rename from apps/common/drf/serializers/__init__.py rename to apps/common/serializers/__init__.py diff --git a/apps/common/drf/serializers/common.py b/apps/common/serializers/common.py similarity index 100% rename from apps/common/drf/serializers/common.py rename to apps/common/serializers/common.py diff --git a/apps/common/drf/fields.py b/apps/common/serializers/fields.py similarity index 77% rename from apps/common/drf/fields.py rename to apps/common/serializers/fields.py index 7c9f55eee..96ff7c103 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/serializers/fields.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- # -import six from django.core.exceptions import ObjectDoesNotExist from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from rest_framework.fields import ChoiceField, empty -from common.db.fields import BitChoices -from common.utils import decrypt_password +from common.db.fields import TreeChoices from common.local import add_encrypted_field_set +from common.utils import decrypt_password __all__ = [ "ReadableHiddenField", @@ -16,7 +15,8 @@ __all__ = [ "LabeledChoiceField", "ObjectRelatedField", "BitChoicesField", - "TreeChoicesMixin" + "TreeChoicesField", + "LabeledMultipleChoiceField", ] @@ -55,16 +55,14 @@ class LabeledChoiceField(ChoiceField): def __init__(self, *args, **kwargs): super(LabeledChoiceField, self).__init__(*args, **kwargs) self.choice_mapper = { - six.text_type(key): value for key, value in self.choices.items() + key: value for key, value in self.choices.items() } - def to_representation(self, value): - if value is None: - return value - return { - "value": value, - "label": self.choice_mapper.get(six.text_type(value), value), - } + def to_representation(self, key): + if key is None: + return key + label = self.choice_mapper.get(key) + return {"value": key, "label": label} def to_internal_value(self, data): if isinstance(data, dict): @@ -72,6 +70,31 @@ class LabeledChoiceField(ChoiceField): return super(LabeledChoiceField, self).to_internal_value(data) +class LabeledMultipleChoiceField(serializers.MultipleChoiceField): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.choice_mapper = { + key: value for key, value in self.choices.items() + } + + def to_representation(self, keys): + if keys is None: + return keys + return [ + {"value": key, "label": self.choice_mapper.get(key)} + for key in keys + ] + + def to_internal_value(self, data): + if not data: + return data + + if isinstance(data[0], dict): + return [item.get("value") for item in data] + else: + return data + + class ObjectRelatedField(serializers.RelatedField): default_error_messages = { "required": _("This field is required."), @@ -87,6 +110,8 @@ class ObjectRelatedField(serializers.RelatedField): def to_representation(self, value): data = {} for attr in self.attrs: + if not hasattr(value, attr): + continue data[attr] = getattr(value, attr) return data @@ -106,22 +131,28 @@ class ObjectRelatedField(serializers.RelatedField): self.fail("incorrect_type", data_type=type(pk).__name__) -class TreeChoicesMixin: - tree = [] - - -class BitChoicesField(TreeChoicesMixin, serializers.MultipleChoiceField): - """ - 位字段 - """ - +class TreeChoicesField(serializers.MultipleChoiceField): def __init__(self, choice_cls, **kwargs): - assert issubclass(choice_cls, BitChoices) + assert issubclass(choice_cls, TreeChoices) choices = [(c.name, c.label) for c in choice_cls] self.tree = choice_cls.tree() self._choice_cls = choice_cls super().__init__(choices=choices, **kwargs) + def to_internal_value(self, data): + if not data: + return data + if isinstance(data[0], dict): + return [item.get("value") for item in data] + else: + return data + + +class BitChoicesField(TreeChoicesField): + """ + 位字段 + """ + def to_representation(self, value): if isinstance(value, list) and len(value) == 1: # Swagger 会使用 field.choices.keys() 迭代传递进来 diff --git a/apps/common/drf/serializers/mixin.py b/apps/common/serializers/mixin.py similarity index 96% rename from apps/common/drf/serializers/mixin.py rename to apps/common/serializers/mixin.py index acda64505..749a39820 100644 --- a/apps/common/drf/serializers/mixin.py +++ b/apps/common/serializers/mixin.py @@ -8,13 +8,15 @@ from rest_framework.fields import SkipField, empty from rest_framework.settings import api_settings from rest_framework.utils import html -from common.drf.fields import EncryptedField -from ..fields import LabeledChoiceField, ObjectRelatedField +from common.serializers.fields import EncryptedField +from common.serializers.fields import LabeledChoiceField, ObjectRelatedField __all__ = [ 'BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin', - 'SecretReadableMixin', + 'SecretReadableMixin', 'CommonModelSerializer', + 'CommonBulkModelSerializer', + ] @@ -355,5 +357,13 @@ class CommonSerializerMixin(DynamicFieldsMixin, RelatedModelSerializerMixin, pass +class CommonModelSerializer(CommonSerializerMixin, serializers.ModelSerializer): + pass + + class CommonBulkSerializerMixin(BulkSerializerMixin, CommonSerializerMixin): pass + + +class CommonBulkModelSerializer(CommonBulkSerializerMixin, serializers.ModelSerializer): + pass diff --git a/apps/common/signal_handlers.py b/apps/common/signal_handlers.py index e14a9fdf8..4471edb94 100644 --- a/apps/common/signal_handlers.py +++ b/apps/common/signal_handlers.py @@ -34,6 +34,8 @@ class Counter: def on_request_finished_logging_db_query(sender, **kwargs): queries = connection.queries counters = defaultdict(Counter) + table_queries = defaultdict(list) + for query in queries: if not query['sql'] or not query['sql'].startswith('SELECT'): continue @@ -44,22 +46,38 @@ def on_request_finished_logging_db_query(sender, **kwargs): counters[table_name].time += float(time) counters['total'].counter += 1 counters['total'].time += float(time) + table_queries[table_name].append(query) counters = sorted(counters.items(), key=lambda x: x[1]) if not counters: return + method = 'GET' path = '/Unknown' current_request = get_current_request() if current_request: method = current_request.method path = current_request.get_full_path() + logger.debug(">>> [{}] {}".format(method, path)) for name, counter in counters: logger.debug("Query {:3} times using {:.2f}s {}".format( counter.counter, counter.time, name) ) + # print(">>> [{}] {}".format(method, path)) + # for table_name, queries in table_queries.items(): + # if table_name.startswith('rbac_') or table_name.startswith('auth_permission'): + # continue + # print("- Table: {}".format(table_name)) + # for i, query in enumerate(queries, 1): + # sql = query['sql'] + # if not sql or not sql.startswith('SELECT'): + # continue + # print('\t{}. {}'.format(i, sql)) + + on_request_finished_release_local(sender, **kwargs) + def on_request_finished_release_local(sender, **kwargs): thread_local.__release_local__() diff --git a/apps/common/utils/http.py b/apps/common/utils/http.py index ab39c4fa2..b684f004a 100644 --- a/apps/common/utils/http.py +++ b/apps/common/utils/http.py @@ -5,6 +5,8 @@ import threading import time from email.utils import formatdate +from rest_framework.serializers import BooleanField + _STRPTIME_LOCK = threading.Lock() _GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" @@ -37,4 +39,9 @@ def iso8601_to_unixtime(time_string): return to_unixtime(time_string, _ISO8601_FORMAT) +def get_remote_addr(request): + return request.META.get("HTTP_X_FORWARDED_HOST") or request.META.get("REMOTE_ADDR") + +def is_true(value): + return value in BooleanField.TRUE_VALUES diff --git a/apps/common/utils/verify_code.py b/apps/common/utils/verify_code.py index abf4d6056..85589de69 100644 --- a/apps/common/utils/verify_code.py +++ b/apps/common/utils/verify_code.py @@ -21,12 +21,12 @@ def send_async(sender): class SendAndVerifyCodeUtil(object): KEY_TMPL = 'auth-verify-code-{}' - def __init__(self, target, code=None, key=None, backend='email', timeout=60, **kwargs): - self.target = target + def __init__(self, target, code=None, key=None, backend='email', timeout=None, **kwargs): self.code = code - self.timeout = timeout + self.target = target self.backend = backend self.key = key or self.KEY_TMPL.format(target) + self.timeout = settings.VERIFY_CODE_TTL if timeout is None else timeout self.other_args = kwargs def gen_and_send_async(self): diff --git a/apps/common/views/__init__.py b/apps/common/views/__init__.py new file mode 100644 index 000000000..8f5452f1e --- /dev/null +++ b/apps/common/views/__init__.py @@ -0,0 +1 @@ +from .msg import * diff --git a/apps/common/http.py b/apps/common/views/http.py similarity index 56% rename from apps/common/http.py rename to apps/common/views/http.py index 5a3d85524..b1bff5754 100644 --- a/apps/common/http.py +++ b/apps/common/views/http.py @@ -3,8 +3,6 @@ from django.http import HttpResponse from django.utils.encoding import iri_to_uri -from rest_framework.serializers import BooleanField - class HttpResponseTemporaryRedirect(HttpResponse): status_code = 307 @@ -13,10 +11,3 @@ class HttpResponseTemporaryRedirect(HttpResponse): HttpResponse.__init__(self) self['Location'] = iri_to_uri(redirect_to) - -def get_remote_addr(request): - return request.META.get("HTTP_X_FORWARDED_HOST") or request.META.get("REMOTE_ADDR") - - -def is_true(value): - return value in BooleanField.TRUE_VALUES diff --git a/apps/common/mixins/views.py b/apps/common/views/mixins.py similarity index 98% rename from apps/common/mixins/views.py rename to apps/common/views/mixins.py index d1feb61d8..39b146b3f 100644 --- a/apps/common/mixins/views.py +++ b/apps/common/views/mixins.py @@ -8,7 +8,6 @@ from rest_framework.request import Request from common.exceptions import UserConfirmRequired from audits.handler import create_or_update_operate_log -from audits.models import OperateLog from audits.const import ActionChoices __all__ = [ diff --git a/apps/common/views.py b/apps/common/views/msg.py similarity index 100% rename from apps/common/views.py rename to apps/common/views/msg.py diff --git a/apps/jumpserver/api.py b/apps/jumpserver/api.py index 8c5f39642..a3e0dc7ac 100644 --- a/apps/jumpserver/api.py +++ b/apps/jumpserver/api.py @@ -343,7 +343,7 @@ class IndexApi(DateTimeMixin, DatesLoginMetricMixin, APIView): http_method_names = ['get'] def check_permissions(self, request): - return request.user.has_perm(['rbac.view_audit', 'rbac.view_console']) + return request.user.has_perm('rbac.view_audit | rbac.view_console') def get(self, request, *args, **kwargs): data = {} diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index d43384987..55b94c87f 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -464,6 +464,7 @@ class Config(dict): 'SECURITY_LUNA_REMEMBER_AUTH': True, 'SECURITY_WATERMARK_ENABLED': True, 'SECURITY_MFA_VERIFY_TTL': 3600, + 'VERIFY_CODE_TTL': 60, 'SECURITY_SESSION_SHARE': True, 'SECURITY_CHECK_DIFFERENT_CITY_LOGIN': True, 'OLD_PASSWORD_HISTORY_LIMIT_COUNT': 5, @@ -504,8 +505,8 @@ class Config(dict): 'GMSSL_ENABLED': False, # 操作日志变更字段的存储ES配置 'OPERATE_LOG_ELASTICSEARCH_CONFIG': {}, - # Magnus 组件需要监听的端口范围 - 'MAGNUS_PORTS': '30000-30100', + # Magnus 组件需要监听的 Oracle 端口范围 + 'MAGNUS_ORACLE_PORTS': '30000-30030', # 记录清理清理 'LOGIN_LOG_KEEP_DAYS': 200, @@ -834,11 +835,13 @@ class ConfigManager: sys.path.insert(0, PROJECT_DIR) try: from config import config as c + except ImportError: + return False + if c: self.from_object(c) return True - except ImportError: - pass - return False + else: + return False def load_from_yml(self): for i in ['config.yml', 'config.yaml']: diff --git a/apps/jumpserver/middleware.py b/apps/jumpserver/middleware.py index bf7aa7945..f76077b7f 100644 --- a/apps/jumpserver/middleware.py +++ b/apps/jumpserver/middleware.py @@ -1,17 +1,21 @@ # ~*~ coding: utf-8 ~*~ +import json import os import re -import pytz import time -import json -from django.utils import timezone -from django.shortcuts import HttpResponse +import pytz +from channels.db import database_sync_to_async from django.conf import settings from django.core.exceptions import MiddlewareNotUsed +from django.core.handlers.asgi import ASGIRequest from django.http.response import HttpResponseForbidden +from django.shortcuts import HttpResponse +from django.utils import timezone +from authentication.backends.drf import (SignatureAuthentication, + AccessTokenAuthentication) from .utils import set_current_request @@ -129,3 +133,35 @@ class EndMiddleware: response = self.get_response(request) request._e_time_end = time.time() return response + + +@database_sync_to_async +def get_signature_user(scope): + headers = dict(scope["headers"]) + if not headers.get(b'authorization'): + return + if scope['type'] == 'websocket': + scope['method'] = 'GET' + try: + # 因为 ws 使用的是 scope,所以需要转换成 request 对象,用于认证校验 + request = ASGIRequest(scope, None) + backends = [SignatureAuthentication(), + AccessTokenAuthentication()] + for backend in backends: + user, _ = backend.authenticate(request) + if user: + return user + except Exception as e: + print(e) + return None + + +class WsSignatureAuthMiddleware: + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + user = await get_signature_user(scope) + if user: + scope['user'] = user + return await self.app(scope, receive, send) diff --git a/apps/jumpserver/routing.py b/apps/jumpserver/routing.py index 773baee99..965a2b71b 100644 --- a/apps/jumpserver/routing.py +++ b/apps/jumpserver/routing.py @@ -5,13 +5,17 @@ from django.core.asgi import get_asgi_application from ops.urls.ws_urls import urlpatterns as ops_urlpatterns from notifications.urls.ws_urls import urlpatterns as notifications_urlpatterns from settings.urls.ws_urls import urlpatterns as setting_urlpatterns +from terminal.urls.ws_urls import urlpatterns as terminal_urlpatterns + +from .middleware import WsSignatureAuthMiddleware urlpatterns = [] -urlpatterns += ops_urlpatterns + notifications_urlpatterns + setting_urlpatterns +urlpatterns += ops_urlpatterns + \ + notifications_urlpatterns + \ + setting_urlpatterns + \ + terminal_urlpatterns application = ProtocolTypeRouter({ - 'websocket': AuthMiddlewareStack( - URLRouter(urlpatterns) - ), + 'websocket': WsSignatureAuthMiddleware(AuthMiddlewareStack(URLRouter(urlpatterns))), "http": get_asgi_application(), }) diff --git a/apps/jumpserver/settings/base.py b/apps/jumpserver/settings/base.py index d1fb7ba9a..d74e604d3 100644 --- a/apps/jumpserver/settings/base.py +++ b/apps/jumpserver/settings/base.py @@ -80,6 +80,7 @@ INSTALLED_APPS = [ 'orgs.apps.OrgsConfig', 'users.apps.UsersConfig', 'assets.apps.AssetsConfig', + 'accounts.apps.AccountsConfig', 'perms.apps.PermsConfig', 'ops.apps.OpsConfig', 'settings.apps.SettingsConfig', @@ -349,8 +350,10 @@ if REDIS_SENTINEL_SERVICE_NAME and REDIS_SENTINELS: DJANGO_REDIS_CONNECTION_FACTORY = 'django_redis.pool.SentinelConnectionFactory' else: REDIS_LOCATION_NO_DB = '%(protocol)s://:%(password)s@%(host)s:%(port)s/{}' % { - 'protocol': REDIS_PROTOCOL, 'password': CONFIG.REDIS_PASSWORD, - 'host': CONFIG.REDIS_HOST, 'port': CONFIG.REDIS_PORT, + 'protocol': REDIS_PROTOCOL, + 'password': CONFIG.REDIS_PASSWORD, + 'host': CONFIG.REDIS_HOST, + 'port': CONFIG.REDIS_PORT, } REDIS_CACHE_DEFAULT = { diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 37bb073c6..7cb92915d 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -50,6 +50,7 @@ SECURITY_PASSWORD_RULES = [ 'SECURITY_PASSWORD_NUMBER', 'SECURITY_PASSWORD_SPECIAL_CHAR' ] +VERIFY_CODE_TTL = CONFIG.VERIFY_CODE_TTL SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA SECURITY_SERVICE_ACCOUNT_REGISTRATION = CONFIG.SECURITY_SERVICE_ACCOUNT_REGISTRATION @@ -187,4 +188,4 @@ OPERATE_LOG_ELASTICSEARCH_CONFIG = CONFIG.OPERATE_LOG_ELASTICSEARCH_CONFIG MAX_LIMIT_PER_PAGE = CONFIG.MAX_LIMIT_PER_PAGE # Magnus DB Port -MAGNUS_PORTS = CONFIG.MAGNUS_PORTS +MAGNUS_ORACLE_PORTS = CONFIG.MAGNUS_ORACLE_PORTS diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index 92e72e843..0c35ba322 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -31,7 +31,6 @@ REST_FRAMEWORK = { ), 'DEFAULT_AUTHENTICATION_CLASSES': ( # 'rest_framework.authentication.BasicAuthentication', - 'authentication.backends.drf.AccessKeyAuthentication', 'authentication.backends.drf.AccessTokenAuthentication', 'authentication.backends.drf.PrivateTokenAuthentication', 'authentication.backends.drf.SignatureAuthentication', @@ -119,7 +118,6 @@ else: REDIS_LAYERS_SSL_PARAMS.pop('ssl', None) REDIS_LAYERS_HOST['address'] = '{}?{}'.format(REDIS_LAYERS_ADDRESS, urlencode(REDIS_LAYERS_SSL_PARAMS)) - CHANNEL_LAYERS = { 'default': { 'BACKEND': 'common.cache.RedisChannelLayer', diff --git a/apps/jumpserver/settings/logging.py b/apps/jumpserver/settings/logging.py index ec2b3d9c2..a021d1716 100644 --- a/apps/jumpserver/settings/logging.py +++ b/apps/jumpserver/settings/logging.py @@ -50,7 +50,7 @@ LOGGING = { 'encoding': 'utf8', 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', - 'maxBytes': 1024*1024*100, + 'maxBytes': 1024 * 1024 * 100, 'backupCount': 7, 'formatter': 'main', 'filename': JUMPSERVER_LOG_FILE, @@ -60,7 +60,7 @@ LOGGING = { 'level': 'DEBUG', 'class': 'logging.handlers.RotatingFileHandler', 'formatter': 'main', - 'maxBytes': 1024*1024*100, + 'maxBytes': 1024 * 1024 * 100, 'backupCount': 7, 'filename': ANSIBLE_LOG_FILE, }, @@ -136,12 +136,6 @@ LOGGING = { } } -if CONFIG.DEBUG_DEV: - LOGGING['loggers']['django.db'] = { - 'handlers': ['console', 'file'], - 'level': 'DEBUG' - } - SYSLOG_ENABLE = CONFIG.SYSLOG_ENABLE if CONFIG.SYSLOG_ADDR != '' and len(CONFIG.SYSLOG_ADDR.split(':')) == 2: diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 730796b1d..2218edd6c 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -13,6 +13,7 @@ api_v1 = [ path('index/', api.IndexApi.as_view()), path('users/', include('users.urls.api_urls', namespace='api-users')), path('assets/', include('assets.urls.api_urls', namespace='api-assets')), + path('accounts/', include('accounts.urls', namespace='api-accounts')), path('perms/', include('perms.urls.api_urls', namespace='api-perms')), path('terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), path('ops/', include('ops.urls.api_urls', namespace='api-ops')), @@ -43,7 +44,6 @@ if settings.XPACK_ENABLED: path('xpack/', include('xpack.urls.api_urls', namespace='api-xpack')) ) - urlpatterns = [ path('', views.IndexView.as_view(), name='index'), path('api/v1/', include(api_v1)), @@ -58,7 +58,7 @@ urlpatterns = [ # 静态文件处理路由 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ - + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # js i18n 路由文件 urlpatterns += [ @@ -78,6 +78,5 @@ if os.environ.get('DEBUG_TOOLBAR', False): path('__debug__/', include('debug_toolbar.urls')), ] - handler404 = 'jumpserver.views.handler404' handler500 = 'jumpserver.views.handler500' diff --git a/apps/jumpserver/views/index.py b/apps/jumpserver/views/index.py index 639f6d683..c443d9c40 100644 --- a/apps/jumpserver/views/index.py +++ b/apps/jumpserver/views/index.py @@ -1,7 +1,7 @@ from django.views.generic import View from django.shortcuts import redirect from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin __all__ = ['IndexView'] diff --git a/apps/jumpserver/views/other.py b/apps/jumpserver/views/other.py index 9cf5a5500..984898158 100644 --- a/apps/jumpserver/views/other.py +++ b/apps/jumpserver/views/other.py @@ -11,8 +11,7 @@ from django.views.decorators.csrf import csrf_exempt from django.http import HttpResponse from rest_framework.views import APIView -from common.http import HttpResponseTemporaryRedirect - +from common.views.http import HttpResponseTemporaryRedirect __all__ = [ 'LunaView', 'I18NView', 'KokoView', 'WsView', @@ -23,8 +22,9 @@ __all__ = [ class LunaView(View): def get(self, request): - msg = _("
Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
" - "If you see this page, prove that you are not accessing the nginx listening port. Good luck.") + msg = _( + "
Luna is a separately deployed program, you need to deploy Luna, koko, configure nginx for url distribution,
" + "If you see this page, prove that you are not accessing the nginx listening port. Good luck.") return HttpResponse(msg) diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index 3072a7280..c4d0c146f 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3969f0fedc2f554a846f5e5f48070db1f954e22cb2aea50dab766bc74fd8eafc -size 119587 +oid sha256:eb850ffd130e7cad2ea8c186f94a059c6a882dd1526f7a4c4a16d2fea2a1815b +size 119290 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index ff458f85e..063cce72e 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-27 12:11+0800\n" +"POT-Creation-Date: 2023-01-16 14:24+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,6 +18,684 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +#: accounts/api/automations/base.py:79 +msgid "The parameter 'action' must be [{}]" +msgstr "パラメータ 'action' は [{}] でなければなりません。" + +#: accounts/const/account.py:6 +#: accounts/serializers/automations/change_secret.py:33 +#: assets/models/_user.py:35 audits/signal_handlers.py:51 +#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:288 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 +#: users/forms/profile.py:22 users/serializers/user.py:97 +#: users/templates/users/_msg_user_created.html:13 +#: users/templates/users/user_password_verify.html:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 +msgid "Password" +msgstr "パスワード" + +#: accounts/const/account.py:7 +#: accounts/serializers/automations/change_secret.py:34 +#, fuzzy +msgid "SSH key" +msgstr "SSHキー" + +#: accounts/const/account.py:8 authentication/models/access_key.py:33 +msgid "Access key" +msgstr "アクセスキー" + +#: accounts/const/account.py:9 assets/models/_user.py:38 +#: authentication/models/sso_token.py:14 +msgid "Token" +msgstr "トークン" + +#: accounts/const/account.py:13 common/db/fields.py:235 +#: settings/serializers/terminal.py:14 +msgid "All" +msgstr "すべて" + +#: accounts/const/account.py:14 +msgid "Manual input" +msgstr "手動入力" + +#: accounts/const/account.py:15 +msgid "Dynamic user" +msgstr "動的コード" + +#: accounts/const/account.py:19 users/models/user.py:631 +msgid "Local" +msgstr "ローカル" + +#: accounts/const/account.py:20 +msgid "Collected" +msgstr "" + +#: accounts/const/automation.py:22 rbac/tree.py:51 +#, fuzzy +msgid "Push account" +msgstr "サービスアカウントです" + +#: accounts/const/automation.py:23 +#, fuzzy +msgid "Change secret" +msgstr "秘密を改める" + +#: accounts/const/automation.py:24 +#, fuzzy +msgid "Verify account" +msgstr "パスワード/キーの確認" + +#: accounts/const/automation.py:25 +#, fuzzy +msgid "Gather accounts" +msgstr "アカウントを集める" + +#: accounts/const/automation.py:43 +#, fuzzy +#| msgid "Set password" +msgid "Specific password" +msgstr "パスワードの設定" + +#: accounts/const/automation.py:44 +msgid "Random" +msgstr "" + +#: accounts/const/automation.py:48 ops/const.py:13 +msgid "Append SSH KEY" +msgstr "追加" + +#: accounts/const/automation.py:49 ops/const.py:14 +msgid "Empty and append SSH KEY" +msgstr "すべてクリアして追加" + +#: accounts/const/automation.py:50 ops/const.py:15 +msgid "Replace (The key generated by JumpServer) " +msgstr "置換(JumpServerによって生成された鍵)" + +#: accounts/const/automation.py:55 +#, fuzzy +#| msgid "Date created" +msgid "On asset create" +msgstr "作成された日付" + +#: accounts/const/automation.py:58 +#, fuzzy +#| msgid "After change" +msgid "On perm add user" +msgstr "変更後" + +#: accounts/const/automation.py:60 +msgid "On perm add user group" +msgstr "" + +#: accounts/const/automation.py:62 +#, fuzzy +#| msgid "permed assets" +msgid "On perm add asset" +msgstr "パーマ資産" + +#: accounts/const/automation.py:64 +#, fuzzy +#| msgid "After change" +msgid "On perm add node" +msgstr "変更後" + +#: accounts/const/automation.py:66 +#, fuzzy +msgid "On perm add account" +msgstr "アカウントを集める" + +#: accounts/const/automation.py:68 +#, fuzzy +#| msgid "Add asset to node" +msgid "On asset join node" +msgstr "ノードにアセットを追加する" + +#: accounts/const/automation.py:70 +#, fuzzy +#| msgid "User group" +msgid "On user join group" +msgstr "ユーザーグループ" + +#: accounts/const/automation.py:78 +#, fuzzy +#| msgid "After change" +msgid "On perm change" +msgstr "変更後" + +#: accounts/const/automation.py:85 +#, fuzzy +#| msgid "Perm ungroup node" +msgid "Inherit from group or node" +msgstr "グループ化されていないノードを表示" + +#: accounts/const/automation.py:93 +#, fuzzy +#| msgid "Created by" +msgid "Create and push" +msgstr "によって作成された" + +#: accounts/const/automation.py:94 +#, fuzzy +#| msgid "Date created" +msgid "Only create" +msgstr "作成された日付" + +#: accounts/models/account.py:47 accounts/serializers/account/account.py:77 +#: accounts/serializers/automations/change_secret.py:107 +#: accounts/serializers/automations/change_secret.py:127 acls/models/base.py:96 +#: acls/serializers/base.py:56 assets/models/asset/common.py:96 +#: assets/models/asset/common.py:281 assets/models/cmd_filter.py:36 +#: assets/serializers/domain.py:19 assets/serializers/label.py:27 +#: audits/models.py:34 authentication/models/connection_token.py:32 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 +#: terminal/models/session/session.py:31 terminal/notifications.py:93 +#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:220 +msgid "Asset" +msgstr "資産" + +#: accounts/models/account.py:51 accounts/serializers/account/account.py:81 +#: authentication/serializers/connect_token_secret.py:49 +msgid "Su from" +msgstr "から切り替え" + +#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: terminal/models/applet/applet.py:25 +msgid "Version" +msgstr "バージョン" + +#: accounts/models/account.py:55 accounts/serializers/account/account.py:78 +#: users/models/user.py:727 +msgid "Source" +msgstr "ソース" + +#: accounts/models/account.py:58 +#: accounts/serializers/automations/change_secret.py:108 +#: accounts/serializers/automations/change_secret.py:128 acls/models/base.py:98 +#: acls/serializers/base.py:57 assets/serializers/asset/common.py:125 +#: assets/serializers/gateway.py:30 audits/models.py:35 ops/models/base.py:18 +#: terminal/backends/command/models.py:22 terminal/models/session/session.py:33 +#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 +msgid "Account" +msgstr "アカウント" + +#: accounts/models/account.py:64 +msgid "Can view asset account secret" +msgstr "資産アカウントの秘密を表示できます" + +#: accounts/models/account.py:65 +msgid "Can change asset account secret" +msgstr "資産口座の秘密を変更できます" + +#: accounts/models/account.py:66 +msgid "Can view asset history account" +msgstr "資産履歴アカウントを表示できます" + +#: accounts/models/account.py:67 +msgid "Can view asset history account secret" +msgstr "資産履歴アカウントパスワードを表示できます" + +#: accounts/models/account.py:104 accounts/serializers/account/account.py:16 +#, fuzzy +msgid "Account template" +msgstr "アカウント名" + +#: accounts/models/account.py:109 +#, fuzzy +msgid "Can view asset account template secret" +msgstr "資産アカウントの秘密を表示できます" + +#: accounts/models/account.py:110 +#, fuzzy +msgid "Can change asset account template secret" +msgstr "資産口座の秘密を変更できます" + +#: accounts/models/automations/backup_account.py:25 +#: accounts/models/automations/change_secret.py:47 +#: accounts/serializers/account/backup.py:29 +#: accounts/serializers/automations/change_secret.py:56 +msgid "Recipient" +msgstr "受信者" + +#: accounts/models/automations/backup_account.py:34 +#: accounts/models/automations/backup_account.py:96 +msgid "Account backup plan" +msgstr "アカウントバックアップ計画" + +#: accounts/models/automations/backup_account.py:77 +#: assets/models/automations/base.py:102 audits/models.py:41 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:107 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:108 +#: terminal/models/session/session.py:43 +#: tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:19 +msgid "Date start" +msgstr "開始日" + +#: accounts/models/automations/backup_account.py:80 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:186 +msgid "Time" +msgstr "時間" + +#: accounts/models/automations/backup_account.py:84 +msgid "Account backup snapshot" +msgstr "アカウントのバックアップスナップショット" + +#: accounts/models/automations/backup_account.py:88 +#: accounts/serializers/automations/base.py:42 +#: assets/models/automations/base.py:109 +#: assets/serializers/automations/base.py:40 +msgid "Trigger mode" +msgstr "トリガーモード" + +#: accounts/models/automations/backup_account.py:91 audits/models.py:130 +#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "理由" + +#: accounts/models/automations/backup_account.py:93 +#: accounts/serializers/automations/change_secret.py:106 +#: accounts/serializers/automations/change_secret.py:129 +#: ops/serializers/job.py:54 terminal/serializers/session.py:49 +msgid "Is success" +msgstr "成功は" + +#: accounts/models/automations/backup_account.py:101 +msgid "Account backup execution" +msgstr "アカウントバックアップの実行" + +#: accounts/models/automations/base.py:15 +#, fuzzy +msgid "Account automation task" +msgstr "自動管理" + +#: accounts/models/automations/base.py:25 +#, fuzzy +msgid "Automation execution" +msgstr "インスタンスタスクの同期実行" + +#: accounts/models/automations/base.py:26 +#, fuzzy +msgid "Automation executions" +msgstr "インスタンスタスクの同期実行" + +#: accounts/models/automations/base.py:28 +msgid "Can view change secret execution" +msgstr "改密実行の表示" + +#: accounts/models/automations/base.py:29 +msgid "Can add change secret execution" +msgstr "改密実行の作成" + +#: accounts/models/automations/base.py:31 +msgid "Can view gather accounts execution" +msgstr "収集アカウント実行の表示" + +#: accounts/models/automations/base.py:32 +msgid "Can add gather accounts execution" +msgstr "収集アカウントの作成実行" + +#: accounts/models/automations/base.py:34 +#, fuzzy +#| msgid "Can view gather accounts execution" +msgid "Can view push account execution" +msgstr "収集アカウント実行の表示" + +#: accounts/models/automations/base.py:35 +#, fuzzy +#| msgid "Can add gather accounts execution" +msgid "Can add push account execution" +msgstr "収集アカウントの作成実行" + +#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/serializers/account/account.py:114 +#: accounts/serializers/account/base.py:16 +#: accounts/serializers/automations/change_secret.py:46 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:50 +msgid "Secret type" +msgstr "鍵の種類" + +#: accounts/models/automations/change_secret.py:21 +#: accounts/serializers/automations/change_secret.py:40 +msgid "Secret strategy" +msgstr "鍵ポリシー" + +#: accounts/models/automations/change_secret.py:23 +#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 +#: authentication/models/temp_token.py:10 +#: authentication/templates/authentication/_access_key_modal.html:31 +#: settings/serializers/auth/radius.py:19 +msgid "Secret" +msgstr "ひみつ" + +#: accounts/models/automations/change_secret.py:24 +msgid "Password rules" +msgstr "パスワードルール" + +#: accounts/models/automations/change_secret.py:27 +#, fuzzy +msgid "SSH key change strategy" +msgstr "SSHキー戦略" + +#: accounts/models/automations/change_secret.py:54 +#, fuzzy +msgid "Change secret automation" +msgstr "セキュリティ設定を変更できます" + +#: accounts/models/automations/change_secret.py:71 +#, fuzzy +msgid "Old secret" +msgstr "OTP 秘密" + +#: accounts/models/automations/change_secret.py:73 +#, fuzzy +msgid "Date started" +msgstr "開始日" + +#: accounts/models/automations/change_secret.py:74 +#: assets/models/automations/base.py:103 ops/models/base.py:56 +#: ops/models/celery.py:64 ops/models/job.py:108 +#: terminal/models/applet/host.py:109 +msgid "Date finished" +msgstr "終了日" + +#: accounts/models/automations/change_secret.py:76 common/const/choices.py:20 +#, fuzzy +msgid "Error" +msgstr "企業微信エラー" + +#: accounts/models/automations/change_secret.py:80 +#, fuzzy +msgid "Change secret record" +msgstr "パスワードの変更" + +#: accounts/models/automations/gather_account.py:15 +#: accounts/tasks/gather_accounts.py:28 +#, fuzzy +msgid "Gather asset accounts" +msgstr "アカウントを集める" + +#: accounts/models/automations/push_account.py:13 +#, fuzzy +#| msgid "Trigger mode" +msgid "Triggers" +msgstr "トリガーモード" + +#: accounts/models/automations/push_account.py:14 accounts/models/base.py:34 +#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: assets/models/_user.py:34 audits/models.py:115 authentication/forms.py:25 +#: authentication/forms.py:27 authentication/models/temp_token.py:9 +#: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 +#: users/forms/profile.py:32 users/forms/profile.py:112 +#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 +msgid "Username" +msgstr "ユーザー名" + +#: accounts/models/automations/push_account.py:15 acls/models/base.py:77 +#: acls/serializers/base.py:81 assets/models/cmd_filter.py:81 +#: audits/models.py:51 audits/serializers.py:75 +#: authentication/serializers/connect_token_secret.py:108 +#: authentication/templates/authentication/_access_key_modal.html:34 +msgid "Action" +msgstr "アクション" + +#: accounts/models/automations/push_account.py:41 +#, fuzzy +msgid "Push asset account" +msgstr "サービスアカウントです" + +#: accounts/models/automations/verify_account.py:15 +#, fuzzy +msgid "Verify asset account" +msgstr "パスワード/キーの確認" + +#: accounts/models/base.py:33 acls/models/base.py:71 +#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: applications/models.py:9 assets/models/_user.py:33 +#: assets/models/asset/common.py:94 assets/models/asset/common.py:106 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/models/platform.py:20 assets/models/platform.py:74 +#: assets/serializers/asset/common.py:143 assets/serializers/platform.py:128 +#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 +#: ops/models/adhoc.py:22 ops/models/celery.py:15 ops/models/celery.py:57 +#: ops/models/job.py:25 ops/models/playbook.py:14 orgs/models.py:69 +#: perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 +#: terminal/models/applet/applet.py:23 terminal/models/component/endpoint.py:12 +#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 +#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: users/models/group.py:13 users/models/user.py:675 +#: xpack/plugins/cloud/models.py:28 +msgid "Name" +msgstr "名前" + +#: accounts/models/base.py:39 +msgid "Privileged" +msgstr "" + +#: accounts/models/base.py:40 assets/models/asset/common.py:113 +#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 +#: assets/models/label.py:22 +#: authentication/serializers/connect_token_secret.py:106 +#: terminal/models/applet/applet.py:28 users/serializers/user.py:158 +msgid "Is active" +msgstr "アクティブです。" + +#: accounts/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "アカウントバックアップルートタスクの結果の通知" + +#: accounts/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "" +"{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご" +"覧ください" + +#: accounts/notifications.py:20 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} -アカウントのバックアップ通過タスクが完了しました: 暗号化パスワードが設定" +"されていません-個人情報にアクセスしてください-> ファイル暗号化パスワードを設" +"定してください暗号化パスワード" + +#: accounts/notifications.py:31 +msgid "Notification of implementation result of encryption change plan" +msgstr "暗号化変更プランの実装結果の通知" + +#: accounts/notifications.py:41 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" + +#: accounts/notifications.py:42 +msgid "" +"{} - The encryption change task has been completed: the encryption password " +"has not been set - please go to personal information -> file encryption " +"password to set the encryption password" +msgstr "" +"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" +"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" + +#: accounts/serializers/account/account.py:19 +#: assets/serializers/asset/common.py:52 +msgid "Push now" +msgstr "" + +#: accounts/serializers/account/account.py:21 +#: accounts/serializers/account/base.py:64 +#, fuzzy +msgid "Has secret" +msgstr "ひみつ" + +#: accounts/serializers/account/account.py:28 +#: assets/serializers/asset/common.py:79 +msgid "Account template not found" +msgstr "" + +#: accounts/serializers/account/account.py:73 +#, fuzzy +#| msgid "Asset Info" +msgid "Asset not found" +msgstr "資産情報" + +#: accounts/serializers/account/backup.py:27 +#: accounts/serializers/automations/base.py:35 +#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 +#: settings/serializers/auth/ldap.py:66 +msgid "Periodic perform" +msgstr "定期的なパフォーマンス" + +#: accounts/serializers/account/backup.py:28 +#: accounts/serializers/automations/gather_accounts.py:23 +#, fuzzy +msgid "Executed amount" +msgstr "実行時間" + +#: accounts/serializers/account/backup.py:30 +#: accounts/serializers/automations/change_secret.py:57 +msgid "Currently only mail sending is supported" +msgstr "現在、メール送信のみがサポートされています" + +#: accounts/serializers/account/base.py:24 +msgid "Key password" +msgstr "キーパスワード" + +#: accounts/serializers/account/base.py:80 +msgid "Specific" +msgstr "" + +#: accounts/serializers/automations/base.py:21 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:35 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "資産" + +#: accounts/serializers/automations/base.py:22 +#: assets/models/asset/common.py:112 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:67 +msgid "Nodes" +msgstr "ノード" + +#: accounts/serializers/automations/base.py:40 +#: assets/models/automations/base.py:105 +#: assets/serializers/automations/base.py:39 +#, fuzzy +msgid "Automation snapshot" +msgstr "製造オーダスナップショット" + +#: accounts/serializers/automations/base.py:41 acls/models/command_acl.py:24 +#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: assets/models/_user.py:46 assets/models/automations/base.py:20 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:76 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:86 +#: audits/serializers.py:47 +#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:33 +#: perms/serializers/user_permission.py:26 terminal/models/applet/applet.py:27 +#: terminal/models/component/storage.py:57 +#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:28 +#: terminal/serializers/session.py:26 terminal/serializers/storage.py:181 +#: tickets/models/comment.py:26 tickets/models/flow.py:56 +#: tickets/models/ticket/apply_application.py:16 +#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:19 +msgid "Type" +msgstr "タイプ" + +#: accounts/serializers/automations/change_secret.py:43 +msgid "SSH Key strategy" +msgstr "SSHキー戦略" + +#: accounts/serializers/automations/change_secret.py:76 +msgid "* Please enter the correct password length" +msgstr "* 正しいパスワードの長さを入力してください" + +#: accounts/serializers/automations/change_secret.py:80 +msgid "* Password length range 6-30 bits" +msgstr "* パスワードの長さの範囲6-30ビット" + +#: accounts/serializers/automations/change_secret.py:110 +#: assets/models/automations/base.py:114 +#, fuzzy +msgid "Automation task execution" +msgstr "インスタンスタスクの同期実行" + +#: accounts/serializers/automations/change_secret.py:150 audits/const.py:45 +#: audits/models.py:40 common/const/choices.py:18 ops/const.py:51 +#: ops/serializers/celery.py:39 terminal/const.py:59 +#: terminal/models/session/sharing.py:103 tickets/views/approve.py:114 +msgid "Success" +msgstr "成功" + +#: accounts/serializers/automations/change_secret.py:151 +#: assets/const/automation.py:8 audits/const.py:46 common/const/choices.py:19 +#: ops/const.py:53 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失敗しました" + +#: accounts/tasks/automation.py:11 +#, fuzzy +msgid "Account execute automation" +msgstr "バッチ実行コマンド" + +#: accounts/tasks/backup_account.py:13 +#, fuzzy +msgid "Execute account backup plan" +msgstr "アカウントバックアップ計画" + +#: accounts/tasks/gather_accounts.py:31 +#, fuzzy +msgid "Gather assets accounts" +msgstr "資産ユーザーの収集" + +#: accounts/tasks/push_account.py:30 accounts/tasks/push_account.py:36 +#, fuzzy +msgid "Push accounts to assets" +msgstr "システムユーザーを資産にプッシュする:" + +#: accounts/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "" + +#: accounts/tasks/verify_account.py:36 +#, fuzzy +msgid "Verify accounts connectivity" +msgstr "テストアカウント接続:" + +#: accounts/utils.py:42 +msgid "Password can not contains `{{` " +msgstr "パスワードには '{{' を含まない" + +#: accounts/utils.py:45 +msgid "Password can not contains `'` " +msgstr "パスワードには `'` を含まない" + +#: accounts/utils.py:47 +msgid "Password can not contains `\"` " +msgstr "パスワードには `\"` を含まない" + +#: accounts/utils.py:53 +msgid "private key invalid or passphrase error" +msgstr "秘密鍵が無効またはpassphraseエラー" + #: acls/apps.py:7 msgid "Acls" msgstr "Acls" @@ -35,67 +713,39 @@ msgstr "受け入れられる" msgid "Review" msgstr "レビュー担当者" -#: acls/models/base.py:71 acls/models/command_acl.py:21 -#: acls/serializers/base.py:34 applications/models.py:9 -#: assets/models/_user.py:33 assets/models/asset/common.py:92 -#: assets/models/asset/common.py:101 assets/models/base.py:64 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:20 assets/models/platform.py:71 -#: assets/serializers/asset/common.py:87 assets/serializers/platform.py:121 -#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 -#: ops/models/adhoc.py:23 ops/models/celery.py:15 ops/models/job.py:24 -#: ops/models/playbook.py:14 orgs/models.py:67 -#: perms/models/asset_permission.py:55 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/applet/applet.py:22 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:86 -#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:675 -#: xpack/plugins/cloud/models.py:28 -msgid "Name" -msgstr "名前" - #: acls/models/base.py:73 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:89 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 msgid "Priority" msgstr "優先順位" #: acls/models/base.py:74 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:90 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" -#: acls/models/base.py:77 acls/serializers/base.py:63 -#: assets/models/cmd_filter.py:81 audits/models.py:51 audits/serializers.py:73 -#: authentication/serializers/connect_token_secret.py:108 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "アクション" - -#: acls/models/base.py:78 acls/serializers/base.py:59 +#: acls/models/base.py:78 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:81 msgid "Reviewers" msgstr "レビュー担当者" #: acls/models/base.py:79 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:47 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:75 terminal/models/session/sharing.py:27 +#: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "アクティブ" -#: acls/models/base.py:91 acls/models/login_acl.py:13 +#: acls/models/base.py:94 acls/models/login_acl.py:13 #: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:30 -#: audits/models.py:49 audits/models.py:93 +#: audits/models.py:49 audits/models.py:99 #: authentication/models/connection_token.py:28 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 -#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:57 -#: perms/serializers/permission.py:23 rbac/builtin.py:120 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 +#: perms/serializers/permission.py:23 rbac/builtin.py:118 #: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 #: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 @@ -105,35 +755,8 @@ msgstr "アクティブ" msgid "User" msgstr "ユーザー" -#: acls/models/base.py:93 acls/serializers/base.py:56 -#: assets/models/account.py:50 assets/models/asset/common.py:94 -#: assets/models/asset/common.py:221 assets/models/cmd_filter.py:36 -#: assets/models/gathered_user.py:12 assets/serializers/account/account.py:76 -#: assets/serializers/automations/change_secret.py:100 -#: assets/serializers/automations/change_secret.py:122 -#: assets/serializers/domain.py:19 assets/serializers/gathered_user.py:11 -#: assets/serializers/label.py:27 audits/models.py:34 -#: authentication/models/connection_token.py:32 -#: perms/models/asset_permission.py:63 perms/serializers/permission.py:27 -#: terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 -#: terminal/models/session/session.py:31 terminal/notifications.py:93 -#: xpack/plugins/cloud/models.py:220 -msgid "Asset" -msgstr "資産" - -#: acls/models/base.py:95 acls/serializers/base.py:57 -#: assets/models/account.py:60 -#: assets/serializers/automations/change_secret.py:101 -#: assets/serializers/automations/change_secret.py:123 audits/models.py:35 -#: ops/models/base.py:18 terminal/backends/command/models.py:22 -#: terminal/models/session/session.py:33 -#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 -msgid "Account" -msgstr "アカウント" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: terminal/backends/command/serializers.py:15 +#: ops/serializers/job.py:53 terminal/backends/command/serializers.py:15 #: terminal/models/session/session.py:41 terminal/serializers/session.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -144,23 +767,6 @@ msgstr "コマンド" msgid "Regex" msgstr "正規情報" -#: acls/models/command_acl.py:24 acls/serializers/command_acl.py:19 -#: applications/models.py:14 assets/models/_user.py:46 -#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:73 assets/serializers/asset/common.py:63 -#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:86 -#: audits/serializers.py:45 -#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:32 -#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:26 -#: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:33 -#: terminal/serializers/session.py:25 tickets/models/comment.py:26 -#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 -#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:54 -#: tickets/serializers/ticket/ticket.py:19 -msgid "Type" -msgstr "タイプ" - #: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 #: settings/serializers/basic.py:10 xpack/plugins/license/models.py:29 msgid "Content" @@ -174,7 +780,8 @@ msgstr "1行1コマンド" msgid "Ignore case" msgstr "家を無視する" -#: acls/models/command_acl.py:33 acls/serializers/command_acl.py:29 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 +#: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:78 msgid "Command group" msgstr "コマンドグループ" @@ -183,10 +790,6 @@ msgstr "コマンドグループ" msgid "The generated regular expression is incorrect: {}" msgstr "生成された正規表現が正しくありません: {}" -#: acls/models/command_acl.py:96 -msgid "Commands" -msgstr "コマンド#コマンド#" - #: acls/models/command_acl.py:100 msgid "Command acl" msgstr "コマンドフィルタリング" @@ -195,7 +798,7 @@ msgstr "コマンドフィルタリング" msgid "Command confirm" msgstr "コマンドの確認" -#: acls/models/login_acl.py:16 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:29 msgid "Rule" msgstr "ルール" @@ -219,19 +822,6 @@ msgstr "ログイン資産の確認" msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "コンマ区切り文字列の形式。* はすべて一致することを示します。" -#: acls/serializers/base.py:18 acls/serializers/base.py:49 -#: assets/models/_user.py:34 assets/models/base.py:65 -#: assets/models/gathered_user.py:13 audits/models.py:109 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models/temp_token.py:9 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "ユーザー名" - #: acls/serializers/base.py:25 msgid "" "Format for comma-delimited string, with * indicating a match all. Such as: " @@ -246,14 +836,50 @@ msgstr "" msgid "IP/Host" msgstr "IP/ホスト" -#: acls/serializers/base.py:90 tickets/serializers/ticket/ticket.py:78 +#: acls/serializers/base.py:60 +#, fuzzy +#| msgid "System user name" +msgid "User (username)" +msgstr "システムユーザー名" + +#: acls/serializers/base.py:64 +#, fuzzy +#| msgid "Asset hostname" +msgid "Asset (name)" +msgstr "資産ホスト名" + +#: acls/serializers/base.py:68 +#, fuzzy +#| msgid "Address" +msgid "Asset (address)" +msgstr "アドレス" + +#: acls/serializers/base.py:72 +#, fuzzy +#| msgid "Account name" +msgid "Account (username)" +msgstr "アカウント名" + +#: acls/serializers/base.py:78 acls/serializers/login_acl.py:27 +#, fuzzy +#| msgid "Reviewers" +msgid "Reviewers amount" +msgstr "レビュー担当者" + +#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "組織 '{}'は存在しません" -#: acls/serializers/base.py:96 +#: acls/serializers/base.py:115 msgid "None of the reviewers belong to Organization `{}`" msgstr "いずれのレビューアも組織 '{}' に属していません" +#: acls/serializers/command_acl.py:31 +#, fuzzy +#| msgid "Command amount" +msgid "Command group amount" +msgstr "コマンド量" + #: acls/serializers/rules/rules.py:20 #: xpack/plugins/cloud/serializers/task.py:22 msgid "IP address invalid: `{}`" @@ -269,11 +895,11 @@ msgstr "" "192.168.10.1、192.168.1.0/24、10.1.1.1-10.1.1.20、2001:db8:2de::e13、2001:" "db8:1a:1110::/64" -#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:102 +#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:107 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 msgid "IP" msgstr "IP" @@ -286,9 +912,9 @@ msgid "Applications" msgstr "アプリケーション" #: applications/models.py:11 assets/models/label.py:21 -#: assets/models/platform.py:72 assets/serializers/asset/common.py:62 +#: assets/models/platform.py:75 assets/serializers/asset/common.py:121 #: assets/serializers/cagegory.py:8 assets/serializers/platform.py:87 -#: assets/serializers/platform.py:122 perms/serializers/user_permission.py:23 +#: assets/serializers/platform.py:129 perms/serializers/user_permission.py:25 #: settings/models.py:35 tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "カテゴリ" @@ -307,7 +933,7 @@ msgid "Can match application" msgstr "アプリケーションを一致させることができます" #: applications/serializers/attrs/application_type/clickhouse.py:11 -#: assets/models/asset/common.py:93 assets/models/platform.py:21 +#: assets/models/asset/common.py:95 assets/models/platform.py:21 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" @@ -321,11 +947,7 @@ msgstr "" "デフォルトポートは9000で、HTTPインタフェースとネイティブインタフェースは異な" "るポートを使用する" -#: assets/api/automations/base.py:77 -msgid "The parameter 'action' must be [{}]" -msgstr "パラメータ 'action' は [{}] でなければなりません。" - -#: assets/api/domain.py:56 +#: assets/api/domain.py:57 msgid "Number required" msgstr "必要な数" @@ -345,110 +967,59 @@ msgstr "削除に失敗し、ノードにアセットが含まれています。 msgid "App assets" msgstr "アプリ資産" -#: assets/automations/base/manager.py:123 +#: assets/automations/base/manager.py:76 #, fuzzy msgid "{} disabled" msgstr "無効" -#: assets/const/account.py:6 audits/const.py:6 audits/const.py:64 +#: assets/automations/ping_gateway/manager.py:33 +#: authentication/models/connection_token.py:113 +#, fuzzy +msgid "No account" +msgstr "アカウント" + +#: assets/automations/ping_gateway/manager.py:55 +#, fuzzy, python-brace-format +msgid "Unable to connect to port {port} on {address}" +msgstr "{ip} でポート {port} に接続できません" + +#: assets/automations/ping_gateway/manager.py:58 +#: authentication/middleware.py:76 xpack/plugins/cloud/providers/fc.py:48 +msgid "Authentication failed" +msgstr "認証に失敗しました" + +#: assets/automations/ping_gateway/manager.py:60 +#: assets/automations/ping_gateway/manager.py:86 +msgid "Connect failed" +msgstr "接続に失敗しました" + +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:35 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" msgstr "不明" -#: assets/const/account.py:7 +#: assets/const/automation.py:7 msgid "Ok" msgstr "OK" -#: assets/const/account.py:8 -#: assets/serializers/automations/change_secret.py:118 -#: assets/serializers/automations/change_secret.py:146 audits/const.py:75 -#: common/const/choices.py:19 ops/const.py:51 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失敗しました" - -#: assets/const/account.py:12 assets/models/_user.py:35 -#: audits/signal_handlers.py:49 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:288 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:97 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "パスワード" - -#: assets/const/account.py:13 -#, fuzzy -msgid "SSH key" -msgstr "SSHキー" - -#: assets/const/account.py:14 authentication/models/access_key.py:33 -msgid "Access key" -msgstr "アクセスキー" - -#: assets/const/account.py:15 assets/models/_user.py:38 -#: authentication/models/sso_token.py:14 -msgid "Token" -msgstr "トークン" - -#: assets/const/automation.py:13 +#: assets/const/automation.py:12 msgid "Ping" msgstr "" +#: assets/const/automation.py:13 +#, fuzzy +#| msgid "Test gateway" +msgid "Ping gateway" +msgstr "テストゲートウェイ" + #: assets/const/automation.py:14 #, fuzzy msgid "Gather facts" msgstr "アカウントを集める" -#: assets/const/automation.py:15 -#, fuzzy -msgid "Create account" -msgstr "アカウントを集める" - -#: assets/const/automation.py:16 -#, fuzzy -msgid "Change secret" -msgstr "秘密を改める" - -#: assets/const/automation.py:17 -#, fuzzy -msgid "Verify account" -msgstr "パスワード/キーの確認" - -#: assets/const/automation.py:18 -#, fuzzy -msgid "Gather accounts" -msgstr "アカウントを集める" - -#: assets/const/automation.py:38 assets/serializers/account/base.py:29 -msgid "Specific" -msgstr "" - -#: assets/const/automation.py:39 ops/const.py:20 -msgid "All assets use the same random password" -msgstr "すべての資産は同じランダムパスワードを使用します" - -#: assets/const/automation.py:40 ops/const.py:21 -msgid "All assets use different random password" -msgstr "すべての資産は異なるランダムパスワードを使用します" - -#: assets/const/automation.py:44 ops/const.py:13 -msgid "Append SSH KEY" -msgstr "追加" - -#: assets/const/automation.py:45 ops/const.py:14 -msgid "Empty and append SSH KEY" -msgstr "すべてクリアして追加" - -#: assets/const/automation.py:46 ops/const.py:15 -msgid "Replace (The key generated by JumpServer) " -msgstr "置換(JumpServerによって生成された鍵)" - #: assets/const/category.py:11 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/applet/applet.py:129 -#: terminal/models/component/endpoint.py:13 +#: settings/serializers/auth/sms.py:67 terminal/models/component/endpoint.py:13 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" @@ -457,8 +1028,8 @@ msgstr "ホスト" msgid "Device" msgstr "" -#: assets/const/category.py:13 assets/models/asset/database.py:8 -#: assets/models/asset/database.py:34 +#: assets/const/category.py:13 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:32 msgid "Database" msgstr "データベース" @@ -467,12 +1038,12 @@ msgstr "データベース" msgid "Cloud service" msgstr "クラウドセンター" -#: assets/const/category.py:15 audits/const.py:62 -#: terminal/models/applet/applet.py:20 +#: assets/const/category.py:15 audits/const.py:33 +#: terminal/models/applet/applet.py:21 msgid "Web" msgstr "" -#: assets/const/device.py:7 terminal/models/applet/applet.py:19 +#: assets/const/device.py:7 terminal/models/applet/applet.py:20 #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -490,7 +1061,7 @@ msgstr "" msgid "Firewall" msgstr "" -#: assets/const/types.py:181 +#: assets/const/types.py:200 #, fuzzy #| msgid "MFA type" msgid "All types" @@ -527,33 +1098,33 @@ msgstr "SSHパブリックキー" #: assets/models/_user.py:40 assets/models/cmd_filter.py:40 #: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: assets/models/platform.py:76 common/db/models.py:78 ops/models/adhoc.py:29 -#: ops/models/job.py:40 ops/models/playbook.py:17 rbac/models/role.py:37 -#: settings/models.py:38 terminal/models/applet/applet.py:31 -#: terminal/models/applet/applet.py:131 terminal/models/applet/host.py:110 -#: terminal/models/component/endpoint.py:20 -#: terminal/models/component/endpoint.py:96 +#: assets/models/platform.py:79 common/db/models.py:37 ops/models/adhoc.py:28 +#: ops/models/job.py:41 ops/models/playbook.py:17 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:32 +#: terminal/models/applet/applet.py:137 terminal/models/applet/host.py:110 +#: terminal/models/component/endpoint.py:24 +#: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:45 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:714 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:119 msgid "Comment" msgstr "コメント" -#: assets/models/_user.py:41 assets/models/automations/base.py:91 +#: assets/models/_user.py:41 assets/models/automations/base.py:101 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:76 ops/models/base.py:54 ops/models/job.py:105 +#: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:106 #: users/models/user.py:932 msgid "Date created" msgstr "作成された日付" #: assets/models/_user.py:42 assets/models/cmd_filter.py:42 -#: common/db/models.py:77 +#: common/db/models.py:36 msgid "Date updated" msgstr "更新日" #: assets/models/_user.py:43 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:21 -#: common/db/models.py:74 users/models/user.py:722 +#: common/db/models.py:33 users/models/user.py:722 #: users/serializers/group.py:33 msgid "Created by" msgstr "によって作成された" @@ -564,8 +1135,8 @@ msgstr "ユーザーと同じユーザー名" #: assets/models/_user.py:48 authentication/models/connection_token.py:37 #: authentication/serializers/connect_token_secret.py:103 -#: terminal/models/applet/applet.py:29 terminal/serializers/session.py:24 -#: terminal/serializers/session.py:40 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:30 terminal/serializers/session.py:24 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "プロトコル" @@ -577,7 +1148,7 @@ msgstr "オートプッシュ" msgid "Sudo" msgstr "すど" -#: assets/models/_user.py:51 ops/const.py:44 ops/models/adhoc.py:19 +#: assets/models/_user.py:51 ops/const.py:44 msgid "Shell" msgstr "シェル" @@ -613,143 +1184,78 @@ msgstr "システムユーザー" msgid "Can match system user" msgstr "システムユーザーに一致できます" -#: assets/models/account.py:44 common/db/fields.py:232 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "すべて" - -#: assets/models/account.py:45 -msgid "Manual input" -msgstr "手動入力" - -#: assets/models/account.py:46 -msgid "Dynamic user" -msgstr "動的コード" - -#: assets/models/account.py:54 assets/serializers/account/account.py:79 -#: authentication/serializers/connect_token_secret.py:48 -msgid "Su from" -msgstr "から切り替え" - -#: assets/models/account.py:56 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:24 -msgid "Version" -msgstr "バージョン" - -#: assets/models/account.py:66 -msgid "Can view asset account secret" -msgstr "資産アカウントの秘密を表示できます" - -#: assets/models/account.py:67 -msgid "Can change asset account secret" -msgstr "資産口座の秘密を変更できます" - -#: assets/models/account.py:68 -msgid "Can view asset history account" -msgstr "資産履歴アカウントを表示できます" - -#: assets/models/account.py:69 -msgid "Can view asset history account secret" -msgstr "資産履歴アカウントパスワードを表示できます" - -#: assets/models/account.py:106 assets/serializers/account/account.py:15 -#, fuzzy -msgid "Account template" -msgstr "アカウント名" - -#: assets/models/account.py:111 -#, fuzzy -msgid "Can view asset account template secret" -msgstr "資産アカウントの秘密を表示できます" - -#: assets/models/account.py:112 -#, fuzzy -msgid "Can change asset account template secret" -msgstr "資産口座の秘密を変更できます" - -#: assets/models/asset/common.py:103 assets/models/platform.py:109 -#: assets/serializers/asset/common.py:65 +#: assets/models/asset/common.py:108 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 -#: perms/serializers/user_permission.py:21 +#: perms/serializers/user_permission.py:23 #: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Platform" msgstr "プラットフォーム" -#: assets/models/asset/common.py:105 assets/models/domain.py:21 -#: assets/serializers/asset/common.py:64 +#: assets/models/asset/common.py:110 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 +#: perms/serializers/user_permission.py:27 msgid "Domain" msgstr "ドメイン" -#: assets/models/asset/common.py:107 assets/models/automations/base.py:18 -#: assets/models/cmd_filter.py:32 assets/serializers/asset/common.py:66 -#: assets/serializers/automations/base.py:21 -#: perms/models/asset_permission.py:66 -msgid "Nodes" -msgstr "ノード" - -#: assets/models/asset/common.py:108 assets/models/automations/base.py:21 -#: assets/models/base.py:71 assets/models/cmd_filter.py:39 -#: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:106 -#: terminal/models/applet/applet.py:27 users/serializers/user.py:158 -msgid "Is active" -msgstr "アクティブです。" - -#: assets/models/asset/common.py:109 assets/serializers/asset/common.py:67 +#: assets/models/asset/common.py:114 msgid "Labels" msgstr "ラベル" -#: assets/models/asset/common.py:224 +#: assets/models/asset/common.py:284 msgid "Can refresh asset hardware info" msgstr "資産ハードウェア情報を更新できます" -#: assets/models/asset/common.py:225 +#: assets/models/asset/common.py:285 msgid "Can test asset connectivity" msgstr "資産接続をテストできます" -#: assets/models/asset/common.py:226 +#: assets/models/asset/common.py:286 #, fuzzy msgid "Can push account to asset" msgstr "システムユーザーを資産にプッシュできます" -#: assets/models/asset/common.py:227 +#: assets/models/asset/common.py:287 +#, fuzzy +msgid "Can verify account" +msgstr "パスワード/キーの確認" + +#: assets/models/asset/common.py:288 msgid "Can match asset" msgstr "アセットを一致させることができます" -#: assets/models/asset/common.py:228 +#: assets/models/asset/common.py:289 msgid "Add asset to node" msgstr "ノードにアセットを追加する" -#: assets/models/asset/common.py:229 +#: assets/models/asset/common.py:290 msgid "Move asset to node" msgstr "アセットをノードに移動する" -#: assets/models/asset/database.py:9 settings/serializers/email.py:37 +#: assets/models/asset/database.py:10 settings/serializers/email.py:37 msgid "Use SSL" msgstr "SSLの使用" -#: assets/models/asset/database.py:10 +#: assets/models/asset/database.py:11 #, fuzzy msgid "CA cert" msgstr "SP 証明書" -#: assets/models/asset/database.py:11 +#: assets/models/asset/database.py:12 #, fuzzy msgid "Client cert" msgstr "クライアント秘密" -#: assets/models/asset/database.py:12 +#: assets/models/asset/database.py:13 #, fuzzy msgid "Client key" msgstr "クライアント" -#: assets/models/asset/database.py:13 +#: assets/models/asset/database.py:14 msgid "Allow invalid cert" msgstr "証明書チェックを無視" -#: assets/models/asset/web.py:9 audits/const.py:68 -#: terminal/serializers/applet_host.py:25 +#: assets/models/asset/web.py:9 audits/const.py:39 +#: terminal/serializers/applet_host.py:27 msgid "Disabled" msgstr "無効" @@ -782,147 +1288,32 @@ msgid "Submit selector" msgstr "" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:69 +#: assets/serializers/asset/common.py:241 perms/models/asset_permission.py:70 #: perms/serializers/permission.py:32 rbac/tree.py:36 msgid "Accounts" msgstr "アカウント" -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:34 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "資産" - -#: assets/models/automations/base.py:81 assets/models/automations/base.py:88 +#: assets/models/automations/base.py:28 assets/models/automations/base.py:98 #, fuzzy msgid "Automation task" msgstr "自動管理" -#: assets/models/automations/base.py:90 audits/models.py:129 -#: audits/serializers.py:46 ops/models/base.py:49 ops/models/job.py:98 -#: terminal/models/applet/applet.py:130 terminal/models/applet/host.py:107 -#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:283 tickets/serializers/ticket/ticket.py:20 -#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:224 +#: assets/models/automations/base.py:91 +#, fuzzy +msgid "Asset automation task" +msgstr "自動管理" + +#: assets/models/automations/base.py:100 audits/models.py:135 +#: audits/serializers.py:48 ops/models/base.py:49 ops/models/job.py:99 +#: terminal/models/applet/applet.py:136 terminal/models/applet/host.py:107 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:17 +#: terminal/serializers/applet_host.py:90 tickets/models/ticket/general.py:283 +#: tickets/serializers/super_ticket.py:13 +#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/models.py:224 msgid "Status" msgstr "ステータス" -#: assets/models/automations/base.py:92 assets/models/backup.py:73 -#: audits/models.py:41 ops/models/base.py:55 ops/models/celery.py:60 -#: ops/models/job.py:106 perms/models/asset_permission.py:71 -#: terminal/models/applet/host.py:108 terminal/models/session/session.py:43 -#: tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Date start" -msgstr "開始日" - -#: assets/models/automations/base.py:93 -#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 -#: ops/models/celery.py:61 ops/models/job.py:107 -#: terminal/models/applet/host.py:109 -msgid "Date finished" -msgstr "終了日" - -#: assets/models/automations/base.py:95 -#: assets/serializers/automations/base.py:39 -#, fuzzy -msgid "Automation snapshot" -msgstr "製造オーダスナップショット" - -#: assets/models/automations/base.py:99 assets/models/backup.py:84 -#: assets/serializers/automations/base.py:41 -msgid "Trigger mode" -msgstr "トリガーモード" - -#: assets/models/automations/base.py:103 -#: assets/serializers/automations/change_secret.py:103 -#, fuzzy -msgid "Automation task execution" -msgstr "インスタンスタスクの同期実行" - -#: assets/models/automations/base.py:105 -msgid "Can view change secret execution" -msgstr "改密実行の表示" - -#: assets/models/automations/base.py:106 -msgid "Can add change secret execution" -msgstr "改密実行の作成" - -#: assets/models/automations/base.py:107 -msgid "Can view gather accounts execution" -msgstr "収集アカウント実行の表示" - -#: assets/models/automations/base.py:108 -msgid "Can add gather accounts execution" -msgstr "収集アカウントの作成実行" - -#: assets/models/automations/change_secret.py:15 assets/models/base.py:67 -#: assets/serializers/account/account.py:112 assets/serializers/base.py:13 -#: authentication/serializers/connect_token_secret.py:39 -#: authentication/serializers/connect_token_secret.py:49 -msgid "Secret type" -msgstr "鍵の種類" - -#: assets/models/automations/change_secret.py:19 -#: assets/serializers/automations/change_secret.py:25 -msgid "Secret strategy" -msgstr "鍵ポリシー" - -#: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:57 assets/models/base.py:69 -#: assets/serializers/base.py:16 authentication/models/temp_token.py:10 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "ひみつ" - -#: assets/models/automations/change_secret.py:22 -msgid "Password rules" -msgstr "パスワードルール" - -#: assets/models/automations/change_secret.py:25 -#, fuzzy -msgid "SSH key change strategy" -msgstr "SSHキー戦略" - -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:25 -#: assets/serializers/account/backup.py:30 -#: assets/serializers/automations/change_secret.py:40 -msgid "Recipient" -msgstr "受信者" - -#: assets/models/automations/change_secret.py:34 -#, fuzzy -msgid "Change secret automation" -msgstr "セキュリティ設定を変更できます" - -#: assets/models/automations/change_secret.py:56 -#, fuzzy -msgid "Old secret" -msgstr "OTP 秘密" - -#: assets/models/automations/change_secret.py:58 -#, fuzzy -msgid "Date started" -msgstr "開始日" - -#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 -#, fuzzy -msgid "Error" -msgstr "企業微信エラー" - -#: assets/models/automations/change_secret.py:64 -#, fuzzy -msgid "Change secret record" -msgstr "パスワードの変更" - -#: assets/models/automations/gather_accounts.py:15 -#: assets/tasks/gather_accounts.py:28 -#, fuzzy -msgid "Gather asset accounts" -msgstr "アカウントを集める" - #: assets/models/automations/gather_facts.py:15 #, fuzzy msgid "Gather asset facts" @@ -933,59 +1324,15 @@ msgstr "資産ユーザーの収集" msgid "Ping asset" msgstr "ログイン資産" -#: assets/models/automations/push_account.py:16 -#, fuzzy -msgid "Push asset account" -msgstr "サービスアカウントです" - -#: assets/models/automations/verify_account.py:15 -#, fuzzy -msgid "Verify asset account" -msgstr "パスワード/キーの確認" - -#: assets/models/backup.py:34 assets/models/backup.py:92 -msgid "Account backup plan" -msgstr "アカウントバックアップ計画" - -#: assets/models/backup.py:76 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:186 -msgid "Time" -msgstr "時間" - -#: assets/models/backup.py:80 -msgid "Account backup snapshot" -msgstr "アカウントのバックアップスナップショット" - -#: assets/models/backup.py:87 audits/models.py:124 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "理由" - -#: assets/models/backup.py:89 -#: assets/serializers/automations/change_secret.py:99 -#: assets/serializers/automations/change_secret.py:124 -#: terminal/serializers/session.py:44 -msgid "Is success" -msgstr "成功は" - -#: assets/models/backup.py:96 -msgid "Account backup execution" -msgstr "アカウントバックアップの実行" - -#: assets/models/base.py:26 +#: assets/models/base.py:19 msgid "Connectivity" msgstr "接続性" -#: assets/models/base.py:28 authentication/models/temp_token.py:12 +#: assets/models/base.py:21 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "確認済みの日付" -#: assets/models/base.py:70 -msgid "Privileged" -msgstr "" - -#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:60 +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:25 users/models/group.py:25 #: users/models/user.py:681 msgid "User group" @@ -1015,45 +1362,14 @@ msgstr "フィルター" msgid "Command filter rule" msgstr "コマンドフィルタルール" -#: assets/models/gateway.py:40 assets/serializers/domain.py:16 +#: assets/models/favorite_asset.py:17 +msgid "Favorite Asset" +msgstr "お気に入り" + +#: assets/models/gateway.py:35 assets/serializers/domain.py:16 msgid "Gateway" msgstr "ゲートウェイ" -#: assets/models/gateway.py:62 authentication/models/connection_token.py:104 -#, fuzzy -msgid "No account" -msgstr "アカウント" - -#: assets/models/gateway.py:84 -#, fuzzy, python-brace-format -msgid "Unable to connect to port {port} on {address}" -msgstr "{ip} でポート {port} に接続できません" - -#: assets/models/gateway.py:87 authentication/middleware.py:76 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "認証に失敗しました" - -#: assets/models/gateway.py:89 assets/models/gateway.py:116 -msgid "Connect failed" -msgstr "接続に失敗しました" - -#: assets/models/gathered_user.py:14 -msgid "Present" -msgstr "プレゼント" - -#: assets/models/gathered_user.py:15 -msgid "Date last login" -msgstr "最終ログイン日" - -#: assets/models/gathered_user.py:16 -msgid "IP last login" -msgstr "IP最終ログイン" - -#: assets/models/gathered_user.py:27 -msgid "GatherUser" -msgstr "収集ユーザー" - #: assets/models/group.py:30 msgid "Asset group" msgstr "資産グループ" @@ -1075,14 +1391,14 @@ msgstr "システム" #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:25 #: authentication/serializers/connect_token_secret.py:114 -#: common/drf/serializers/common.py:82 settings/models.py:34 +#: common/serializers/common.py:82 settings/models.py:34 msgid "Value" msgstr "値" -#: assets/models/label.py:40 assets/serializers/cagegory.py:6 -#: assets/serializers/cagegory.py:13 +#: assets/models/label.py:40 assets/serializers/asset/common.py:123 +#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:113 -#: common/drf/serializers/common.py:81 settings/serializers/sms.py:7 +#: common/serializers/common.py:81 settings/serializers/sms.py:7 msgid "Label" msgstr "ラベル" @@ -1094,7 +1410,7 @@ msgstr "新しいノード" msgid "empty" msgstr "空" -#: assets/models/node.py:551 perms/models/perm_node.py:27 +#: assets/models/node.py:551 perms/models/perm_node.py:28 msgid "Key" msgstr "キー" @@ -1102,12 +1418,12 @@ msgstr "キー" msgid "Full value" msgstr "フルバリュー" -#: assets/models/node.py:557 perms/models/perm_node.py:29 +#: assets/models/node.py:557 perms/models/perm_node.py:30 msgid "Parent key" msgstr "親キー" #: assets/models/node.py:566 perms/serializers/permission.py:28 -#: xpack/plugins/cloud/models.py:96 +#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "ノード" @@ -1125,8 +1441,8 @@ msgstr "MFAが必要" msgid "Setting" msgstr "設定" -#: assets/models/platform.py:41 audits/const.py:69 settings/models.py:37 -#: terminal/serializers/applet_host.py:26 +#: assets/models/platform.py:41 audits/const.py:40 settings/models.py:37 +#: terminal/serializers/applet_host.py:28 msgid "Enabled" msgstr "有効化" @@ -1143,181 +1459,116 @@ msgstr "MFA有効化" msgid "Ping method" msgstr "" -#: assets/models/platform.py:45 assets/models/platform.py:55 +#: assets/models/platform.py:45 assets/models/platform.py:58 #, fuzzy msgid "Gather facts enabled" msgstr "資産ユーザーの収集" -#: assets/models/platform.py:46 assets/models/platform.py:57 +#: assets/models/platform.py:46 assets/models/platform.py:60 #, fuzzy msgid "Gather facts method" msgstr "資産ユーザーの収集" #: assets/models/platform.py:47 #, fuzzy -msgid "Push account enabled" -msgstr "MFAが有効化されていません" - -#: assets/models/platform.py:48 -msgid "Push account method" -msgstr "" +msgid "Change secret enabled" +msgstr "パスワードの変更" #: assets/models/platform.py:49 #, fuzzy -msgid "Change password enabled" +msgid "Change secret method" msgstr "パスワードの変更" #: assets/models/platform.py:51 #, fuzzy -msgid "Change password method" -msgstr "パスワードの変更" +msgid "Push account enabled" +msgstr "MFAが有効化されていません" -#: assets/models/platform.py:52 +#: assets/models/platform.py:53 +#, fuzzy +msgid "Push account method" +msgstr "サービスアカウントです" + +#: assets/models/platform.py:55 #, fuzzy msgid "Verify account enabled" msgstr "サービスアカウントキー" -#: assets/models/platform.py:54 +#: assets/models/platform.py:57 #, fuzzy msgid "Verify account method" msgstr "パスワード/キーの確認" -#: assets/models/platform.py:74 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:77 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "メタ" -#: assets/models/platform.py:75 +#: assets/models/platform.py:78 msgid "Internal" msgstr "ビルトイン" -#: assets/models/platform.py:79 assets/serializers/platform.py:84 +#: assets/models/platform.py:82 assets/serializers/platform.py:84 msgid "Charset" msgstr "シャーセット" -#: assets/models/platform.py:81 +#: assets/models/platform.py:84 #, fuzzy msgid "Domain enabled" msgstr "ドメイン名" -#: assets/models/platform.py:82 +#: assets/models/platform.py:85 #, fuzzy msgid "Protocols enabled" msgstr "プロトコル" -#: assets/models/platform.py:84 +#: assets/models/platform.py:87 #, fuzzy msgid "Su enabled" msgstr "MFA有効化" -#: assets/models/platform.py:85 +#: assets/models/platform.py:88 #, fuzzy msgid "Su method" msgstr "接続タイムアウト" -#: assets/models/platform.py:87 assets/serializers/platform.py:91 +#: assets/models/platform.py:90 assets/serializers/platform.py:91 #, fuzzy msgid "Automation" msgstr "自動管理" -#: assets/models/utils.py:19 +#: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s は偶数ではありません" -#: assets/notifications.py:8 -msgid "Notification of account backup route task results" -msgstr "アカウントバックアップルートタスクの結果の通知" - -#: assets/notifications.py:18 -msgid "" -"{} - The account backup passage task has been completed. See the attachment " -"for details" -msgstr "" -"{} -アカウントバックアップの通過タスクが完了しました。詳細は添付ファイルをご" -"覧ください" - -#: assets/notifications.py:20 -msgid "" -"{} - The account backup passage task has been completed: the encryption " -"password has not been set - please go to personal information -> file " -"encryption password to set the encryption password" -msgstr "" -"{} -アカウントのバックアップ通過タスクが完了しました: 暗号化パスワードが設定" -"されていません-個人情報にアクセスしてください-> ファイル暗号化パスワードを設" -"定してください暗号化パスワード" - -#: assets/notifications.py:31 -msgid "Notification of implementation result of encryption change plan" -msgstr "暗号化変更プランの実装結果の通知" - -#: assets/notifications.py:41 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} -暗号化変更タスクが完了しました。詳細は添付ファイルをご覧ください" - -#: assets/notifications.py:42 -msgid "" -"{} - The encryption change task has been completed: the encryption password " -"has not been set - please go to personal information -> file encryption " -"password to set the encryption password" -msgstr "" -"{} -暗号化変更タスクが完了しました: 暗号化パスワードが設定されていません-個人" -"情報にアクセスしてください-> ファイル暗号化パスワードを設定してください" - -#: assets/serializers/account/account.py:18 -msgid "Push now" -msgstr "" - -#: assets/serializers/account/account.py:20 -#: assets/serializers/account/base.py:13 -#, fuzzy -msgid "Has secret" -msgstr "ひみつ" - -#: assets/serializers/account/account.py:27 -msgid "Account template not found" -msgstr "" - -#: assets/serializers/account/account.py:72 -#, fuzzy -#| msgid "Asset Info" -msgid "Asset not found" -msgstr "資産情報" - -#: assets/serializers/account/backup.py:29 -#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 -#: settings/serializers/auth/ldap.py:66 -msgid "Periodic perform" -msgstr "定期的なパフォーマンス" - -#: assets/serializers/account/backup.py:31 -#: assets/serializers/automations/change_secret.py:41 -msgid "Currently only mail sending is supported" -msgstr "現在、メール送信のみがサポートされています" - -#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:89 -#: authentication/serializers/connect_token_secret.py:27 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:89 +#: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:65 -#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:107 +#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:107 #: xpack/plugins/cloud/serializers/task.py:38 msgid "Protocols" msgstr "プロトコル" -#: assets/serializers/asset/common.py:88 +#: assets/serializers/asset/common.py:126 +#, fuzzy +#| msgid "Enabled" +msgid "Enabled info" +msgstr "有効化" + +#: assets/serializers/asset/common.py:144 msgid "Address" msgstr "アドレス" -#: assets/serializers/asset/common.py:89 +#: assets/serializers/asset/common.py:145 msgid "Node path" msgstr "ノードパスです" -#: assets/serializers/asset/common.py:157 +#: assets/serializers/asset/common.py:205 #, fuzzy msgid "Platform not exist" msgstr "アプリが存在しません" -#: assets/serializers/asset/common.py:173 +#: assets/serializers/asset/common.py:221 #, fuzzy msgid "Protocol is required: {}" msgstr "プロトコル重複: {}" @@ -1383,35 +1634,6 @@ msgstr "ホスト名生" msgid "Asset number" msgstr "資産番号" -#: assets/serializers/automations/change_secret.py:28 -msgid "SSH Key strategy" -msgstr "SSHキー戦略" - -#: assets/serializers/automations/change_secret.py:70 -msgid "* Please enter the correct password length" -msgstr "* 正しいパスワードの長さを入力してください" - -#: assets/serializers/automations/change_secret.py:73 -msgid "* Password length range 6-30 bits" -msgstr "* パスワードの長さの範囲6-30ビット" - -#: assets/serializers/automations/change_secret.py:117 -#: assets/serializers/automations/change_secret.py:145 audits/const.py:74 -#: audits/models.py:40 common/const/choices.py:18 ops/const.py:50 -#: ops/serializers/celery.py:39 terminal/models/session/sharing.py:103 -#: tickets/views/approve.py:114 -msgid "Success" -msgstr "成功" - -#: assets/serializers/automations/gather_accounts.py:23 -#, fuzzy -msgid "Executed amount" -msgstr "実行時間" - -#: assets/serializers/base.py:21 -msgid "Key password" -msgstr "キーパスワード" - #: assets/serializers/cagegory.py:9 msgid "Constraints" msgstr "" @@ -1421,9 +1643,9 @@ msgstr "" msgid "Types" msgstr "タイプ" -#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "ホスト名" +#: assets/serializers/gateway.py:24 common/validators.py:32 +msgid "This field must be unique." +msgstr "このフィールドは一意である必要があります。" #: assets/serializers/label.py:12 msgid "Assets amount" @@ -1455,55 +1677,29 @@ msgstr "SFTPルート" msgid "Primary" msgstr "" -#: assets/serializers/utils.py:13 -msgid "Password can not contains `{{` " -msgstr "パスワードには '{{' を含まない" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `'` " -msgstr "パスワードには `'` を含まない" - -#: assets/serializers/utils.py:18 -msgid "Password can not contains `\"` " -msgstr "パスワードには `\"` を含まない" - -#: assets/serializers/utils.py:24 -msgid "private key invalid or passphrase error" -msgstr "秘密鍵が無効またはpassphraseエラー" - #: assets/tasks/automation.py:11 #, fuzzy -msgid "Execute automation" +msgid "Asset execute automation" msgstr "バッチ実行コマンド" -#: assets/tasks/backup.py:13 -#, fuzzy -msgid "Execute account backup plan" -msgstr "アカウントバックアップ計画" - -#: assets/tasks/gather_accounts.py:31 -#, fuzzy -msgid "Gather assets accounts" -msgstr "資産ユーザーの収集" - -#: assets/tasks/gather_facts.py:26 +#: assets/tasks/gather_facts.py:23 msgid "Update some assets hardware info. " msgstr "一部の資産ハードウェア情報を更新します。" -#: assets/tasks/gather_facts.py:44 +#: assets/tasks/gather_facts.py:53 #, fuzzy msgid "Manually update the hardware information of assets" msgstr "ノード資産のハードウェア情報を更新します。" -#: assets/tasks/gather_facts.py:49 +#: assets/tasks/gather_facts.py:57 msgid "Update assets hardware info: " msgstr "資産のハードウェア情報を更新する:" -#: assets/tasks/gather_facts.py:53 +#: assets/tasks/gather_facts.py:61 msgid "Manually update the hardware information of assets under a node" msgstr "" -#: assets/tasks/gather_facts.py:59 +#: assets/tasks/gather_facts.py:65 msgid "Update node asset hardware information: " msgstr "ノード資産のハードウェア情報を更新します。" @@ -1522,30 +1718,25 @@ msgstr "" msgid "Periodic check the amount of assets under the node" msgstr "" -#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 +#: assets/tasks/ping.py:37 assets/tasks/ping.py:54 #, fuzzy msgid "Test assets connectivity " msgstr "資産の接続性をテストします。" -#: assets/tasks/ping.py:33 +#: assets/tasks/ping.py:50 #, fuzzy -msgid "Manually test the connectivity of a asset" +msgid "Manually test the connectivity of a asset" msgstr "資産接続をテストできます" -#: assets/tasks/ping.py:43 +#: assets/tasks/ping.py:58 msgid "Manually test the connectivity of assets under a node" msgstr "" -#: assets/tasks/ping.py:49 +#: assets/tasks/ping.py:62 #, fuzzy msgid "Test if the assets under the node are connectable " msgstr "ノードの下のアセットが接続可能かどうかをテストします。" -#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:34 -#, fuzzy -msgid "Push accounts to assets" -msgstr "システムユーザーを資産にプッシュする:" - #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "資産が無効化されました。スキップ: {}" @@ -1562,15 +1753,6 @@ msgstr "セキュリティのために、ユーザー {} をプッシュしな msgid "No assets matched, stop task" msgstr "一致する資産がない、タスクを停止" -#: assets/tasks/verify_account.py:30 -msgid "Verify asset account availability" -msgstr "" - -#: assets/tasks/verify_account.py:37 -#, fuzzy -msgid "Verify accounts connectivity" -msgstr "テストアカウント接続:" - #: audits/apps.py:9 msgid "Audits" msgstr "監査" @@ -1579,78 +1761,93 @@ msgstr "監査" msgid "The text content is too long. Use Elasticsearch to store operation logs" msgstr "文章の内容が長すぎる。Elasticsearchで操作履歴を保存する" -#: audits/backends/db.py:24 audits/backends/db.py:26 +#: audits/backends/db.py:25 audits/backends/db.py:27 msgid "Tips" msgstr "謎々" -#: audits/const.py:45 +#: audits/const.py:12 msgid "Mkdir" msgstr "Mkdir" -#: audits/const.py:46 +#: audits/const.py:13 msgid "Rmdir" msgstr "Rmdir" -#: audits/const.py:47 audits/const.py:57 +#: audits/const.py:14 audits/const.py:24 #: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:232 +#: rbac/tree.py:231 msgid "Delete" msgstr "削除" -#: audits/const.py:48 perms/const.py:13 +#: audits/const.py:15 perms/const.py:13 msgid "Upload" msgstr "アップロード" -#: audits/const.py:49 +#: audits/const.py:16 msgid "Rename" msgstr "名前の変更" -#: audits/const.py:50 +#: audits/const.py:17 msgid "Symlink" msgstr "Symlink" -#: audits/const.py:51 perms/const.py:14 +#: audits/const.py:18 perms/const.py:14 msgid "Download" msgstr "ダウンロード" -#: audits/const.py:55 rbac/tree.py:230 +#: audits/const.py:22 rbac/tree.py:229 msgid "View" msgstr "表示" -#: audits/const.py:56 rbac/tree.py:231 templates/_csv_import_export.html:18 +#: audits/const.py:23 rbac/tree.py:230 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/const.py:58 +#: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:229 +#: rbac/tree.py:228 msgid "Create" msgstr "作成" -#: audits/const.py:63 settings/serializers/terminal.py:6 +#: audits/const.py:27 perms/const.py:12 +msgid "Connect" +msgstr "接続" + +#: audits/const.py:28 authentication/templates/authentication/login.html:254 +#: authentication/templates/authentication/login.html:327 +#: templates/_header_bar.html:89 +msgid "Login" +msgstr "ログイン" + +#: audits/const.py:29 ops/const.py:9 +#, fuzzy +msgid "Change password" +msgstr "パスワードの変更" + +#: audits/const.py:34 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:24 terminal/models/component/terminal.py:156 msgid "Terminal" msgstr "ターミナル" -#: audits/const.py:70 +#: audits/const.py:41 msgid "-" msgstr "-" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "Yes" msgstr "是" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "No" msgstr "否" -#: audits/models.py:32 audits/models.py:55 audits/models.py:96 +#: audits/models.py:32 audits/models.py:59 audits/models.py:102 #: terminal/models/session/session.py:37 terminal/models/session/sharing.py:95 msgid "Remote addr" msgstr "リモートaddr" -#: audits/models.py:37 audits/serializers.py:30 +#: audits/models.py:37 audits/serializers.py:32 msgid "Operate" msgstr "操作" @@ -1662,104 +1859,116 @@ msgstr "ファイル名" msgid "File transfer log" msgstr "ファイル転送ログ" -#: audits/models.py:53 audits/serializers.py:84 +#: audits/models.py:53 audits/serializers.py:86 msgid "Resource Type" msgstr "リソースタイプ" -#: audits/models.py:54 +#: audits/models.py:54 audits/models.py:57 msgid "Resource" msgstr "リソース" -#: audits/models.py:56 audits/models.py:98 -#: terminal/backends/command/serializers.py:40 +#: audits/models.py:60 audits/models.py:104 +#: terminal/backends/command/serializers.py:41 msgid "Datetime" msgstr "時間" -#: audits/models.py:88 +#: audits/models.py:63 +#, fuzzy +#| msgid "Is active" +msgid "Is Activity" +msgstr "アクティブです。" + +#: audits/models.py:93 msgid "Operate log" msgstr "ログの操作" -#: audits/models.py:94 +#: audits/models.py:100 msgid "Change by" msgstr "による変更" -#: audits/models.py:104 +#: audits/models.py:110 msgid "Password change log" msgstr "パスワード変更ログ" -#: audits/models.py:111 +#: audits/models.py:117 msgid "Login type" msgstr "ログインタイプ" -#: audits/models.py:113 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:119 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "ログインIP" -#: audits/models.py:115 +#: audits/models.py:121 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "ログイン都市" -#: audits/models.py:118 audits/serializers.py:60 +#: audits/models.py:124 audits/serializers.py:62 msgid "User agent" msgstr "ユーザーエージェント" -#: audits/models.py:121 audits/serializers.py:44 +#: audits/models.py:127 audits/serializers.py:46 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:698 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:131 +#: audits/models.py:137 msgid "Date login" msgstr "日付ログイン" -#: audits/models.py:133 audits/serializers.py:62 +#: audits/models.py:139 audits/serializers.py:64 msgid "Authentication backend" msgstr "認証バックエンド" -#: audits/models.py:174 +#: audits/models.py:180 msgid "User login log" msgstr "ユーザーログインログ" -#: audits/serializers.py:61 +#: audits/serializers.py:63 msgid "Reason display" msgstr "理由表示" -#: audits/signal_handlers.py:48 +#: audits/serializers.py:112 +#, fuzzy +#| msgid "User {} {} it." +msgid "User {} {} this resource." +msgstr "ユーザー {} はそれを {} しました" + +#: audits/signal_handlers.py:50 msgid "SSH Key" msgstr "SSHキー" -#: audits/signal_handlers.py:50 settings/serializers/auth/sso.py:10 +#: audits/signal_handlers.py:52 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:53 msgid "Auth Token" msgstr "認証トークン" -#: audits/signal_handlers.py:52 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: audits/signal_handlers.py:54 authentication/notifications.py:73 +#: authentication/views/login.py:73 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:736 msgid "WeCom" msgstr "企業微信" -#: audits/signal_handlers.py:53 authentication/views/feishu.py:145 +#: audits/signal_handlers.py:55 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 users/models/user.py:738 msgid "FeiShu" msgstr "本を飛ばす" -#: audits/signal_handlers.py:54 authentication/views/dingtalk.py:180 +#: audits/signal_handlers.py:56 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:737 msgid "DingTalk" msgstr "DingTalk" -#: audits/signal_handlers.py:55 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:57 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "仮パスワード" @@ -1767,6 +1976,26 @@ msgstr "仮パスワード" msgid "This action require verify your MFA" msgstr "この操作には、MFAを検証する必要があります" +#: authentication/api/connection_token.py:264 +#, fuzzy +#| msgid "Asset Info" +msgid "Account not found" +msgstr "資産情報" + +#: authentication/api/connection_token.py:267 +#, fuzzy +#| msgid "Permission name" +msgid "Permission Expired" +msgstr "認可ルール名" + +#: authentication/api/connection_token.py:279 +msgid "ACL action is reject" +msgstr "" + +#: authentication/api/connection_token.py:283 +msgid "ACL action is review" +msgstr "" + #: authentication/api/mfa.py:59 msgid "Current user not support mfa type: {}" msgstr "現在のユーザーはmfaタイプをサポートしていません: {}" @@ -1800,7 +2029,7 @@ msgstr "パスワードを忘れた" #: authentication/apps.py:7 settings/serializers/auth/base.py:10 #: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 #: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 -#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/oauth2.py:18 settings/serializers/auth/oidc.py:12 #: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 #: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" @@ -2001,21 +2230,21 @@ msgstr "電話が設定されていない" msgid "SSO auth closed" msgstr "SSO authは閉鎖されました" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 msgid "WeCom is already bound" msgstr "企業の微信はすでにバインドされています" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237 -#: authentication/views/wecom.py:291 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 +#: authentication/views/wecom.py:290 msgid "WeCom is not bound" msgstr "企業の微信をバインドしていません" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 -#: authentication/views/dingtalk.py:297 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 +#: authentication/views/dingtalk.py:296 msgid "DingTalk is not bound" msgstr "DingTalkはバインドされていません" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 msgid "FeiShu is not bound" msgstr "本を飛ばすは拘束されていません" @@ -2173,34 +2402,45 @@ msgid "Asset display" msgstr "アセット名" #: authentication/models/connection_token.py:41 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:73 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:719 msgid "Date expired" msgstr "期限切れの日付" #: authentication/models/connection_token.py:45 +#: perms/models/asset_permission.py:77 +msgid "From ticket" +msgstr "チケットから" + +#: authentication/models/connection_token.py:51 msgid "Connection token" msgstr "接続トークン" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:53 msgid "Can view connection token secret" msgstr "接続トークンの秘密を表示できます" -#: authentication/models/connection_token.py:94 +#: authentication/models/connection_token.py:100 +#, fuzzy +#| msgid "Connection token" +msgid "Connection token inactive" +msgstr "接続トークン" + +#: authentication/models/connection_token.py:103 msgid "Connection token expired at: {}" msgstr "接続トークンの有効期限: {}" -#: authentication/models/connection_token.py:97 +#: authentication/models/connection_token.py:106 msgid "No user or invalid user" msgstr "" -#: authentication/models/connection_token.py:101 +#: authentication/models/connection_token.py:110 #, fuzzy msgid "No asset or inactive asset" msgstr "アセットがアクティブ化されていません" -#: authentication/models/connection_token.py:248 +#: authentication/models/connection_token.py:257 msgid "Super connection token" msgstr "スーパー接続トークン" @@ -2248,9 +2488,9 @@ msgstr "システムコンポーネント" msgid "Expired now" msgstr "期限切れ" -#: authentication/serializers/connect_token_secret.py:146 +#: authentication/serializers/connect_token_secret.py:147 #: authentication/templates/authentication/_access_key_modal.html:30 -#: perms/models/perm_node.py:20 users/serializers/group.py:35 +#: perms/models/perm_node.py:21 users/serializers/group.py:35 msgid "ID" msgstr "ID" @@ -2258,6 +2498,12 @@ msgstr "ID" msgid "Expired time" msgstr "期限切れ時間" +#: authentication/serializers/connection_token.py:18 +#, fuzzy +#| msgid "Ticket flow" +msgid "Ticket info" +msgstr "チケットの流れ" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2274,7 +2520,7 @@ msgid "The {} cannot be empty" msgstr "{} 空にしてはならない" #: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:61 users/serializers/user.py:159 +#: perms/serializers/permission.py:62 users/serializers/user.py:159 msgid "Is valid" msgstr "有効です" @@ -2463,12 +2709,6 @@ msgstr "" msgid "Cancel" msgstr "キャンセル" -#: authentication/templates/authentication/login.html:254 -#: authentication/templates/authentication/login.html:327 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "ログイン" - #: authentication/templates/authentication/login.html:334 msgid "More login options" msgstr "その他のログインオプション" @@ -2510,73 +2750,73 @@ msgstr "コピー成功" msgid "LAN" msgstr "ローカルエリアネットワーク" -#: authentication/views/dingtalk.py:42 +#: authentication/views/dingtalk.py:41 msgid "DingTalk Error, Please contact your system administrator" msgstr "DingTalkエラー、システム管理者に連絡してください" -#: authentication/views/dingtalk.py:45 +#: authentication/views/dingtalk.py:44 msgid "DingTalk Error" msgstr "DingTalkエラー" -#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 -#: authentication/views/wecom.py:56 +#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:55 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "システム設定が正しくありません。管理者に連絡してください" -#: authentication/views/dingtalk.py:81 +#: authentication/views/dingtalk.py:80 msgid "DingTalk is already bound" msgstr "DingTalkはすでにバインドされています" -#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 msgid "Invalid user_id" msgstr "無効なuser_id" -#: authentication/views/dingtalk.py:165 +#: authentication/views/dingtalk.py:164 msgid "DingTalk query user failed" msgstr "DingTalkクエリユーザーが失敗しました" -#: authentication/views/dingtalk.py:174 +#: authentication/views/dingtalk.py:173 msgid "The DingTalk is already bound to another user" msgstr "DingTalkはすでに別のユーザーにバインドされています" -#: authentication/views/dingtalk.py:181 +#: authentication/views/dingtalk.py:180 msgid "Binding DingTalk successfully" msgstr "DingTalkのバインドに成功" -#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 +#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 msgid "Failed to get user from DingTalk" msgstr "DingTalkからユーザーを取得できませんでした" -#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 +#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 msgid "Please login with a password and then bind the DingTalk" msgstr "パスワードでログインし、DingTalkをバインドしてください" -#: authentication/views/feishu.py:40 +#: authentication/views/feishu.py:39 msgid "FeiShu Error" msgstr "FeiShuエラー" -#: authentication/views/feishu.py:88 +#: authentication/views/feishu.py:87 msgid "FeiShu is already bound" msgstr "FeiShuはすでにバインドされています" -#: authentication/views/feishu.py:130 +#: authentication/views/feishu.py:129 msgid "FeiShu query user failed" msgstr "FeiShuクエリユーザーが失敗しました" -#: authentication/views/feishu.py:139 +#: authentication/views/feishu.py:138 msgid "The FeiShu is already bound to another user" msgstr "FeiShuはすでに別のユーザーにバインドされています" -#: authentication/views/feishu.py:146 +#: authentication/views/feishu.py:145 msgid "Binding FeiShu successfully" msgstr "本を飛ばすのバインドに成功" -#: authentication/views/feishu.py:198 +#: authentication/views/feishu.py:197 msgid "Failed to get user from FeiShu" msgstr "本を飛ばすからユーザーを取得できませんでした" -#: authentication/views/feishu.py:205 +#: authentication/views/feishu.py:204 msgid "Please login with a password and then bind the FeiShu" msgstr "パスワードでログインしてから本を飛ばすをバインドしてください" @@ -2612,34 +2852,38 @@ msgstr "ログアウト成功" msgid "Logout success, return login page" msgstr "ログアウト成功、ログインページを返す" -#: authentication/views/wecom.py:41 +#: authentication/views/wecom.py:40 msgid "WeCom Error, Please contact your system administrator" msgstr "企業微信エラー、システム管理者に連絡してください" -#: authentication/views/wecom.py:44 +#: authentication/views/wecom.py:43 msgid "WeCom Error" msgstr "企業微信エラー" -#: authentication/views/wecom.py:163 +#: authentication/views/wecom.py:162 msgid "WeCom query user failed" msgstr "企業微信ユーザーの問合せに失敗しました" -#: authentication/views/wecom.py:172 +#: authentication/views/wecom.py:171 msgid "The WeCom is already bound to another user" msgstr "この企業の微信はすでに他のユーザーをバインドしている。" -#: authentication/views/wecom.py:179 +#: authentication/views/wecom.py:178 msgid "Binding WeCom successfully" msgstr "企業の微信のバインドに成功" -#: authentication/views/wecom.py:231 authentication/views/wecom.py:285 +#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 msgid "Failed to get user from WeCom" msgstr "企業の微信からユーザーを取得できませんでした" -#: authentication/views/wecom.py:238 authentication/views/wecom.py:292 +#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 msgid "Please login with a password and then bind the WeCom" msgstr "パスワードでログインしてからWeComをバインドしてください" +#: common/api/action.py:52 +msgid "Request file format may be wrong" +msgstr "リクエストファイルの形式が間違っている可能性があります" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -2662,11 +2906,12 @@ msgstr "タイミングトリガー" msgid "Ready" msgstr "の準備を" -#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:39 +#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: tickets/const.py:39 msgid "Pending" msgstr "未定" -#: common/const/choices.py:17 ops/const.py:49 +#: common/const/choices.py:17 ops/const.py:50 msgid "Running" msgstr "" @@ -2679,67 +2924,55 @@ msgstr "キャンセル" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:94 +#: common/db/fields.py:97 msgid "Marshal dict data to char field" msgstr "チャーフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:98 +#: common/db/fields.py:101 msgid "Marshal dict data to text field" msgstr "テキストフィールドへのマーシャルディクトデータ" -#: common/db/fields.py:110 +#: common/db/fields.py:113 msgid "Marshal list data to char field" msgstr "元帥リストデータをチャーフィールドに" -#: common/db/fields.py:114 +#: common/db/fields.py:117 msgid "Marshal list data to text field" msgstr "マーシャルリストデータをテキストフィールドに" -#: common/db/fields.py:118 +#: common/db/fields.py:121 msgid "Marshal data to char field" msgstr "チャーフィールドへのマーシャルデータ" -#: common/db/fields.py:122 +#: common/db/fields.py:125 msgid "Marshal data to text field" msgstr "テキストフィールドへのマーシャルデータ" -#: common/db/fields.py:164 +#: common/db/fields.py:167 msgid "Encrypt field using Secret Key" msgstr "Secret Keyを使用したフィールドの暗号化" -#: common/db/models.py:75 +#: common/db/mixins.py:32 +msgid "is discard" +msgstr "は破棄されます" + +#: common/db/mixins.py:33 +msgid "discard time" +msgstr "時間を捨てる" + +#: common/db/models.py:34 msgid "Updated by" msgstr "によって更新" -#: common/drf/exc_handlers.py:25 +#: common/db/validators.py:9 +msgid "Invalid port range, should be like and within {}-{}" +msgstr "" + +#: common/drf/exc_handlers.py:26 msgid "Object" msgstr "オブジェクト" -#: common/drf/fields.py:77 tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "このフィールドは必須です。" - -#: common/drf/fields.py:78 -#, fuzzy, python-brace-format -msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "%s オブジェクトは存在しません。" - -#: common/drf/fields.py:79 -#, python-brace-format -msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "" - -#: common/drf/fields.py:141 -msgid "Invalid data type, should be list" -msgstr "" - -#: common/drf/fields.py:156 -#, fuzzy -msgid "Invalid choice: {}" -msgstr "無効なIP" - -#: common/drf/metadata.py:130 +#: common/drf/metadata.py:127 msgid "Organization ID" msgstr "組織 ID" @@ -2751,15 +2984,6 @@ msgstr "ファイルの内容がオーバーフローしました (最大長 '{} msgid "Parse file error: {}" msgstr "解析ファイルエラー: {}" -#: common/drf/serializers/common.py:86 -msgid "Children" -msgstr "" - -#: common/drf/serializers/common.py:94 -#, fuzzy -msgid "File" -msgstr "ファイル名" - #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -2789,31 +3013,6 @@ msgstr "このアクションでは、MFAの確認が必要です。" msgid "Unexpect error occur" msgstr "予期しないエラーが発生します" -#: common/mixins/api/action.py:52 -msgid "Request file format may be wrong" -msgstr "リクエストファイルの形式が間違っている可能性があります" - -#: common/mixins/models.py:32 -msgid "is discard" -msgstr "は破棄されます" - -#: common/mixins/models.py:33 -msgid "discard time" -msgstr "時間を捨てる" - -#: common/mixins/views.py:58 -msgid "Export all" -msgstr "すべてエクスポート" - -#: common/mixins/views.py:60 -msgid "Export only selected items" -msgstr "選択項目のみエクスポート" - -#: common/mixins/views.py:65 -#, python-format -msgid "Export filtered: %s" -msgstr "検索のエクスポート: %s" - #: common/plugins/es.py:28 msgid "Invalid elasticsearch config" msgstr "無効なElasticsearch構成" @@ -2878,6 +3077,39 @@ msgstr "確認コードが正しくありません" msgid "Please wait {} seconds before sending" msgstr "{} 秒待ってから送信してください" +#: common/serializers/common.py:86 +msgid "Children" +msgstr "" + +#: common/serializers/common.py:94 +#, fuzzy +msgid "File" +msgstr "ファイル名" + +#: common/serializers/fields.py:100 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "このフィールドは必須です。" + +#: common/serializers/fields.py:101 +#, fuzzy, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "%s オブジェクトは存在しません。" + +#: common/serializers/fields.py:102 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: common/serializers/fields.py:172 +msgid "Invalid data type, should be list" +msgstr "" + +#: common/serializers/fields.py:187 +#, fuzzy +msgid "Invalid choice: {}" +msgstr "無効なIP" + #: common/tasks.py:13 #, fuzzy msgid "Send email" @@ -2900,10 +3132,6 @@ msgstr "署名が無効です。" msgid "Special char not allowed" msgstr "特別なcharは許可されていません" -#: common/validators.py:32 -msgid "This field must be unique." -msgstr "このフィールドは一意である必要があります。" - #: common/validators.py:40 msgid "Should not contains special characters" msgstr "特殊文字を含むべきではない" @@ -2912,6 +3140,19 @@ msgstr "特殊文字を含むべきではない" msgid "The mobile phone number format is incorrect" msgstr "携帯電話番号の形式が正しくありません" +#: common/views/mixins.py:57 +msgid "Export all" +msgstr "すべてエクスポート" + +#: common/views/mixins.py:59 +msgid "Export only selected items" +msgstr "選択項目のみエクスポート" + +#: common/views/mixins.py:64 +#, python-format +msgid "Export filtered: %s" +msgstr "検索のエクスポート: %s" + #: jumpserver/conf.py:415 msgid "Create account successfully" msgstr "アカウントを正常に作成" @@ -3002,7 +3243,7 @@ msgstr "" msgid "Waiting task start" msgstr "タスク開始待ち" -#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:56 msgid "App ops" msgstr "アプリ操作" @@ -3019,20 +3260,23 @@ msgstr "確認済み" msgid "Collect" msgstr "" -#: ops/const.py:9 -#, fuzzy -msgid "Change password" -msgstr "パスワードの変更" - #: ops/const.py:19 msgid "Custom password" msgstr "カスタムパスワード" +#: ops/const.py:20 +msgid "All assets use the same random password" +msgstr "すべての資産は同じランダムパスワードを使用します" + +#: ops/const.py:21 +msgid "All assets use different random password" +msgstr "すべての資産は異なるランダムパスワードを使用します" + #: ops/const.py:33 msgid "Adhoc" msgstr "コマンド#コマンド#" -#: ops/const.py:34 ops/models/job.py:31 +#: ops/const.py:34 ops/models/job.py:32 msgid "Playbook" msgstr "Playbook" @@ -3048,12 +3292,22 @@ msgstr "特権アカウント優先" msgid "Skip" msgstr "スキップ" -#: ops/const.py:45 ops/models/adhoc.py:20 +#: ops/const.py:45 #, fuzzy #| msgid "PowerShell" msgid "Powershell" msgstr "PowerShell" +#: ops/const.py:46 +msgid "Python" +msgstr "" + +#: ops/const.py:52 +#, fuzzy +#| msgid "Test timeout" +msgid "Timeout" +msgstr "テストタイムアウト" + #: ops/exception.py:6 msgid "no valid program entry found." msgstr "利用可能なプログラムポータルがありません" @@ -3083,26 +3337,26 @@ msgstr "{} から {} までの範囲" msgid "Require periodic or regularly perform setting" msgstr "定期的または定期的に設定を行う必要があります" -#: ops/models/adhoc.py:24 +#: ops/models/adhoc.py:23 msgid "Pattern" msgstr "パターン" -#: ops/models/adhoc.py:26 ops/models/job.py:28 +#: ops/models/adhoc.py:25 ops/models/job.py:29 msgid "Module" msgstr "モジュール" -#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: ops/models/adhoc.py:26 ops/models/celery.py:58 ops/models/job.py:27 #: terminal/models/component/task.py:16 msgid "Args" msgstr "アルグ" -#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: ops/models/adhoc.py:27 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:34 ops/models/job.py:105 ops/models/playbook.py:16 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "作成者" -#: ops/models/adhoc.py:46 +#: ops/models/adhoc.py:45 msgid "AdHoc" msgstr "タスクの各バージョン" @@ -3116,85 +3370,95 @@ msgstr "アカウントキー" msgid "Last execution" msgstr "コマンド実行" -#: ops/models/base.py:22 +#: ops/models/base.py:22 ops/serializers/job.py:16 #, fuzzy msgid "Date last run" msgstr "最終同期日" -#: ops/models/base.py:51 ops/models/job.py:102 +#: ops/models/base.py:51 ops/models/job.py:103 #: xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "結果" -#: ops/models/base.py:52 ops/models/job.py:103 +#: ops/models/base.py:52 ops/models/job.py:104 msgid "Summary" msgstr "概要" +#: ops/models/celery.py:16 +#, fuzzy +msgid "Date last publish" +msgstr "終了日" + #: ops/models/celery.py:47 msgid "Celery Task" msgstr "Celery タスク#タスク#" -#: ops/models/celery.py:56 terminal/models/component/task.py:17 +#: ops/models/celery.py:50 +msgid "Can view task monitor" +msgstr "タスクモニターを表示できます" + +#: ops/models/celery.py:59 terminal/models/component/task.py:17 msgid "Kwargs" msgstr "クワーグ" -#: ops/models/celery.py:57 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: ops/models/celery.py:60 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 +#: tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状態" -#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 #: tickets/const.py:25 msgid "Finished" msgstr "終了" -#: ops/models/celery.py:59 +#: ops/models/celery.py:62 #, fuzzy msgid "Date published" msgstr "終了日" -#: ops/models/celery.py:83 +#: ops/models/celery.py:86 msgid "Celery Task Execution" msgstr "Celery タスク実行" -#: ops/models/job.py:29 +#: ops/models/job.py:30 msgid "Chdir" msgstr "Chdir" -#: ops/models/job.py:30 +#: ops/models/job.py:31 msgid "Timeout (Seconds)" msgstr "タイムアウト(秒)" -#: ops/models/job.py:35 +#: ops/models/job.py:36 msgid "Runas" msgstr "Runas" -#: ops/models/job.py:37 +#: ops/models/job.py:38 msgid "Runas policy" msgstr "Runas ポリシー" -#: ops/models/job.py:38 +#: ops/models/job.py:39 msgid "Use Parameter Define" msgstr "パラメータ定義を使用する" -#: ops/models/job.py:39 +#: ops/models/job.py:40 msgid "Parameters define" msgstr "パラメータ定義" -#: ops/models/job.py:91 +#: ops/models/job.py:92 msgid "Job" msgstr "ジョブ#ジョブ#" -#: ops/models/job.py:101 +#: ops/models/job.py:102 msgid "Parameters" msgstr "パラメータ" -#: ops/models/job.py:300 +#: ops/models/job.py:311 msgid "Job Execution" msgstr "ジョブ実行" -#: ops/models/job.py:311 +#: ops/models/job.py:322 msgid "Job audit log" msgstr "ジョブ監査ログ" @@ -3231,14 +3495,20 @@ msgstr "{max_threshold} を超えるCPUロード: => {value}" msgid "Run after save" msgstr "システムユーザーの実行" -#: ops/serializers/job.py:43 +#: ops/serializers/job.py:52 #, fuzzy msgid "Job type" msgstr "Docタイプ" -#: ops/serializers/job.py:44 -msgid "Material" -msgstr "マテリアル" +#: ops/serializers/job.py:55 terminal/serializers/session.py:53 +msgid "Is finished" +msgstr "終了しました" + +#: ops/serializers/job.py:56 +#, fuzzy +#| msgid "Test timeout" +msgid "Time cost" +msgstr "テストタイムアウト" #: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 #: terminal/models/component/task.py:24 @@ -3281,10 +3551,6 @@ msgstr "定期的なパフォーマンス" msgid "Task log" msgstr "タスクログ" -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "タスク内容の更新: {}" - #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "JumpServerの現在のユーザーのユーザー名" @@ -3321,18 +3587,18 @@ msgstr "ジョブのID" msgid "Name of the job" msgstr "ジョブの名前" -#: orgs/api.py:67 +#: orgs/api.py:63 msgid "The current organization ({}) cannot be deleted" msgstr "現在の組織 ({}) は削除できません" -#: orgs/api.py:72 +#: orgs/api.py:68 msgid "" "LDAP synchronization is set to the current organization. Please switch to " "another organization before deleting" msgstr "" "LDAP 同期は現在の組織に設定されます。削除する前に別の組織に切り替えてください" -#: orgs/api.py:81 +#: orgs/api.py:78 msgid "The organization have resource ({}) cannot be deleted" msgstr "組織のリソース ({}) は削除できません" @@ -3340,10 +3606,10 @@ msgstr "組織のリソース ({}) は削除できません" msgid "App organizations" msgstr "アプリ組織" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:84 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 -#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "組織" @@ -3351,28 +3617,28 @@ msgstr "組織" msgid "Org name" msgstr "組織名" -#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +#: orgs/models.py:70 rbac/models/role.py:36 terminal/models/applet/applet.py:29 #, fuzzy msgid "Builtin" msgstr "内蔵" -#: orgs/models.py:74 +#: orgs/models.py:76 msgid "GLOBAL" msgstr "グローバル組織" -#: orgs/models.py:76 +#: orgs/models.py:78 msgid "DEFAULT" msgstr "デフォルト組織" -#: orgs/models.py:78 +#: orgs/models.py:80 msgid "SYSTEM" msgstr "システム組織" -#: orgs/models.py:84 +#: orgs/models.py:86 msgid "Can view root org" msgstr "グローバル組織を表示できます" -#: orgs/models.py:85 +#: orgs/models.py:87 msgid "Can view all joined org" msgstr "参加しているすべての組織を表示できます" @@ -3385,10 +3651,6 @@ msgstr "グローバル組織名" msgid "App permissions" msgstr "アプリの権限" -#: perms/const.py:12 -msgid "Connect" -msgstr "接続" - #: perms/const.py:15 #, fuzzy msgid "Copy" @@ -3407,46 +3669,42 @@ msgstr "転送" msgid "Clipboard" msgstr "クリップボードのコピー" -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:59 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:60 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "アクション" -#: perms/models/asset_permission.py:76 -msgid "From ticket" -msgstr "チケットから" - -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "資産権限" -#: perms/models/perm_node.py:67 +#: perms/models/perm_node.py:68 msgid "Ungrouped" msgstr "グループ化されていません" -#: perms/models/perm_node.py:69 +#: perms/models/perm_node.py:70 msgid "Favorite" msgstr "お気に入り" -#: perms/models/perm_node.py:120 +#: perms/models/perm_node.py:121 msgid "Permed asset" msgstr "許可された資産" -#: perms/models/perm_node.py:122 +#: perms/models/perm_node.py:123 msgid "Can view my assets" msgstr "私の資産を見ることができます" -#: perms/models/perm_node.py:123 +#: perms/models/perm_node.py:124 msgid "Can view user assets" msgstr "ユーザー資産を表示できます" -#: perms/models/perm_node.py:124 +#: perms/models/perm_node.py:125 msgid "Can view usergroup assets" msgstr "ユーザーグループの資産を表示できます" -#: perms/models/perm_node.py:135 +#: perms/models/perm_node.py:136 #, fuzzy msgid "Permed account" msgstr "アカウントを集める" @@ -3471,7 +3729,7 @@ msgstr "資産権限の有効期限が近づいています" msgid "asset permissions of organization {}" msgstr "組織 {} の資産権限" -#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 #: users/serializers/user.py:91 users/serializers/user.py:161 msgid "Is expired" msgstr "期限切れです" @@ -3512,27 +3770,27 @@ msgstr "{} 少なくとも1つのシステムロール" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:111 +#: rbac/builtin.py:109 msgid "SystemAdmin" msgstr "システム管理者" -#: rbac/builtin.py:114 +#: rbac/builtin.py:112 msgid "SystemAuditor" msgstr "システム監査人" -#: rbac/builtin.py:117 +#: rbac/builtin.py:115 msgid "SystemComponent" msgstr "システムコンポーネント" -#: rbac/builtin.py:123 +#: rbac/builtin.py:121 msgid "OrgAdmin" msgstr "組織管理者" -#: rbac/builtin.py:126 +#: rbac/builtin.py:124 msgid "OrgAuditor" msgstr "監査員を組織する" -#: rbac/builtin.py:129 +#: rbac/builtin.py:127 msgid "OrgUser" msgstr "組織ユーザー" @@ -3565,7 +3823,7 @@ msgid "Permissions" msgstr "権限" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:36 msgid "Scope" msgstr "スコープ" @@ -3613,7 +3871,7 @@ msgstr "パーマ" msgid "Users amount" msgstr "ユーザー数" -#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:24 msgid "Display name" msgstr "表示名" @@ -3665,24 +3923,24 @@ msgstr "バックアップアカウント" msgid "Gather account" msgstr "アカウントを集める" -#: rbac/tree.py:51 +#: rbac/tree.py:52 msgid "Asset change auth" msgstr "資産の改ざん" -#: rbac/tree.py:52 +#: rbac/tree.py:53 msgid "Terminal setting" msgstr "ターミナル設定" -#: rbac/tree.py:53 +#: rbac/tree.py:54 msgid "Task Center" msgstr "タスクセンター" -#: rbac/tree.py:54 +#: rbac/tree.py:55 msgid "My assets" msgstr "私の資産" -#: rbac/tree.py:56 terminal/models/applet/applet.py:38 -#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +#: rbac/tree.py:57 terminal/models/applet/applet.py:39 +#: terminal/models/applet/applet.py:133 terminal/models/applet/host.py:27 msgid "Applet" msgstr "リモートアプリケーション" @@ -3702,10 +3960,6 @@ msgstr "共通設定" msgid "View permission tree" msgstr "権限ツリーの表示" -#: rbac/tree.py:124 -msgid "Execute batch command" -msgstr "バッチ実行コマンド" - #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 #: settings/api/sms.py:148 settings/api/wecom.py:37 msgid "Test success" @@ -3847,7 +4101,7 @@ msgstr "サービス側アドレス" msgid "Proxy server url" msgstr "コールバックアドレス" -#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:54 #: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同期ログアウト" @@ -3909,7 +4163,7 @@ msgstr "ユーザー検索フィルター" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "選択は (cnまたはuidまたはsAMAccountName)=%(user)s)" -#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:56 #: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "ユーザー属性マッピング" @@ -3934,52 +4188,52 @@ msgstr "ページサイズを検索" msgid "Enable LDAP auth" msgstr "LDAP認証の有効化" -#: settings/serializers/auth/oauth2.py:19 +#: settings/serializers/auth/oauth2.py:18 msgid "OAuth2" msgstr "OAuth2" -#: settings/serializers/auth/oauth2.py:22 +#: settings/serializers/auth/oauth2.py:21 msgid "Enable OAuth2 Auth" msgstr "OAuth2認証の有効化" -#: settings/serializers/auth/oauth2.py:25 +#: settings/serializers/auth/oauth2.py:24 msgid "Logo" msgstr "アイコン" -#: settings/serializers/auth/oauth2.py:28 +#: settings/serializers/auth/oauth2.py:27 msgid "Service provider" msgstr "サービスプロバイダー" -#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +#: settings/serializers/auth/oauth2.py:30 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "クライアントID" -#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: settings/serializers/auth/oauth2.py:33 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "クライアント秘密" -#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oauth2.py:39 settings/serializers/auth/oidc.py:68 msgid "Provider auth endpoint" msgstr "認証エンドポイントアドレス" -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:42 settings/serializers/auth/oidc.py:71 msgid "Provider token endpoint" msgstr "プロバイダートークンエンドポイント" -#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +#: settings/serializers/auth/oauth2.py:45 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "クライアント認証方式" -#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oauth2.py:49 settings/serializers/auth/oidc.py:77 msgid "Provider userinfo endpoint" msgstr "プロバイダーuserinfoエンドポイント" -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oauth2.py:52 settings/serializers/auth/oidc.py:80 msgid "Provider end session endpoint" msgstr "プロバイダーのセッション終了エンドポイント" -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/oauth2.py:59 settings/serializers/auth/oidc.py:98 #: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "常にユーザーを更新" @@ -4117,7 +4371,7 @@ msgstr "SMSプロバイダ / プロトコル" #: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 #: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 -#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:69 msgid "Signature" msgstr "署名" @@ -4191,7 +4445,7 @@ msgstr "" msgid "SSO auth key TTL" msgstr "Token有効期間" -#: settings/serializers/auth/sso.py:17 +#: settings/serializers/auth/sso.py:17 settings/serializers/security.py:117 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "単位: 秒" @@ -4373,7 +4627,7 @@ msgstr "" msgid "Create user email content" msgstr "ユーザーのメールコンテンツを作成する" -#: settings/serializers/email.py:65 +#: settings/serializers/email.py:66 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4382,7 +4636,7 @@ msgstr "" "ヒント:ユーザーの作成時にパスワード設定メールの内容を送信し、{username}{name}" "{email}ラベルをサポートします。" -#: settings/serializers/email.py:69 +#: settings/serializers/email.py:70 msgid "Tips: Email signature (eg:jumpserver)" msgstr "ヒント: メール署名 (例: jumpserver)" @@ -4608,10 +4862,16 @@ msgstr "" "ます。" #: settings/serializers/security.py:116 +#, fuzzy +#| msgid "Verify code" +msgid "Verify code TTL" +msgstr "コードの確認" + +#: settings/serializers/security.py:121 msgid "Enable Login dynamic code" msgstr "ログイン動的コードの有効化" -#: settings/serializers/security.py:117 +#: settings/serializers/security.py:122 msgid "" "The password and additional code are sent to a third party authentication " "system for verification" @@ -4619,32 +4879,32 @@ msgstr "" "パスワードと追加コードは、検証のためにサードパーティの認証システムに送信され" "ます" -#: settings/serializers/security.py:122 +#: settings/serializers/security.py:127 msgid "MFA in login page" msgstr "ログインページのMFA" -#: settings/serializers/security.py:123 +#: settings/serializers/security.py:128 msgid "Eu security regulations(GDPR) require MFA to be on the login page" msgstr "" "Euセキュリティ規制 (GDPR) では、MFAがログインページにある必要があります" -#: settings/serializers/security.py:126 +#: settings/serializers/security.py:131 msgid "Enable Login captcha" msgstr "ログインcaptchaの有効化" -#: settings/serializers/security.py:127 +#: settings/serializers/security.py:132 msgid "Enable captcha to prevent robot authentication" msgstr "Captchaを有効にしてロボット認証を防止する" -#: settings/serializers/security.py:146 +#: settings/serializers/security.py:151 msgid "Security" msgstr "セキュリティ" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "ターミナルレジスタの有効化" -#: settings/serializers/security.py:151 +#: settings/serializers/security.py:156 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" @@ -4652,64 +4912,64 @@ msgstr "" "ターミナルレジスタを許可し、すべてのターミナルセットアップの後、セキュリティ" "のためにこれを無効にする必要があります" -#: settings/serializers/security.py:155 +#: settings/serializers/security.py:160 msgid "Enable watermark" msgstr "透かしの有効化" -#: settings/serializers/security.py:156 +#: settings/serializers/security.py:161 msgid "Enabled, the web session and replay contains watermark information" msgstr "Webセッションとリプレイには透かし情報が含まれています。" -#: settings/serializers/security.py:160 +#: settings/serializers/security.py:165 msgid "Connection max idle time" msgstr "接続最大アイドル時間" -#: settings/serializers/security.py:161 +#: settings/serializers/security.py:166 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "アイドル時間がそれ以上の場合は、接続単位を切断します: 分" -#: settings/serializers/security.py:164 +#: settings/serializers/security.py:169 msgid "Remember manual auth" msgstr "手動入力パスワードの保存" -#: settings/serializers/security.py:167 +#: settings/serializers/security.py:172 msgid "Enable change auth secure mode" msgstr "安全モードの変更を有効にする" -#: settings/serializers/security.py:170 +#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "安全でないコマンドアラート" -#: settings/serializers/security.py:173 +#: settings/serializers/security.py:178 msgid "Email recipient" msgstr "メール受信者" -#: settings/serializers/security.py:174 +#: settings/serializers/security.py:179 msgid "Multiple user using , split" msgstr "複数のユーザーを使用して、分割" -#: settings/serializers/security.py:177 -msgid "Batch command execution" -msgstr "バッチコマンドの実行" +#: settings/serializers/security.py:182 +msgid "Operation center" +msgstr "職業センター" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:183 msgid "Allow user run batch command or not using ansible" msgstr "ユーザー実行バッチコマンドを許可するか、ansibleを使用しない" -#: settings/serializers/security.py:181 +#: settings/serializers/security.py:186 msgid "Session share" msgstr "セッション共有" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:187 msgid "Enabled, Allows user active session to be shared with other users" msgstr "" "ユーザーのアクティブなセッションを他のユーザーと共有できるようにします。" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:190 msgid "Remote Login Protection" msgstr "リモートログイン保護" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:192 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -4719,6 +4979,10 @@ msgstr "" "します。アカウントが共通のログイン都市からログインしている場合、システムはリ" "モートログインリマインダーを送信します" +#: settings/serializers/terminal.py:9 +msgid "Hostname" +msgstr "ホスト名" + #: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自動" @@ -5161,7 +5425,7 @@ msgid "Input" msgstr "入力" #: terminal/backends/command/models.py:24 -#: terminal/backends/command/serializers.py:38 +#: terminal/backends/command/serializers.py:39 msgid "Output" msgstr "出力" @@ -5181,23 +5445,23 @@ msgstr "リスクレベル" msgid "Session ID" msgstr "セッションID" -#: terminal/backends/command/serializers.py:37 +#: terminal/backends/command/serializers.py:38 #, fuzzy msgid "Account " msgstr "アカウント" -#: terminal/backends/command/serializers.py:39 +#: terminal/backends/command/serializers.py:40 msgid "Timestamp" msgstr "タイムスタンプ" -#: terminal/backends/command/serializers.py:41 +#: terminal/backends/command/serializers.py:42 #: terminal/models/component/terminal.py:84 msgid "Remote Address" msgstr "リモートアドレス" -#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 -#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 -#: terminal/connect_methods.py:50 +#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 +#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 +#: terminal/connect_methods.py:51 #, fuzzy msgid "DB Client" msgstr "クライアント" @@ -5219,6 +5483,10 @@ msgstr "正常" msgid "Offline" msgstr "オフライン" +#: terminal/const.py:61 +msgid "Mismatch" +msgstr "" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "一括作成非サポート" @@ -5227,20 +5495,26 @@ msgstr "一括作成非サポート" msgid "Storage is invalid" msgstr "ストレージが無効です" -#: terminal/models/applet/applet.py:25 +#: terminal/models/applet/applet.py:26 #, fuzzy msgid "Author" msgstr "資産アカウント" -#: terminal/models/applet/applet.py:30 +#: terminal/models/applet/applet.py:31 msgid "Tags" msgstr "" -#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:35 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "ホスト" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +#: terminal/models/applet/applet.py:135 terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:105 +#, fuzzy +msgid "Hosting" +msgstr "ホスト" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:40 #, fuzzy msgid "Deploy options" msgstr "その他のログインオプション" @@ -5259,48 +5533,61 @@ msgstr "終了日" msgid "Date synced" msgstr "日付の同期" -#: terminal/models/applet/host.py:33 -msgid "Applet host" -msgstr "リモートアプリケーションパブリッシャ" - -#: terminal/models/applet/host.py:105 -#, fuzzy -msgid "Hosting" -msgstr "ホスト" - #: terminal/models/applet/host.py:106 msgid "Initial" msgstr "" #: terminal/models/component/endpoint.py:15 -msgid "HTTPS Port" +msgid "HTTPS port" msgstr "HTTPS ポート" #: terminal/models/component/endpoint.py:16 -msgid "HTTP Port" +msgid "HTTP port" msgstr "HTTP ポート" #: terminal/models/component/endpoint.py:17 -msgid "SSH Port" +msgid "SSH port" msgstr "SSH ポート" #: terminal/models/component/endpoint.py:18 -msgid "RDP Port" +msgid "RDP port" msgstr "RDP ポート" -#: terminal/models/component/endpoint.py:25 -#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/models/component/endpoint.py:19 +#, fuzzy +#| msgid "SSH port" +msgid "MySQL port" +msgstr "SSH ポート" + +#: terminal/models/component/endpoint.py:20 +#, fuzzy +#| msgid "RDP port" +msgid "MariaDB port" +msgstr "RDP ポート" + +#: terminal/models/component/endpoint.py:21 +msgid "PostgreSQL port" +msgstr "" + +#: terminal/models/component/endpoint.py:22 +#, fuzzy +#| msgid "Test port" +msgid "Redis port" +msgstr "テストポート" + +#: terminal/models/component/endpoint.py:29 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "エンドポイント" -#: terminal/models/component/endpoint.py:87 +#: terminal/models/component/endpoint.py:91 msgid "IP group" msgstr "IP グループ" -#: terminal/models/component/endpoint.py:99 +#: terminal/models/component/endpoint.py:103 msgid "Endpoint rule" msgstr "エンドポイントルール" @@ -5466,78 +5753,65 @@ msgstr "レベル" msgid "Batch danger command alert" msgstr "一括危険コマンド警告" -#: terminal/serializers/applet.py:16 -#, fuzzy -msgid "Published" -msgstr "公開キー" - -#: terminal/serializers/applet.py:17 -#, fuzzy -msgid "Unpublished" -msgstr "終了" - -#: terminal/serializers/applet.py:18 -#, fuzzy -msgid "Not match" -msgstr "ユーザーにマッチしなかった" - -#: terminal/serializers/applet.py:32 +#: terminal/serializers/applet.py:27 msgid "Icon" msgstr "" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:23 #, fuzzy msgid "Per Session" msgstr "セッション" -#: terminal/serializers/applet_host.py:22 +#: terminal/serializers/applet_host.py:24 msgid "Per Device" msgstr "" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:30 #, fuzzy msgid "RDS Licensing" msgstr "ライセンス" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:31 msgid "RDS License Server" msgstr "" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS Licensing Mode" msgstr "" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet_host.py:33 +#: terminal/serializers/applet_host.py:35 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet_host.py:34 +#: terminal/serializers/applet_host.py:36 msgid "RDS Remote App Logoff Time Limit" msgstr "" -#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:42 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "ロードステータス" #: terminal/serializers/endpoint.py:14 -msgid "Magnus listen db port" -msgstr "Magnus がリッスンするデータベース ポート" +msgid "Oracle port" +msgstr "Oracle ポート" #: terminal/serializers/endpoint.py:17 -msgid "Magnus Listen port range" -msgstr "Magnus がリッスンするポート範囲" +msgid "Oracle port range" +msgstr "Oracle がリッスンするポート範囲" #: terminal/serializers/endpoint.py:19 msgid "" -"The range of ports that Magnus listens on is modified in the configuration " -"file" -msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" +"Oracle proxy server listen port is dynamic, Each additional Oracle database " +"instance adds a port listener" +msgstr "" +"Oracle プロキシサーバーがリッスンするポートは動的です。追加の Oracle データ" +"ベースインスタンスはポートリスナーを追加します" -#: terminal/serializers/endpoint.py:51 +#: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "" @@ -5548,43 +5822,39 @@ msgstr "" msgid "Tunnel" msgstr "" -#: terminal/serializers/session.py:41 -msgid "User ID" -msgstr "ユーザーID" - -#: terminal/serializers/session.py:42 -msgid "Asset ID" -msgstr "資産ID" - -#: terminal/serializers/session.py:43 -msgid "Login from display" -msgstr "表示からのログイン" - -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:28 terminal/serializers/session.py:50 msgid "Can replay" msgstr "再生できます" -#: terminal/serializers/session.py:46 +#: terminal/serializers/session.py:29 terminal/serializers/session.py:51 msgid "Can join" msgstr "参加できます" -#: terminal/serializers/session.py:47 -msgid "Terminal ID" -msgstr "ターミナル ID" - -#: terminal/serializers/session.py:48 -msgid "Is finished" -msgstr "終了しました" - -#: terminal/serializers/session.py:49 +#: terminal/serializers/session.py:30 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "終了できます" -#: terminal/serializers/session.py:50 +#: terminal/serializers/session.py:46 +msgid "User ID" +msgstr "ユーザーID" + +#: terminal/serializers/session.py:47 +msgid "Asset ID" +msgstr "資産ID" + +#: terminal/serializers/session.py:48 +msgid "Login from display" +msgstr "表示からのログイン" + +#: terminal/serializers/session.py:52 +msgid "Terminal ID" +msgstr "ターミナル ID" + +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "ターミナルディスプレイ" -#: terminal/serializers/session.py:55 +#: terminal/serializers/session.py:60 msgid "Command amount" msgstr "コマンド量" @@ -5662,7 +5932,7 @@ msgstr "見つかりません" msgid "view" msgstr "表示" -#: terminal/utils/db_port_mapper.py:77 +#: terminal/utils/db_port_mapper.py:84 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -5672,7 +5942,7 @@ msgstr "" "サービスによって開かれたポートの数を超えた可能性があります。さらにポートを開" "くには、管理者に連絡してください。" -#: terminal/utils/db_port_mapper.py:103 +#: terminal/utils/db_port_mapper.py:112 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." @@ -5680,7 +5950,7 @@ msgstr "" "使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の" "制限を確認して変更してください. " -#: terminal/utils/db_port_mapper.py:105 +#: terminal/utils/db_port_mapper.py:114 msgid "All available port count: {}, Already use port count: {}" msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" @@ -5781,15 +6051,15 @@ msgid "Body" msgstr "ボディ" #: tickets/models/flow.py:19 tickets/models/flow.py:61 -#: tickets/models/ticket/general.py:40 +#: tickets/models/ticket/general.py:41 msgid "Approve level" msgstr "レベルを承認する" -#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +#: tickets/models/flow.py:24 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "戦略を承認する" -#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +#: tickets/models/flow.py:29 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "アシニーズ" @@ -5823,21 +6093,15 @@ msgstr "システムユーザーの適用" msgid "Select at least one asset or node" msgstr "少なくとも1つのアセットまたはノードを選択します。" -#: tickets/models/ticket/apply_asset.py:14 -#: tickets/serializers/ticket/apply_asset.py:26 -msgid "Apply nodes" -msgstr "ノードの適用" - -#: tickets/models/ticket/apply_asset.py:16 -#: tickets/serializers/ticket/apply_asset.py:22 -msgid "Apply assets" -msgstr "申請資産" - #: tickets/models/ticket/apply_asset.py:17 #, fuzzy msgid "Apply accounts" msgstr "アプリケーションアカウント" +#: tickets/models/ticket/apply_asset.py:26 +msgid "Apply Asset Ticket" +msgstr "申請資産" + #: tickets/models/ticket/command_confirm.py:9 msgid "Run user" msgstr "ユーザーの実行" @@ -5856,11 +6120,11 @@ msgstr "実行コマンド" msgid "Command filter acl" msgstr "コマンドフィルター" -#: tickets/models/ticket/general.py:75 +#: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "チケットステップ" -#: tickets/models/ticket/general.py:93 +#: tickets/models/ticket/general.py:94 msgid "Ticket assignee" msgstr "割り当てられたチケット" @@ -5888,7 +6152,7 @@ msgstr "製造オーダスナップショット" msgid "Please try again" msgstr "もう一度お試しください" -#: tickets/models/ticket/general.py:425 +#: tickets/models/ticket/general.py:458 msgid "Super ticket" msgstr "スーパーチケット" @@ -5933,19 +6197,19 @@ msgstr "チケットが処理されました。プロセッサー- {}" msgid "Ticket has processed - {} ({})" msgstr "チケットが処理済み- {} ({})" -#: tickets/serializers/flow.py:21 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受付者名" -#: tickets/serializers/flow.py:47 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "受付をお選びください" -#: tickets/serializers/flow.py:75 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "現在の組織タイプは既に存在します。" -#: tickets/serializers/super_ticket.py:11 +#: tickets/serializers/super_ticket.py:15 msgid "Processor" msgstr "プロセッサ" @@ -5953,6 +6217,14 @@ msgstr "プロセッサ" msgid "Support fuzzy search, and display up to 10 items" msgstr "ファジー検索をサポートし、最大10項目を表示します。" +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申請資産" + +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "ノードの適用" + #: tickets/serializers/ticket/apply_asset.py:28 msgid "Apply actions" msgstr "申請アクション" @@ -5970,7 +6242,7 @@ msgstr "有効期限は開始日より大きくする必要があります" msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" -#: tickets/serializers/ticket/ticket.py:95 +#: tickets/serializers/ticket/ticket.py:88 msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" @@ -6141,10 +6413,6 @@ msgstr "公開キー" msgid "Force enable" msgstr "強制有効" -#: users/models/user.py:631 -msgid "Local" -msgstr "ローカル" - #: users/models/user.py:687 users/serializers/user.py:160 msgid "Is service account" msgstr "サービスアカウントです" @@ -6178,10 +6446,6 @@ msgstr "秘密キー" msgid "Is first login" msgstr "最初のログインです" -#: users/models/user.py:727 -msgid "Source" -msgstr "ソース" - #: users/models/user.py:731 msgid "Date password last updated" msgstr "最終更新日パスワード" @@ -7118,26 +7382,107 @@ msgstr "ライセンスのインポートに成功" msgid "License is invalid" msgstr "ライセンスが無効です" -#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:135 msgid "License" msgstr "ライセンス" -#: xpack/plugins/license/models.py:71 +#: xpack/plugins/license/models.py:79 msgid "Standard edition" msgstr "標準版" -#: xpack/plugins/license/models.py:73 +#: xpack/plugins/license/models.py:81 msgid "Enterprise edition" msgstr "エンタープライズ版" -#: xpack/plugins/license/models.py:75 +#: xpack/plugins/license/models.py:83 msgid "Ultimate edition" msgstr "究極のエディション" -#: xpack/plugins/license/models.py:77 +#: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "コミュニティ版" +#, fuzzy +#~| msgid "Dynamic user" +#~ msgid "Dynamic username" +#~ msgstr "動的コード" + +#, fuzzy +#~| msgid "Permission named `{}` already exists" +#~ msgid "Username already exists" +#~ msgstr "'{}'という名前の権限は既に存在します" + +#, fuzzy +#~| msgid "Permission named `{}` already exists" +#~ msgid "Dynamic username already exists" +#~ msgstr "'{}'という名前の権限は既に存在します" + +#~ msgid "Commands" +#~ msgstr "コマンド#コマンド#" + +#, fuzzy +#~ msgid "Change password enabled" +#~ msgstr "パスワードの変更" + +#, fuzzy +#~ msgid "Change password method" +#~ msgstr "パスワードの変更" + +#~ msgid "{} used account[{}], login method[{}] login the asset." +#~ msgstr "" +#~ "{} トムはアカウント[{}]、ログイン方法[{}]を使ってこの資産を登録しました" + +#~ msgid "User {} has executed change auth plan for this account.({})" +#~ msgstr "ユーザー {} はこのアカウントのために改密計画を実行しました。({})" + +#~ msgid "" +#~ "The range of ports that Magnus listens on is modified in the " +#~ "configuration file" +#~ msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" + +#~ msgid "Magnus Listen port range" +#~ msgstr "Magnus がリッスンするポート範囲" + +#, fuzzy +#~ msgid "Published" +#~ msgstr "公開キー" + +#, fuzzy +#~ msgid "Unpublished" +#~ msgstr "終了" + +#, fuzzy +#~ msgid "Not match" +#~ msgstr "ユーザーにマッチしなかった" + +#~ msgid "Magnus listen db port" +#~ msgstr "Magnus がリッスンするデータベース ポート" + +#~ msgid "Update task content: {}" +#~ msgstr "タスク内容の更新: {}" + +#~ msgid "Material" +#~ msgstr "マテリアル" + +#~ msgid "Execute batch command" +#~ msgstr "バッチ実行コマンド" + +#, fuzzy +#~ msgid "Create account" +#~ msgstr "アカウントを集める" + +#~ msgid "Present" +#~ msgstr "プレゼント" + +#~ msgid "Date last login" +#~ msgstr "最終ログイン日" + +#~ msgid "IP last login" +#~ msgstr "IP最終ログイン" + +#~ msgid "GatherUser" +#~ msgstr "収集ユーザー" + #~ msgid "Welcome back, please enter username and password to login" #~ msgstr "" #~ "おかえりなさい、ログインするためにユーザー名とパスワードを入力してください" @@ -7405,9 +7750,6 @@ msgstr "コミュニティ版" #~ msgid "Default Cluster" #~ msgstr "デフォルトクラスター" -#~ msgid "Test gateway" -#~ msgstr "テストゲートウェイ" - #~ msgid "User groups" #~ msgstr "ユーザーグループ" @@ -7480,12 +7822,6 @@ msgstr "コミュニティ版" #~ msgid "Only system users with automatic login are allowed" #~ msgstr "自動ログインを持つシステムユーザーのみが許可されます" -#~ msgid "System user name" -#~ msgstr "システムユーザー名" - -#~ msgid "Asset hostname" -#~ msgstr "資産ホスト名" - #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "" #~ "資産 {} システムプラットフォーム {} はAnsibleタスクの実行をサポートしてい" @@ -7589,9 +7925,6 @@ msgstr "コミュニティ版" #~ msgid "Callback" #~ msgstr "コールバック" -#~ msgid "Can view task monitor" -#~ msgstr "タスクモニターを表示できます" - #~ msgid "Tasks" #~ msgstr "タスク" @@ -7631,7 +7964,7 @@ msgstr "コミュニティ版" #~ msgid "Clean task history period" #~ msgstr "クリーンなタスク履歴期間" -#~ msgid "The administrator is modifying permissions. Please wait" +#~ msgid "The administrator is modifyidng permissions. Please wait" #~ msgstr "管理者は権限を変更しています。お待ちください" #~ msgid "The authorization cannot be revoked for the time being" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index f75598dd1..230afb603 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c73f2cbae215e4169dd5d71cb12c434caf465f2d81e37bb90aa2305cebb3ab39 -size 105850 +oid sha256:4af8f2ead4a9d5aaf943efea76305d8cad1ff0692758d21a93937601c6f150fd +size 105736 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index c7ad08aa0..219de87f3 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-12-27 12:11+0800\n" +"POT-Creation-Date: 2023-01-16 14:24+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -17,6 +17,653 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Generator: Poedit 2.4.3\n" +#: accounts/api/automations/base.py:79 +msgid "The parameter 'action' must be [{}]" +msgstr "参数 'action' 必须是 [{}]" + +#: accounts/const/account.py:6 +#: accounts/serializers/automations/change_secret.py:33 +#: assets/models/_user.py:35 audits/signal_handlers.py:51 +#: authentication/confirm/password.py:9 authentication/forms.py:32 +#: authentication/templates/authentication/login.html:288 +#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 +#: users/forms/profile.py:22 users/serializers/user.py:97 +#: users/templates/users/_msg_user_created.html:13 +#: users/templates/users/user_password_verify.html:18 +#: xpack/plugins/cloud/serializers/account_attrs.py:28 +msgid "Password" +msgstr "密码" + +#: accounts/const/account.py:7 +#: accounts/serializers/automations/change_secret.py:34 +msgid "SSH key" +msgstr "SSH 密钥" + +#: accounts/const/account.py:8 authentication/models/access_key.py:33 +msgid "Access key" +msgstr "Access key" + +#: accounts/const/account.py:9 assets/models/_user.py:38 +#: authentication/models/sso_token.py:14 +msgid "Token" +msgstr "Token" + +#: accounts/const/account.py:13 common/db/fields.py:235 +#: settings/serializers/terminal.py:14 +msgid "All" +msgstr "全部" + +#: accounts/const/account.py:14 +msgid "Manual input" +msgstr "手动输入" + +#: accounts/const/account.py:15 +msgid "Dynamic user" +msgstr "同名账号" + +#: accounts/const/account.py:19 users/models/user.py:631 +msgid "Local" +msgstr "数据库" + +#: accounts/const/account.py:20 +#, fuzzy +#| msgid "Collect" +msgid "Collected" +msgstr "收集" + +#: accounts/const/automation.py:22 rbac/tree.py:51 +#, fuzzy +#| msgid "Push asset account" +msgid "Push account" +msgstr "账号推送" + +#: accounts/const/automation.py:23 +msgid "Change secret" +msgstr "更改密码" + +#: accounts/const/automation.py:24 +msgid "Verify account" +msgstr "验证账号" + +#: accounts/const/automation.py:25 +msgid "Gather accounts" +msgstr "收集账号" + +#: accounts/const/automation.py:43 +msgid "Specific password" +msgstr "指定" + +#: accounts/const/automation.py:44 +msgid "Random" +msgstr "" + +#: accounts/const/automation.py:48 ops/const.py:13 +msgid "Append SSH KEY" +msgstr "追加" + +#: accounts/const/automation.py:49 ops/const.py:14 +msgid "Empty and append SSH KEY" +msgstr "清空所有并添加" + +#: accounts/const/automation.py:50 ops/const.py:15 +msgid "Replace (The key generated by JumpServer) " +msgstr "替换 (仅替换由 JumpServer 生成的密钥)" + +#: accounts/const/automation.py:55 +msgid "On asset create" +msgstr "资产创建时" + +#: accounts/const/automation.py:58 +#, fuzzy +#| msgid "On perm change" +msgid "On perm add user" +msgstr "授权变更时" + +#: accounts/const/automation.py:60 +msgid "On perm add user group" +msgstr "" + +#: accounts/const/automation.py:62 +#, fuzzy +#| msgid "permed assets" +msgid "On perm add asset" +msgstr "授权的资产" + +#: accounts/const/automation.py:64 +#, fuzzy +#| msgid "On perm change" +msgid "On perm add node" +msgstr "授权变更时" + +#: accounts/const/automation.py:66 +#, fuzzy +#| msgid "Permed account" +msgid "On perm add account" +msgstr "授权账号" + +#: accounts/const/automation.py:68 +#, fuzzy +#| msgid "Add asset to node" +msgid "On asset join node" +msgstr "添加资产到节点" + +#: accounts/const/automation.py:70 +#, fuzzy +#| msgid "User group" +msgid "On user join group" +msgstr "用户组" + +#: accounts/const/automation.py:78 +msgid "On perm change" +msgstr "授权变更时" + +#: accounts/const/automation.py:85 +#, fuzzy +#| msgid "Perm ungroup node" +msgid "Inherit from group or node" +msgstr "显示未分组节点" + +#: accounts/const/automation.py:93 +msgid "Create and push" +msgstr "创建并推送到资产" + +#: accounts/const/automation.py:94 +msgid "Only create" +msgstr "仅创建到资产" + +#: accounts/models/account.py:47 accounts/serializers/account/account.py:77 +#: accounts/serializers/automations/change_secret.py:107 +#: accounts/serializers/automations/change_secret.py:127 acls/models/base.py:96 +#: acls/serializers/base.py:56 assets/models/asset/common.py:96 +#: assets/models/asset/common.py:281 assets/models/cmd_filter.py:36 +#: assets/serializers/domain.py:19 assets/serializers/label.py:27 +#: audits/models.py:34 authentication/models/connection_token.py:32 +#: perms/models/asset_permission.py:64 perms/serializers/permission.py:27 +#: terminal/backends/command/models.py:21 +#: terminal/backends/command/serializers.py:14 +#: terminal/models/session/session.py:31 terminal/notifications.py:93 +#: tickets/models/ticket/apply_asset.py:16 xpack/plugins/cloud/models.py:220 +msgid "Asset" +msgstr "资产" + +#: accounts/models/account.py:51 accounts/serializers/account/account.py:81 +#: authentication/serializers/connect_token_secret.py:49 +msgid "Su from" +msgstr "切换自" + +#: accounts/models/account.py:53 settings/serializers/auth/cas.py:20 +#: terminal/models/applet/applet.py:25 +msgid "Version" +msgstr "版本" + +#: accounts/models/account.py:55 accounts/serializers/account/account.py:78 +#: users/models/user.py:727 +msgid "Source" +msgstr "来源" + +#: accounts/models/account.py:58 +#: accounts/serializers/automations/change_secret.py:108 +#: accounts/serializers/automations/change_secret.py:128 acls/models/base.py:98 +#: acls/serializers/base.py:57 assets/serializers/asset/common.py:125 +#: assets/serializers/gateway.py:30 audits/models.py:35 ops/models/base.py:18 +#: terminal/backends/command/models.py:22 terminal/models/session/session.py:33 +#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 +msgid "Account" +msgstr "账号" + +#: accounts/models/account.py:64 +msgid "Can view asset account secret" +msgstr "可以查看资产账号密码" + +#: accounts/models/account.py:65 +msgid "Can change asset account secret" +msgstr "可以更改资产账号密码" + +#: accounts/models/account.py:66 +msgid "Can view asset history account" +msgstr "可以查看资产历史账号" + +#: accounts/models/account.py:67 +msgid "Can view asset history account secret" +msgstr "可以查看资产历史账号密码" + +#: accounts/models/account.py:104 accounts/serializers/account/account.py:16 +msgid "Account template" +msgstr "账号模版" + +#: accounts/models/account.py:109 +msgid "Can view asset account template secret" +msgstr "可以查看资产账号密码" + +#: accounts/models/account.py:110 +msgid "Can change asset account template secret" +msgstr "可以更改账号模版密码" + +#: accounts/models/automations/backup_account.py:25 +#: accounts/models/automations/change_secret.py:47 +#: accounts/serializers/account/backup.py:29 +#: accounts/serializers/automations/change_secret.py:56 +msgid "Recipient" +msgstr "收件人" + +#: accounts/models/automations/backup_account.py:34 +#: accounts/models/automations/backup_account.py:96 +msgid "Account backup plan" +msgstr "账号备份计划" + +#: accounts/models/automations/backup_account.py:77 +#: assets/models/automations/base.py:102 audits/models.py:41 +#: ops/models/base.py:55 ops/models/celery.py:63 ops/models/job.py:107 +#: perms/models/asset_permission.py:72 terminal/models/applet/host.py:108 +#: terminal/models/session/session.py:43 +#: tickets/models/ticket/apply_application.py:30 +#: tickets/models/ticket/apply_asset.py:19 +msgid "Date start" +msgstr "开始日期" + +#: accounts/models/automations/backup_account.py:80 +#: authentication/templates/authentication/_msg_oauth_bind.html:11 +#: notifications/notifications.py:186 +msgid "Time" +msgstr "时间" + +#: accounts/models/automations/backup_account.py:84 +msgid "Account backup snapshot" +msgstr "账号备份快照" + +#: accounts/models/automations/backup_account.py:88 +#: accounts/serializers/automations/base.py:42 +#: assets/models/automations/base.py:109 +#: assets/serializers/automations/base.py:40 +msgid "Trigger mode" +msgstr "触发模式" + +#: accounts/models/automations/backup_account.py:91 audits/models.py:130 +#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 +msgid "Reason" +msgstr "原因" + +#: accounts/models/automations/backup_account.py:93 +#: accounts/serializers/automations/change_secret.py:106 +#: accounts/serializers/automations/change_secret.py:129 +#: ops/serializers/job.py:54 terminal/serializers/session.py:49 +msgid "Is success" +msgstr "是否成功" + +#: accounts/models/automations/backup_account.py:101 +msgid "Account backup execution" +msgstr "账号备份执行" + +#: accounts/models/automations/base.py:15 +#, fuzzy +#| msgid "Automation task" +msgid "Account automation task" +msgstr "自动化任务" + +#: accounts/models/automations/base.py:25 +#, fuzzy +#| msgid "Automation task execution" +msgid "Automation execution" +msgstr "自动化任务执行历史" + +#: accounts/models/automations/base.py:26 +#, fuzzy +#| msgid "Automation task execution" +msgid "Automation executions" +msgstr "自动化任务执行历史" + +#: accounts/models/automations/base.py:28 +msgid "Can view change secret execution" +msgstr "查看改密执行" + +#: accounts/models/automations/base.py:29 +msgid "Can add change secret execution" +msgstr "创建改密执行" + +#: accounts/models/automations/base.py:31 +msgid "Can view gather accounts execution" +msgstr "查看收集账号执行" + +#: accounts/models/automations/base.py:32 +msgid "Can add gather accounts execution" +msgstr "创建收集账号执行" + +#: accounts/models/automations/base.py:34 +#, fuzzy +#| msgid "Can view gather accounts execution" +msgid "Can view push account execution" +msgstr "查看收集账号执行" + +#: accounts/models/automations/base.py:35 +#, fuzzy +#| msgid "Can add gather accounts execution" +msgid "Can add push account execution" +msgstr "创建收集账号执行" + +#: accounts/models/automations/change_secret.py:17 accounts/models/base.py:36 +#: accounts/serializers/account/account.py:114 +#: accounts/serializers/account/base.py:16 +#: accounts/serializers/automations/change_secret.py:46 +#: authentication/serializers/connect_token_secret.py:40 +#: authentication/serializers/connect_token_secret.py:50 +msgid "Secret type" +msgstr "密文类型" + +#: accounts/models/automations/change_secret.py:21 +#: accounts/serializers/automations/change_secret.py:40 +msgid "Secret strategy" +msgstr "密文策略" + +#: accounts/models/automations/change_secret.py:23 +#: accounts/models/automations/change_secret.py:72 accounts/models/base.py:38 +#: accounts/serializers/account/base.py:19 +#: authentication/models/temp_token.py:10 +#: authentication/templates/authentication/_access_key_modal.html:31 +#: settings/serializers/auth/radius.py:19 +msgid "Secret" +msgstr "密钥" + +#: accounts/models/automations/change_secret.py:24 +msgid "Password rules" +msgstr "密码规则" + +#: accounts/models/automations/change_secret.py:27 +msgid "SSH key change strategy" +msgstr "SSH 密钥推送方式" + +#: accounts/models/automations/change_secret.py:54 +msgid "Change secret automation" +msgstr "自动化改密" + +#: accounts/models/automations/change_secret.py:71 +msgid "Old secret" +msgstr "原密码" + +#: accounts/models/automations/change_secret.py:73 +msgid "Date started" +msgstr "开始日期" + +#: accounts/models/automations/change_secret.py:74 +#: assets/models/automations/base.py:103 ops/models/base.py:56 +#: ops/models/celery.py:64 ops/models/job.py:108 +#: terminal/models/applet/host.py:109 +msgid "Date finished" +msgstr "结束日期" + +#: accounts/models/automations/change_secret.py:76 common/const/choices.py:20 +msgid "Error" +msgstr "错误" + +#: accounts/models/automations/change_secret.py:80 +msgid "Change secret record" +msgstr "改密记录" + +#: accounts/models/automations/gather_account.py:15 +#: accounts/tasks/gather_accounts.py:28 +msgid "Gather asset accounts" +msgstr "收集账号" + +#: accounts/models/automations/push_account.py:13 +#, fuzzy +#| msgid "Trigger" +msgid "Triggers" +msgstr "触发方式" + +#: accounts/models/automations/push_account.py:14 accounts/models/base.py:34 +#: acls/serializers/base.py:18 acls/serializers/base.py:49 +#: assets/models/_user.py:34 audits/models.py:115 authentication/forms.py:25 +#: authentication/forms.py:27 authentication/models/temp_token.py:9 +#: authentication/templates/authentication/_msg_different_city.html:9 +#: authentication/templates/authentication/_msg_oauth_bind.html:9 +#: users/forms/profile.py:32 users/forms/profile.py:112 +#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 +#: xpack/plugins/cloud/serializers/account_attrs.py:26 +msgid "Username" +msgstr "用户名" + +#: accounts/models/automations/push_account.py:15 acls/models/base.py:77 +#: acls/serializers/base.py:81 assets/models/cmd_filter.py:81 +#: audits/models.py:51 audits/serializers.py:75 +#: authentication/serializers/connect_token_secret.py:108 +#: authentication/templates/authentication/_access_key_modal.html:34 +msgid "Action" +msgstr "动作" + +#: accounts/models/automations/push_account.py:41 +msgid "Push asset account" +msgstr "账号推送" + +#: accounts/models/automations/verify_account.py:15 +msgid "Verify asset account" +msgstr "账号验证" + +#: accounts/models/base.py:33 acls/models/base.py:71 +#: acls/models/command_acl.py:21 acls/serializers/base.py:34 +#: applications/models.py:9 assets/models/_user.py:33 +#: assets/models/asset/common.py:94 assets/models/asset/common.py:106 +#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/models/platform.py:20 assets/models/platform.py:74 +#: assets/serializers/asset/common.py:143 assets/serializers/platform.py:128 +#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 +#: ops/models/adhoc.py:22 ops/models/celery.py:15 ops/models/celery.py:57 +#: ops/models/job.py:25 ops/models/playbook.py:14 orgs/models.py:69 +#: perms/models/asset_permission.py:56 rbac/models/role.py:29 +#: settings/models.py:33 settings/serializers/sms.py:6 +#: terminal/models/applet/applet.py:23 terminal/models/component/endpoint.py:12 +#: terminal/models/component/endpoint.py:90 +#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 +#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 +#: users/models/group.py:13 users/models/user.py:675 +#: xpack/plugins/cloud/models.py:28 +msgid "Name" +msgstr "名称" + +#: accounts/models/base.py:39 +msgid "Privileged" +msgstr "特权账号" + +#: accounts/models/base.py:40 assets/models/asset/common.py:113 +#: assets/models/automations/base.py:21 assets/models/cmd_filter.py:39 +#: assets/models/label.py:22 +#: authentication/serializers/connect_token_secret.py:106 +#: terminal/models/applet/applet.py:28 users/serializers/user.py:158 +msgid "Is active" +msgstr "激活" + +#: accounts/notifications.py:8 +msgid "Notification of account backup route task results" +msgstr "账号备份任务结果通知" + +#: accounts/notifications.py:18 +msgid "" +"{} - The account backup passage task has been completed. See the attachment " +"for details" +msgstr "{} - 账号备份任务已完成, 详情见附件" + +#: accounts/notifications.py:20 +msgid "" +"{} - The account backup passage task has been completed: the encryption " +"password has not been set - please go to personal information -> file " +"encryption password to set the encryption password" +msgstr "" +"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" +"置加密密码" + +#: accounts/notifications.py:31 +msgid "Notification of implementation result of encryption change plan" +msgstr "改密计划任务结果通知" + +#: accounts/notifications.py:41 +msgid "" +"{} - The encryption change task has been completed. See the attachment for " +"details" +msgstr "{} - 改密任务已完成, 详情见附件" + +#: accounts/notifications.py:42 +msgid "" +"{} - The encryption change task has been completed: the encryption password " +"has not been set - please go to personal information -> file encryption " +"password to set the encryption password" +msgstr "" +"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" +"密密码" + +#: accounts/serializers/account/account.py:19 +#: assets/serializers/asset/common.py:52 +msgid "Push now" +msgstr "立即推送" + +#: accounts/serializers/account/account.py:21 +#: accounts/serializers/account/base.py:64 +msgid "Has secret" +msgstr "已托管密码" + +#: accounts/serializers/account/account.py:28 +#: assets/serializers/asset/common.py:79 +msgid "Account template not found" +msgstr "账号模版未找到" + +#: accounts/serializers/account/account.py:73 +msgid "Asset not found" +msgstr "资产不存在" + +#: accounts/serializers/account/backup.py:27 +#: accounts/serializers/automations/base.py:35 +#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 +#: settings/serializers/auth/ldap.py:66 +msgid "Periodic perform" +msgstr "定时执行" + +#: accounts/serializers/account/backup.py:28 +#: accounts/serializers/automations/gather_accounts.py:23 +msgid "Executed amount" +msgstr "执行次数" + +#: accounts/serializers/account/backup.py:30 +#: accounts/serializers/automations/change_secret.py:57 +msgid "Currently only mail sending is supported" +msgstr "当前只支持邮件发送" + +#: accounts/serializers/account/base.py:24 +msgid "Key password" +msgstr "密钥密码" + +#: accounts/serializers/account/base.py:80 +msgid "Specific" +msgstr "指定的" + +#: accounts/serializers/automations/base.py:21 +#: assets/models/automations/base.py:19 +#: assets/serializers/automations/base.py:20 ops/models/base.py:17 +#: ops/models/job.py:35 +#: terminal/templates/terminal/_msg_command_execute_alert.html:16 +msgid "Assets" +msgstr "资产" + +#: accounts/serializers/automations/base.py:22 +#: assets/models/asset/common.py:112 assets/models/automations/base.py:18 +#: assets/models/cmd_filter.py:32 assets/serializers/automations/base.py:21 +#: perms/models/asset_permission.py:67 +msgid "Nodes" +msgstr "节点" + +#: accounts/serializers/automations/base.py:40 +#: assets/models/automations/base.py:105 +#: assets/serializers/automations/base.py:39 +msgid "Automation snapshot" +msgstr "工单快照" + +#: accounts/serializers/automations/base.py:41 acls/models/command_acl.py:24 +#: acls/serializers/command_acl.py:18 applications/models.py:14 +#: assets/models/_user.py:46 assets/models/automations/base.py:20 +#: assets/models/cmd_filter.py:74 assets/models/platform.py:76 +#: assets/serializers/asset/common.py:122 assets/serializers/platform.py:86 +#: audits/serializers.py:47 +#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:33 +#: perms/serializers/user_permission.py:26 terminal/models/applet/applet.py:27 +#: terminal/models/component/storage.py:57 +#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:28 +#: terminal/serializers/session.py:26 terminal/serializers/storage.py:181 +#: tickets/models/comment.py:26 tickets/models/flow.py:56 +#: tickets/models/ticket/apply_application.py:16 +#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:53 +#: tickets/serializers/ticket/ticket.py:19 +msgid "Type" +msgstr "类型" + +#: accounts/serializers/automations/change_secret.py:43 +msgid "SSH Key strategy" +msgstr "SSH 密钥更改方式" + +#: accounts/serializers/automations/change_secret.py:76 +msgid "* Please enter the correct password length" +msgstr "* 请输入正确的密码长度" + +#: accounts/serializers/automations/change_secret.py:80 +msgid "* Password length range 6-30 bits" +msgstr "* 密码长度范围 6-30 位" + +#: accounts/serializers/automations/change_secret.py:110 +#: assets/models/automations/base.py:114 +msgid "Automation task execution" +msgstr "自动化任务执行历史" + +#: accounts/serializers/automations/change_secret.py:150 audits/const.py:45 +#: audits/models.py:40 common/const/choices.py:18 ops/const.py:51 +#: ops/serializers/celery.py:39 terminal/const.py:59 +#: terminal/models/session/sharing.py:103 tickets/views/approve.py:114 +msgid "Success" +msgstr "成功" + +#: accounts/serializers/automations/change_secret.py:151 +#: assets/const/automation.py:8 audits/const.py:46 common/const/choices.py:19 +#: ops/const.py:53 terminal/const.py:60 xpack/plugins/cloud/const.py:41 +msgid "Failed" +msgstr "失败" + +#: accounts/tasks/automation.py:11 +#, fuzzy +#| msgid "Execute automation" +msgid "Account execute automation" +msgstr "执行自动化任务" + +#: accounts/tasks/backup_account.py:13 +msgid "Execute account backup plan" +msgstr "执行账号备份计划" + +#: accounts/tasks/gather_accounts.py:31 +msgid "Gather assets accounts" +msgstr "收集资产上的账号" + +#: accounts/tasks/push_account.py:30 accounts/tasks/push_account.py:36 +msgid "Push accounts to assets" +msgstr "推送账号到资产" + +#: accounts/tasks/verify_account.py:30 +msgid "Verify asset account availability" +msgstr "" + +#: accounts/tasks/verify_account.py:36 +msgid "Verify accounts connectivity" +msgstr "测试账号可连接性" + +#: accounts/utils.py:42 +msgid "Password can not contains `{{` " +msgstr "密码不能包含 `{{` 字符" + +#: accounts/utils.py:45 +msgid "Password can not contains `'` " +msgstr "密码不能包含 `'` 字符" + +#: accounts/utils.py:47 +msgid "Password can not contains `\"` " +msgstr "密码不能包含 `\"` 字符" + +#: accounts/utils.py:53 +msgid "private key invalid or passphrase error" +msgstr "密钥不合法或密钥密码错误" + #: acls/apps.py:7 msgid "Acls" msgstr "访问控制" @@ -34,67 +681,39 @@ msgstr "接受" msgid "Review" msgstr "审批" -#: acls/models/base.py:71 acls/models/command_acl.py:21 -#: acls/serializers/base.py:34 applications/models.py:9 -#: assets/models/_user.py:33 assets/models/asset/common.py:92 -#: assets/models/asset/common.py:101 assets/models/base.py:64 -#: assets/models/cmd_filter.py:21 assets/models/domain.py:18 -#: assets/models/group.py:20 assets/models/label.py:18 -#: assets/models/platform.py:20 assets/models/platform.py:71 -#: assets/serializers/asset/common.py:87 assets/serializers/platform.py:121 -#: authentication/serializers/connect_token_secret.py:102 ops/mixin.py:20 -#: ops/models/adhoc.py:23 ops/models/celery.py:15 ops/models/job.py:24 -#: ops/models/playbook.py:14 orgs/models.py:67 -#: perms/models/asset_permission.py:55 rbac/models/role.py:29 -#: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/applet/applet.py:22 terminal/models/component/endpoint.py:12 -#: terminal/models/component/endpoint.py:86 -#: terminal/models/component/storage.py:26 terminal/models/component/task.py:15 -#: terminal/models/component/terminal.py:79 users/forms/profile.py:33 -#: users/models/group.py:13 users/models/user.py:675 -#: xpack/plugins/cloud/models.py:28 -msgid "Name" -msgstr "名称" - #: acls/models/base.py:73 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:89 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:93 msgid "Priority" msgstr "优先级" #: acls/models/base.py:74 assets/models/_user.py:47 -#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:90 +#: assets/models/cmd_filter.py:76 terminal/models/component/endpoint.py:94 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" -#: acls/models/base.py:77 acls/serializers/base.py:63 -#: assets/models/cmd_filter.py:81 audits/models.py:51 audits/serializers.py:73 -#: authentication/serializers/connect_token_secret.py:108 -#: authentication/templates/authentication/_access_key_modal.html:34 -msgid "Action" -msgstr "动作" - -#: acls/models/base.py:78 acls/serializers/base.py:59 +#: acls/models/base.py:78 acls/serializers/base.py:75 #: acls/serializers/login_acl.py:23 assets/models/cmd_filter.py:86 #: authentication/serializers/connect_token_secret.py:81 msgid "Reviewers" msgstr "审批人" #: acls/models/base.py:79 authentication/models/access_key.py:17 +#: authentication/models/connection_token.py:47 #: authentication/templates/authentication/_access_key_modal.html:32 -#: perms/models/asset_permission.py:75 terminal/models/session/sharing.py:27 +#: perms/models/asset_permission.py:76 terminal/models/session/sharing.py:27 #: tickets/const.py:37 msgid "Active" msgstr "激活中" -#: acls/models/base.py:91 acls/models/login_acl.py:13 +#: acls/models/base.py:94 acls/models/login_acl.py:13 #: acls/serializers/base.py:55 acls/serializers/login_acl.py:21 #: assets/models/cmd_filter.py:24 assets/models/label.py:16 audits/models.py:30 -#: audits/models.py:49 audits/models.py:93 +#: audits/models.py:49 audits/models.py:99 #: authentication/models/connection_token.py:28 #: authentication/models/sso_token.py:16 #: notifications/models/notification.py:12 -#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:57 -#: perms/serializers/permission.py:23 rbac/builtin.py:120 +#: perms/api/user_permission/mixin.py:55 perms/models/asset_permission.py:58 +#: perms/serializers/permission.py:23 rbac/builtin.py:118 #: rbac/models/rolebinding.py:41 terminal/backends/command/models.py:20 #: terminal/backends/command/serializers.py:13 #: terminal/models/session/session.py:29 terminal/models/session/sharing.py:32 @@ -104,35 +723,8 @@ msgstr "激活中" msgid "User" msgstr "用户" -#: acls/models/base.py:93 acls/serializers/base.py:56 -#: assets/models/account.py:50 assets/models/asset/common.py:94 -#: assets/models/asset/common.py:221 assets/models/cmd_filter.py:36 -#: assets/models/gathered_user.py:12 assets/serializers/account/account.py:76 -#: assets/serializers/automations/change_secret.py:100 -#: assets/serializers/automations/change_secret.py:122 -#: assets/serializers/domain.py:19 assets/serializers/gathered_user.py:11 -#: assets/serializers/label.py:27 audits/models.py:34 -#: authentication/models/connection_token.py:32 -#: perms/models/asset_permission.py:63 perms/serializers/permission.py:27 -#: terminal/backends/command/models.py:21 -#: terminal/backends/command/serializers.py:14 -#: terminal/models/session/session.py:31 terminal/notifications.py:93 -#: xpack/plugins/cloud/models.py:220 -msgid "Asset" -msgstr "资产" - -#: acls/models/base.py:95 acls/serializers/base.py:57 -#: assets/models/account.py:60 -#: assets/serializers/automations/change_secret.py:101 -#: assets/serializers/automations/change_secret.py:123 audits/models.py:35 -#: ops/models/base.py:18 terminal/backends/command/models.py:22 -#: terminal/models/session/session.py:33 -#: tickets/models/ticket/command_confirm.py:13 xpack/plugins/cloud/models.py:85 -msgid "Account" -msgstr "账号" - #: acls/models/command_acl.py:16 assets/models/cmd_filter.py:60 -#: terminal/backends/command/serializers.py:15 +#: ops/serializers/job.py:53 terminal/backends/command/serializers.py:15 #: terminal/models/session/session.py:41 terminal/serializers/session.py:19 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 @@ -143,23 +735,6 @@ msgstr "命令" msgid "Regex" msgstr "正则表达式" -#: acls/models/command_acl.py:24 acls/serializers/command_acl.py:19 -#: applications/models.py:14 assets/models/_user.py:46 -#: assets/models/automations/base.py:20 assets/models/cmd_filter.py:74 -#: assets/models/platform.py:73 assets/serializers/asset/common.py:63 -#: assets/serializers/automations/base.py:40 assets/serializers/platform.py:86 -#: audits/serializers.py:45 -#: authentication/serializers/connect_token_secret.py:115 ops/models/job.py:32 -#: perms/serializers/user_permission.py:24 terminal/models/applet/applet.py:26 -#: terminal/models/component/storage.py:57 -#: terminal/models/component/storage.py:146 terminal/serializers/applet.py:33 -#: terminal/serializers/session.py:25 tickets/models/comment.py:26 -#: tickets/models/flow.py:56 tickets/models/ticket/apply_application.py:16 -#: tickets/models/ticket/general.py:275 tickets/serializers/flow.py:54 -#: tickets/serializers/ticket/ticket.py:19 -msgid "Type" -msgstr "类型" - #: acls/models/command_acl.py:26 assets/models/cmd_filter.py:79 #: settings/serializers/basic.py:10 xpack/plugins/license/models.py:29 msgid "Content" @@ -173,7 +748,8 @@ msgstr "每行一个命令" msgid "Ignore case" msgstr "忽略大小写" -#: acls/models/command_acl.py:33 acls/serializers/command_acl.py:29 +#: acls/models/command_acl.py:33 acls/models/command_acl.py:96 +#: acls/serializers/command_acl.py:28 #: authentication/serializers/connect_token_secret.py:78 msgid "Command group" msgstr "命令组" @@ -182,10 +758,6 @@ msgstr "命令组" msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" -#: acls/models/command_acl.py:96 -msgid "Commands" -msgstr "命令" - #: acls/models/command_acl.py:100 msgid "Command acl" msgstr "命令过滤" @@ -194,7 +766,7 @@ msgstr "命令过滤" msgid "Command confirm" msgstr "命令复核" -#: acls/models/login_acl.py:16 +#: acls/models/login_acl.py:16 acls/serializers/login_acl.py:29 msgid "Rule" msgstr "规则" @@ -218,19 +790,6 @@ msgstr "登录资产复核" msgid "Format for comma-delimited string, with * indicating a match all. " msgstr "格式为逗号分隔的字符串, * 表示匹配所有. " -#: acls/serializers/base.py:18 acls/serializers/base.py:49 -#: assets/models/_user.py:34 assets/models/base.py:65 -#: assets/models/gathered_user.py:13 audits/models.py:109 -#: authentication/forms.py:25 authentication/forms.py:27 -#: authentication/models/temp_token.py:9 -#: authentication/templates/authentication/_msg_different_city.html:9 -#: authentication/templates/authentication/_msg_oauth_bind.html:9 -#: users/forms/profile.py:32 users/forms/profile.py:112 -#: users/models/user.py:673 users/templates/users/_msg_user_created.html:12 -#: xpack/plugins/cloud/serializers/account_attrs.py:26 -msgid "Username" -msgstr "用户名" - #: acls/serializers/base.py:25 msgid "" "Format for comma-delimited string, with * indicating a match all. Such as: " @@ -244,14 +803,50 @@ msgstr "" msgid "IP/Host" msgstr "IP/主机" -#: acls/serializers/base.py:90 tickets/serializers/ticket/ticket.py:78 +#: acls/serializers/base.py:60 +#, fuzzy +#| msgid "System user name" +msgid "User (username)" +msgstr "系统用户名称" + +#: acls/serializers/base.py:64 +#, fuzzy +#| msgid "Asset hostname" +msgid "Asset (name)" +msgstr "资产主机名" + +#: acls/serializers/base.py:68 +#, fuzzy +#| msgid "Address" +msgid "Asset (address)" +msgstr "地址" + +#: acls/serializers/base.py:72 +#, fuzzy +#| msgid "Account name" +msgid "Account (username)" +msgstr "账号名称" + +#: acls/serializers/base.py:78 acls/serializers/login_acl.py:27 +#, fuzzy +#| msgid "Reviewers" +msgid "Reviewers amount" +msgstr "审批人" + +#: acls/serializers/base.py:109 tickets/serializers/ticket/ticket.py:76 msgid "The organization `{}` does not exist" msgstr "组织 `{}` 不存在" -#: acls/serializers/base.py:96 +#: acls/serializers/base.py:115 msgid "None of the reviewers belong to Organization `{}`" msgstr "所有复核人都不属于组织 `{}`" +#: acls/serializers/command_acl.py:31 +#, fuzzy +#| msgid "Command amount" +msgid "Command group amount" +msgstr "命令数量" + #: acls/serializers/rules/rules.py:20 #: xpack/plugins/cloud/serializers/task.py:22 msgid "IP address invalid: `{}`" @@ -266,11 +861,11 @@ msgstr "" "格式为逗号分隔的字符串, * 表示匹配所有。例如: 192.168.10.1, 192.168.1.0/24, " "10.1.1.1-10.1.1.20, 2001:db8:2de::e13, 2001:db8:1a:1110::/64" -#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:102 +#: acls/serializers/rules/rules.py:33 assets/models/asset/common.py:107 #: authentication/templates/authentication/_msg_oauth_bind.html:12 #: authentication/templates/authentication/_msg_rest_password_success.html:8 #: authentication/templates/authentication/_msg_rest_public_key_success.html:8 -#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:54 +#: settings/serializers/terminal.py:10 terminal/serializers/endpoint.py:61 msgid "IP" msgstr "IP" @@ -283,9 +878,9 @@ msgid "Applications" msgstr "应用管理" #: applications/models.py:11 assets/models/label.py:21 -#: assets/models/platform.py:72 assets/serializers/asset/common.py:62 +#: assets/models/platform.py:75 assets/serializers/asset/common.py:121 #: assets/serializers/cagegory.py:8 assets/serializers/platform.py:87 -#: assets/serializers/platform.py:122 perms/serializers/user_permission.py:23 +#: assets/serializers/platform.py:129 perms/serializers/user_permission.py:25 #: settings/models.py:35 tickets/models/ticket/apply_application.py:13 msgid "Category" msgstr "类别" @@ -304,7 +899,7 @@ msgid "Can match application" msgstr "匹配应用" #: applications/serializers/attrs/application_type/clickhouse.py:11 -#: assets/models/asset/common.py:93 assets/models/platform.py:21 +#: assets/models/asset/common.py:95 assets/models/platform.py:21 #: settings/serializers/auth/radius.py:17 settings/serializers/auth/sms.py:68 #: xpack/plugins/cloud/serializers/account_attrs.py:73 msgid "Port" @@ -316,11 +911,7 @@ msgid "" "different ports" msgstr "默认端口为9000, HTTP接口和本机接口使用不同的端口" -#: assets/api/automations/base.py:77 -msgid "The parameter 'action' must be [{}]" -msgstr "参数 'action' 必须是 [{}]" - -#: assets/api/domain.py:56 +#: assets/api/domain.py:57 msgid "Number required" msgstr "需要为数字" @@ -340,103 +931,56 @@ msgstr "删除失败,节点包含资产" msgid "App assets" msgstr "资产管理" -#: assets/automations/base/manager.py:123 +#: assets/automations/base/manager.py:76 msgid "{} disabled" msgstr "{} 已禁用" -#: assets/const/account.py:6 audits/const.py:6 audits/const.py:64 +#: assets/automations/ping_gateway/manager.py:33 +#: authentication/models/connection_token.py:113 +msgid "No account" +msgstr "没有账号" + +#: assets/automations/ping_gateway/manager.py:55 +#, python-brace-format +msgid "Unable to connect to port {port} on {address}" +msgstr "无法连接到 {port} 上的端口 {address}" + +#: assets/automations/ping_gateway/manager.py:58 +#: authentication/middleware.py:76 xpack/plugins/cloud/providers/fc.py:48 +msgid "Authentication failed" +msgstr "认证失败" + +#: assets/automations/ping_gateway/manager.py:60 +#: assets/automations/ping_gateway/manager.py:86 +msgid "Connect failed" +msgstr "连接失败" + +#: assets/const/automation.py:6 audits/const.py:6 audits/const.py:35 #: common/utils/ip/geoip/utils.py:31 common/utils/ip/geoip/utils.py:37 #: common/utils/ip/utils.py:84 msgid "Unknown" msgstr "未知" -#: assets/const/account.py:7 +#: assets/const/automation.py:7 msgid "Ok" msgstr "成功" -#: assets/const/account.py:8 -#: assets/serializers/automations/change_secret.py:118 -#: assets/serializers/automations/change_secret.py:146 audits/const.py:75 -#: common/const/choices.py:19 ops/const.py:51 xpack/plugins/cloud/const.py:41 -msgid "Failed" -msgstr "失败" - -#: assets/const/account.py:12 assets/models/_user.py:35 -#: audits/signal_handlers.py:49 authentication/confirm/password.py:9 -#: authentication/forms.py:32 -#: authentication/templates/authentication/login.html:288 -#: settings/serializers/auth/ldap.py:25 settings/serializers/auth/ldap.py:47 -#: users/forms/profile.py:22 users/serializers/user.py:97 -#: users/templates/users/_msg_user_created.html:13 -#: users/templates/users/user_password_verify.html:18 -#: xpack/plugins/cloud/serializers/account_attrs.py:28 -msgid "Password" -msgstr "密码" - -#: assets/const/account.py:13 -msgid "SSH key" -msgstr "SSH 密钥" - -#: assets/const/account.py:14 authentication/models/access_key.py:33 -msgid "Access key" -msgstr "访问密钥" - -#: assets/const/account.py:15 assets/models/_user.py:38 -#: authentication/models/sso_token.py:14 -msgid "Token" -msgstr "令牌" - -#: assets/const/automation.py:13 +#: assets/const/automation.py:12 msgid "Ping" msgstr "" +#: assets/const/automation.py:13 +#, fuzzy +#| msgid "Test gateway" +msgid "Ping gateway" +msgstr "测试网关" + #: assets/const/automation.py:14 msgid "Gather facts" msgstr "收集资产信息" -#: assets/const/automation.py:15 -msgid "Create account" -msgstr "创建账号" - -#: assets/const/automation.py:16 -msgid "Change secret" -msgstr "更改密码" - -#: assets/const/automation.py:17 -msgid "Verify account" -msgstr "验证账号" - -#: assets/const/automation.py:18 -msgid "Gather accounts" -msgstr "收集账号" - -#: assets/const/automation.py:38 assets/serializers/account/base.py:29 -msgid "Specific" -msgstr "特有的" - -#: assets/const/automation.py:39 ops/const.py:20 -msgid "All assets use the same random password" -msgstr "使用相同的随机密码" - -#: assets/const/automation.py:40 ops/const.py:21 -msgid "All assets use different random password" -msgstr "使用不同的随机密码" - -#: assets/const/automation.py:44 ops/const.py:13 -msgid "Append SSH KEY" -msgstr "追加" - -#: assets/const/automation.py:45 ops/const.py:14 -msgid "Empty and append SSH KEY" -msgstr "清空所有并添加" - -#: assets/const/automation.py:46 ops/const.py:15 -msgid "Replace (The key generated by JumpServer) " -msgstr "替换 (由 JumpServer 生成的密钥)" - #: assets/const/category.py:11 settings/serializers/auth/radius.py:16 -#: settings/serializers/auth/sms.py:67 terminal/models/applet/applet.py:129 -#: terminal/models/component/endpoint.py:13 +#: settings/serializers/auth/sms.py:67 terminal/models/component/endpoint.py:13 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" @@ -445,8 +989,8 @@ msgstr "主机" msgid "Device" msgstr "网络设备" -#: assets/const/category.py:13 assets/models/asset/database.py:8 -#: assets/models/asset/database.py:34 +#: assets/const/category.py:13 assets/models/asset/database.py:9 +#: assets/models/asset/database.py:32 msgid "Database" msgstr "数据库" @@ -454,12 +998,12 @@ msgstr "数据库" msgid "Cloud service" msgstr "云服务" -#: assets/const/category.py:15 audits/const.py:62 -#: terminal/models/applet/applet.py:20 +#: assets/const/category.py:15 audits/const.py:33 +#: terminal/models/applet/applet.py:21 msgid "Web" msgstr "Web" -#: assets/const/device.py:7 terminal/models/applet/applet.py:19 +#: assets/const/device.py:7 terminal/models/applet/applet.py:20 #: tickets/const.py:8 msgid "General" msgstr "一般" @@ -476,7 +1020,7 @@ msgstr "路由器" msgid "Firewall" msgstr "防火墙" -#: assets/const/types.py:181 +#: assets/const/types.py:200 msgid "All types" msgstr "所有类型" @@ -510,33 +1054,33 @@ msgstr "SSH公钥" #: assets/models/_user.py:40 assets/models/cmd_filter.py:40 #: assets/models/cmd_filter.py:88 assets/models/group.py:23 -#: assets/models/platform.py:76 common/db/models.py:78 ops/models/adhoc.py:29 -#: ops/models/job.py:40 ops/models/playbook.py:17 rbac/models/role.py:37 -#: settings/models.py:38 terminal/models/applet/applet.py:31 -#: terminal/models/applet/applet.py:131 terminal/models/applet/host.py:110 -#: terminal/models/component/endpoint.py:20 -#: terminal/models/component/endpoint.py:96 +#: assets/models/platform.py:79 common/db/models.py:37 ops/models/adhoc.py:28 +#: ops/models/job.py:41 ops/models/playbook.py:17 rbac/models/role.py:37 +#: settings/models.py:38 terminal/models/applet/applet.py:32 +#: terminal/models/applet/applet.py:137 terminal/models/applet/host.py:110 +#: terminal/models/component/endpoint.py:24 +#: terminal/models/component/endpoint.py:100 #: terminal/models/session/session.py:45 tickets/models/comment.py:32 #: tickets/models/ticket/general.py:297 users/models/user.py:714 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:119 msgid "Comment" msgstr "备注" -#: assets/models/_user.py:41 assets/models/automations/base.py:91 +#: assets/models/_user.py:41 assets/models/automations/base.py:101 #: assets/models/cmd_filter.py:41 assets/models/group.py:22 -#: common/db/models.py:76 ops/models/base.py:54 ops/models/job.py:105 +#: common/db/models.py:35 ops/models/base.py:54 ops/models/job.py:106 #: users/models/user.py:932 msgid "Date created" msgstr "创建日期" #: assets/models/_user.py:42 assets/models/cmd_filter.py:42 -#: common/db/models.py:77 +#: common/db/models.py:36 msgid "Date updated" msgstr "更新日期" #: assets/models/_user.py:43 assets/models/cmd_filter.py:44 #: assets/models/cmd_filter.py:91 assets/models/group.py:21 -#: common/db/models.py:74 users/models/user.py:722 +#: common/db/models.py:33 users/models/user.py:722 #: users/serializers/group.py:33 msgid "Created by" msgstr "创建者" @@ -547,8 +1091,8 @@ msgstr "用户名与用户相同" #: assets/models/_user.py:48 authentication/models/connection_token.py:37 #: authentication/serializers/connect_token_secret.py:103 -#: terminal/models/applet/applet.py:29 terminal/serializers/session.py:24 -#: terminal/serializers/session.py:40 terminal/serializers/storage.py:68 +#: terminal/models/applet/applet.py:30 terminal/serializers/session.py:24 +#: terminal/serializers/session.py:45 terminal/serializers/storage.py:68 msgid "Protocol" msgstr "协议" @@ -560,7 +1104,7 @@ msgstr "自动推送" msgid "Sudo" msgstr "Sudo" -#: assets/models/_user.py:51 ops/const.py:44 ops/models/adhoc.py:19 +#: assets/models/_user.py:51 ops/const.py:44 msgid "Shell" msgstr "Shell" @@ -596,136 +1140,75 @@ msgstr "系统用户" msgid "Can match system user" msgstr "可以匹配系统用户" -#: assets/models/account.py:44 common/db/fields.py:232 -#: settings/serializers/terminal.py:14 -msgid "All" -msgstr "全部" - -#: assets/models/account.py:45 -msgid "Manual input" -msgstr "手动输入" - -#: assets/models/account.py:46 -msgid "Dynamic user" -msgstr "同名账号" - -#: assets/models/account.py:54 assets/serializers/account/account.py:79 -#: authentication/serializers/connect_token_secret.py:48 -msgid "Su from" -msgstr "切换自" - -#: assets/models/account.py:56 settings/serializers/auth/cas.py:20 -#: terminal/models/applet/applet.py:24 -msgid "Version" -msgstr "版本" - -#: assets/models/account.py:66 -msgid "Can view asset account secret" -msgstr "可以查看资产账号密码" - -#: assets/models/account.py:67 -msgid "Can change asset account secret" -msgstr "可以更改资产账号密码" - -#: assets/models/account.py:68 -msgid "Can view asset history account" -msgstr "可以查看资产历史账号" - -#: assets/models/account.py:69 -msgid "Can view asset history account secret" -msgstr "可以查看资产历史账号密码" - -#: assets/models/account.py:106 assets/serializers/account/account.py:15 -msgid "Account template" -msgstr "账号模版" - -#: assets/models/account.py:111 -msgid "Can view asset account template secret" -msgstr "可以查看资产账号密码" - -#: assets/models/account.py:112 -msgid "Can change asset account template secret" -msgstr "可以更改账号模版密码" - -#: assets/models/asset/common.py:103 assets/models/platform.py:109 -#: assets/serializers/asset/common.py:65 +#: assets/models/asset/common.py:108 assets/models/platform.py:112 #: authentication/serializers/connect_token_secret.py:107 -#: perms/serializers/user_permission.py:21 +#: perms/serializers/user_permission.py:23 #: xpack/plugins/cloud/serializers/account_attrs.py:179 msgid "Platform" msgstr "系统平台" -#: assets/models/asset/common.py:105 assets/models/domain.py:21 -#: assets/serializers/asset/common.py:64 +#: assets/models/asset/common.py:110 assets/models/domain.py:21 #: authentication/serializers/connect_token_secret.py:125 +#: perms/serializers/user_permission.py:27 msgid "Domain" msgstr "网域" -#: assets/models/asset/common.py:107 assets/models/automations/base.py:18 -#: assets/models/cmd_filter.py:32 assets/serializers/asset/common.py:66 -#: assets/serializers/automations/base.py:21 -#: perms/models/asset_permission.py:66 -msgid "Nodes" -msgstr "节点" - -#: assets/models/asset/common.py:108 assets/models/automations/base.py:21 -#: assets/models/base.py:71 assets/models/cmd_filter.py:39 -#: assets/models/label.py:22 -#: authentication/serializers/connect_token_secret.py:106 -#: terminal/models/applet/applet.py:27 users/serializers/user.py:158 -msgid "Is active" -msgstr "激活" - -#: assets/models/asset/common.py:109 assets/serializers/asset/common.py:67 +#: assets/models/asset/common.py:114 msgid "Labels" msgstr "标签管理" -#: assets/models/asset/common.py:224 +#: assets/models/asset/common.py:284 msgid "Can refresh asset hardware info" msgstr "可以更新资产硬件信息" -#: assets/models/asset/common.py:225 +#: assets/models/asset/common.py:285 msgid "Can test asset connectivity" msgstr "可以测试资产连接性" -#: assets/models/asset/common.py:226 +#: assets/models/asset/common.py:286 msgid "Can push account to asset" msgstr "可以推送账号到资产" -#: assets/models/asset/common.py:227 +#: assets/models/asset/common.py:287 +#, fuzzy +#| msgid "Verify account" +msgid "Can verify account" +msgstr "验证账号" + +#: assets/models/asset/common.py:288 msgid "Can match asset" msgstr "可以匹配资产" -#: assets/models/asset/common.py:228 +#: assets/models/asset/common.py:289 msgid "Add asset to node" msgstr "添加资产到节点" -#: assets/models/asset/common.py:229 +#: assets/models/asset/common.py:290 msgid "Move asset to node" msgstr "移动资产到节点" -#: assets/models/asset/database.py:9 settings/serializers/email.py:37 +#: assets/models/asset/database.py:10 settings/serializers/email.py:37 msgid "Use SSL" msgstr "使用 SSL" -#: assets/models/asset/database.py:10 +#: assets/models/asset/database.py:11 msgid "CA cert" msgstr "CA 证书" -#: assets/models/asset/database.py:11 +#: assets/models/asset/database.py:12 msgid "Client cert" msgstr "客户端证书" -#: assets/models/asset/database.py:12 +#: assets/models/asset/database.py:13 msgid "Client key" msgstr "客户端密钥" -#: assets/models/asset/database.py:13 +#: assets/models/asset/database.py:14 msgid "Allow invalid cert" msgstr "忽略证书校验" -#: assets/models/asset/web.py:9 audits/const.py:68 -#: terminal/serializers/applet_host.py:25 +#: assets/models/asset/web.py:9 audits/const.py:39 +#: terminal/serializers/applet_host.py:27 msgid "Disabled" msgstr "禁用" @@ -755,137 +1238,32 @@ msgid "Submit selector" msgstr "确认按钮选择器" #: assets/models/automations/base.py:17 assets/models/cmd_filter.py:38 -#: assets/serializers/asset/common.py:69 perms/models/asset_permission.py:69 +#: assets/serializers/asset/common.py:241 perms/models/asset_permission.py:70 #: perms/serializers/permission.py:32 rbac/tree.py:36 msgid "Accounts" msgstr "账号管理" -#: assets/models/automations/base.py:19 -#: assets/serializers/automations/base.py:20 ops/models/base.py:17 -#: ops/models/job.py:34 -#: terminal/templates/terminal/_msg_command_execute_alert.html:16 -msgid "Assets" -msgstr "资产" - -#: assets/models/automations/base.py:81 assets/models/automations/base.py:88 +#: assets/models/automations/base.py:28 assets/models/automations/base.py:98 msgid "Automation task" msgstr "自动化任务" -#: assets/models/automations/base.py:90 audits/models.py:129 -#: audits/serializers.py:46 ops/models/base.py:49 ops/models/job.py:98 -#: terminal/models/applet/applet.py:130 terminal/models/applet/host.py:107 -#: terminal/models/component/status.py:27 terminal/serializers/applet.py:22 -#: tickets/models/ticket/general.py:283 tickets/serializers/ticket/ticket.py:20 -#: xpack/plugins/cloud/models.py:172 xpack/plugins/cloud/models.py:224 +#: assets/models/automations/base.py:91 +#, fuzzy +#| msgid "Automation task" +msgid "Asset automation task" +msgstr "自动化任务" + +#: assets/models/automations/base.py:100 audits/models.py:135 +#: audits/serializers.py:48 ops/models/base.py:49 ops/models/job.py:99 +#: terminal/models/applet/applet.py:136 terminal/models/applet/host.py:107 +#: terminal/models/component/status.py:27 terminal/serializers/applet.py:17 +#: terminal/serializers/applet_host.py:90 tickets/models/ticket/general.py:283 +#: tickets/serializers/super_ticket.py:13 +#: tickets/serializers/ticket/ticket.py:20 xpack/plugins/cloud/models.py:172 +#: xpack/plugins/cloud/models.py:224 msgid "Status" msgstr "状态" -#: assets/models/automations/base.py:92 assets/models/backup.py:73 -#: audits/models.py:41 ops/models/base.py:55 ops/models/celery.py:60 -#: ops/models/job.py:106 perms/models/asset_permission.py:71 -#: terminal/models/applet/host.py:108 terminal/models/session/session.py:43 -#: tickets/models/ticket/apply_application.py:30 -#: tickets/models/ticket/apply_asset.py:19 -msgid "Date start" -msgstr "开始日期" - -#: assets/models/automations/base.py:93 -#: assets/models/automations/change_secret.py:59 ops/models/base.py:56 -#: ops/models/celery.py:61 ops/models/job.py:107 -#: terminal/models/applet/host.py:109 -msgid "Date finished" -msgstr "结束日期" - -#: assets/models/automations/base.py:95 -#: assets/serializers/automations/base.py:39 -msgid "Automation snapshot" -msgstr "工单快照" - -#: assets/models/automations/base.py:99 assets/models/backup.py:84 -#: assets/serializers/automations/base.py:41 -msgid "Trigger mode" -msgstr "触发模式" - -#: assets/models/automations/base.py:103 -#: assets/serializers/automations/change_secret.py:103 -msgid "Automation task execution" -msgstr "自动化任务执行历史" - -#: assets/models/automations/base.py:105 -msgid "Can view change secret execution" -msgstr "查看改密执行" - -#: assets/models/automations/base.py:106 -msgid "Can add change secret execution" -msgstr "创建改密执行" - -#: assets/models/automations/base.py:107 -msgid "Can view gather accounts execution" -msgstr "查看收集账号执行" - -#: assets/models/automations/base.py:108 -msgid "Can add gather accounts execution" -msgstr "创建收集账号执行" - -#: assets/models/automations/change_secret.py:15 assets/models/base.py:67 -#: assets/serializers/account/account.py:112 assets/serializers/base.py:13 -#: authentication/serializers/connect_token_secret.py:39 -#: authentication/serializers/connect_token_secret.py:49 -msgid "Secret type" -msgstr "密文类型" - -#: assets/models/automations/change_secret.py:19 -#: assets/serializers/automations/change_secret.py:25 -msgid "Secret strategy" -msgstr "密文策略" - -#: assets/models/automations/change_secret.py:21 -#: assets/models/automations/change_secret.py:57 assets/models/base.py:69 -#: assets/serializers/base.py:16 authentication/models/temp_token.py:10 -#: authentication/templates/authentication/_access_key_modal.html:31 -#: settings/serializers/auth/radius.py:19 -msgid "Secret" -msgstr "密钥" - -#: assets/models/automations/change_secret.py:22 -msgid "Password rules" -msgstr "密码规则" - -#: assets/models/automations/change_secret.py:25 -msgid "SSH key change strategy" -msgstr "SSH 密钥策略" - -#: assets/models/automations/change_secret.py:27 assets/models/backup.py:25 -#: assets/serializers/account/backup.py:30 -#: assets/serializers/automations/change_secret.py:40 -msgid "Recipient" -msgstr "收件人" - -#: assets/models/automations/change_secret.py:34 -msgid "Change secret automation" -msgstr "自动化改密" - -#: assets/models/automations/change_secret.py:56 -msgid "Old secret" -msgstr "原密码" - -#: assets/models/automations/change_secret.py:58 -msgid "Date started" -msgstr "开始日期" - -#: assets/models/automations/change_secret.py:61 common/const/choices.py:20 -msgid "Error" -msgstr "错误" - -#: assets/models/automations/change_secret.py:64 -msgid "Change secret record" -msgstr "改密记录" - -#: assets/models/automations/gather_accounts.py:15 -#: assets/tasks/gather_accounts.py:28 -msgid "Gather asset accounts" -msgstr "收集账号" - #: assets/models/automations/gather_facts.py:15 msgid "Gather asset facts" msgstr "收集资产信息" @@ -894,57 +1272,15 @@ msgstr "收集资产信息" msgid "Ping asset" msgstr "测试资产" -#: assets/models/automations/push_account.py:16 -msgid "Push asset account" -msgstr "账号推送" - -#: assets/models/automations/verify_account.py:15 -msgid "Verify asset account" -msgstr "账号验证" - -#: assets/models/backup.py:34 assets/models/backup.py:92 -msgid "Account backup plan" -msgstr "账号备份计划" - -#: assets/models/backup.py:76 -#: authentication/templates/authentication/_msg_oauth_bind.html:11 -#: notifications/notifications.py:186 -msgid "Time" -msgstr "时间" - -#: assets/models/backup.py:80 -msgid "Account backup snapshot" -msgstr "账号备份快照" - -#: assets/models/backup.py:87 audits/models.py:124 -#: terminal/models/session/sharing.py:107 xpack/plugins/cloud/models.py:176 -msgid "Reason" -msgstr "原因" - -#: assets/models/backup.py:89 -#: assets/serializers/automations/change_secret.py:99 -#: assets/serializers/automations/change_secret.py:124 -#: terminal/serializers/session.py:44 -msgid "Is success" -msgstr "是否成功" - -#: assets/models/backup.py:96 -msgid "Account backup execution" -msgstr "账号备份执行" - -#: assets/models/base.py:26 +#: assets/models/base.py:19 msgid "Connectivity" msgstr "可连接性" -#: assets/models/base.py:28 authentication/models/temp_token.py:12 +#: assets/models/base.py:21 authentication/models/temp_token.py:12 msgid "Date verified" msgstr "校验日期" -#: assets/models/base.py:70 -msgid "Privileged" -msgstr "特权账号" - -#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:60 +#: assets/models/cmd_filter.py:28 perms/models/asset_permission.py:61 #: perms/serializers/permission.py:25 users/models/group.py:25 #: users/models/user.py:681 msgid "User group" @@ -974,44 +1310,14 @@ msgstr "过滤器" msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/gateway.py:40 assets/serializers/domain.py:16 +#: assets/models/favorite_asset.py:17 +msgid "Favorite Asset" +msgstr "收藏的资产" + +#: assets/models/gateway.py:35 assets/serializers/domain.py:16 msgid "Gateway" msgstr "网关" -#: assets/models/gateway.py:62 authentication/models/connection_token.py:104 -msgid "No account" -msgstr "没有账号" - -#: assets/models/gateway.py:84 -#, python-brace-format -msgid "Unable to connect to port {port} on {address}" -msgstr "无法连接到 {port} 上的端口 {address}" - -#: assets/models/gateway.py:87 authentication/middleware.py:76 -#: xpack/plugins/cloud/providers/fc.py:48 -msgid "Authentication failed" -msgstr "认证失败" - -#: assets/models/gateway.py:89 assets/models/gateway.py:116 -msgid "Connect failed" -msgstr "连接失败" - -#: assets/models/gathered_user.py:14 -msgid "Present" -msgstr "存在" - -#: assets/models/gathered_user.py:15 -msgid "Date last login" -msgstr "最后登录日期" - -#: assets/models/gathered_user.py:16 -msgid "IP last login" -msgstr "最后登录IP" - -#: assets/models/gathered_user.py:27 -msgid "GatherUser" -msgstr "收集用户" - #: assets/models/group.py:30 msgid "Asset group" msgstr "资产组" @@ -1033,14 +1339,14 @@ msgstr "系统" #: assets/serializers/cagegory.py:7 assets/serializers/cagegory.py:14 #: authentication/models/connection_token.py:25 #: authentication/serializers/connect_token_secret.py:114 -#: common/drf/serializers/common.py:82 settings/models.py:34 +#: common/serializers/common.py:82 settings/models.py:34 msgid "Value" msgstr "值" -#: assets/models/label.py:40 assets/serializers/cagegory.py:6 -#: assets/serializers/cagegory.py:13 +#: assets/models/label.py:40 assets/serializers/asset/common.py:123 +#: assets/serializers/cagegory.py:6 assets/serializers/cagegory.py:13 #: authentication/serializers/connect_token_secret.py:113 -#: common/drf/serializers/common.py:81 settings/serializers/sms.py:7 +#: common/serializers/common.py:81 settings/serializers/sms.py:7 msgid "Label" msgstr "标签" @@ -1052,7 +1358,7 @@ msgstr "新节点" msgid "empty" msgstr "空" -#: assets/models/node.py:551 perms/models/perm_node.py:27 +#: assets/models/node.py:551 perms/models/perm_node.py:28 msgid "Key" msgstr "键" @@ -1060,12 +1366,12 @@ msgstr "键" msgid "Full value" msgstr "全称" -#: assets/models/node.py:557 perms/models/perm_node.py:29 +#: assets/models/node.py:557 perms/models/perm_node.py:30 msgid "Parent key" msgstr "ssh私钥" #: assets/models/node.py:566 perms/serializers/permission.py:28 -#: xpack/plugins/cloud/models.py:96 +#: tickets/models/ticket/apply_asset.py:14 xpack/plugins/cloud/models.py:96 msgid "Node" msgstr "节点" @@ -1082,8 +1388,8 @@ msgstr "必须的" msgid "Setting" msgstr "设置" -#: assets/models/platform.py:41 audits/const.py:69 settings/models.py:37 -#: terminal/serializers/applet_host.py:26 +#: assets/models/platform.py:41 audits/const.py:40 settings/models.py:37 +#: terminal/serializers/applet_host.py:28 msgid "Enabled" msgstr "启用" @@ -1099,162 +1405,106 @@ msgstr "启用资产探活" msgid "Ping method" msgstr "资产探活方式" -#: assets/models/platform.py:45 assets/models/platform.py:55 +#: assets/models/platform.py:45 assets/models/platform.py:58 msgid "Gather facts enabled" msgstr "收集资产信息" -#: assets/models/platform.py:46 assets/models/platform.py:57 +#: assets/models/platform.py:46 assets/models/platform.py:60 msgid "Gather facts method" msgstr "收集信息方式" #: assets/models/platform.py:47 +#, fuzzy +#| msgid "Change secret record" +msgid "Change secret enabled" +msgstr "改密记录" + +#: assets/models/platform.py:49 +#, fuzzy +#| msgid "Change secret record" +msgid "Change secret method" +msgstr "改密记录" + +#: assets/models/platform.py:51 msgid "Push account enabled" msgstr "启用账号推送" -#: assets/models/platform.py:48 +#: assets/models/platform.py:53 msgid "Push account method" msgstr "账号推送方式" -#: assets/models/platform.py:49 -msgid "Change password enabled" -msgstr "开启账号改密" - -#: assets/models/platform.py:51 -msgid "Change password method" -msgstr "更改密码方式" - -#: assets/models/platform.py:52 +#: assets/models/platform.py:55 msgid "Verify account enabled" msgstr "开启账号验证" -#: assets/models/platform.py:54 +#: assets/models/platform.py:57 msgid "Verify account method" msgstr "账号验证方式" -#: assets/models/platform.py:74 tickets/models/ticket/general.py:300 +#: assets/models/platform.py:77 tickets/models/ticket/general.py:300 msgid "Meta" msgstr "元数据" -#: assets/models/platform.py:75 +#: assets/models/platform.py:78 msgid "Internal" msgstr "内置" -#: assets/models/platform.py:79 assets/serializers/platform.py:84 +#: assets/models/platform.py:82 assets/serializers/platform.py:84 msgid "Charset" msgstr "编码" -#: assets/models/platform.py:81 +#: assets/models/platform.py:84 msgid "Domain enabled" msgstr "启用网域" -#: assets/models/platform.py:82 +#: assets/models/platform.py:85 msgid "Protocols enabled" msgstr "启用协议" -#: assets/models/platform.py:84 +#: assets/models/platform.py:87 msgid "Su enabled" msgstr "启用账号切换" -#: assets/models/platform.py:85 +#: assets/models/platform.py:88 msgid "Su method" msgstr "账号切换方式" -#: assets/models/platform.py:87 assets/serializers/platform.py:91 +#: assets/models/platform.py:90 assets/serializers/platform.py:91 msgid "Automation" msgstr "自动化" -#: assets/models/utils.py:19 +#: assets/models/utils.py:18 #, python-format msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" -#: assets/notifications.py:8 -msgid "Notification of account backup route task results" -msgstr "账号备份任务结果通知" - -#: assets/notifications.py:18 -msgid "" -"{} - The account backup passage task has been completed. See the attachment " -"for details" -msgstr "{} - 账号备份任务已完成, 详情见附件" - -#: assets/notifications.py:20 -msgid "" -"{} - The account backup passage task has been completed: the encryption " -"password has not been set - please go to personal information -> file " -"encryption password to set the encryption password" -msgstr "" -"{} - 账号备份任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设" -"置加密密码" - -#: assets/notifications.py:31 -msgid "Notification of implementation result of encryption change plan" -msgstr "改密计划任务结果通知" - -#: assets/notifications.py:41 -msgid "" -"{} - The encryption change task has been completed. See the attachment for " -"details" -msgstr "{} - 改密任务已完成, 详情见附件" - -#: assets/notifications.py:42 -msgid "" -"{} - The encryption change task has been completed: the encryption password " -"has not been set - please go to personal information -> file encryption " -"password to set the encryption password" -msgstr "" -"{} - 改密任务已完成: 未设置加密密码 - 请前往个人信息 -> 文件加密密码中设置加" -"密密码" - -#: assets/serializers/account/account.py:18 -msgid "Push now" -msgstr "立即推送" - -#: assets/serializers/account/account.py:20 -#: assets/serializers/account/base.py:13 -msgid "Has secret" -msgstr "已托管密码" - -#: assets/serializers/account/account.py:27 -msgid "Account template not found" -msgstr "账号模版未找到" - -#: assets/serializers/account/account.py:72 -msgid "Asset not found" -msgstr "资产不存在" - -#: assets/serializers/account/backup.py:29 -#: assets/serializers/automations/base.py:34 ops/mixin.py:22 ops/mixin.py:102 -#: settings/serializers/auth/ldap.py:66 -msgid "Periodic perform" -msgstr "定时执行" - -#: assets/serializers/account/backup.py:31 -#: assets/serializers/automations/change_secret.py:41 -msgid "Currently only mail sending is supported" -msgstr "当前只支持邮件发送" - -#: assets/serializers/asset/common.py:68 assets/serializers/platform.py:89 -#: authentication/serializers/connect_token_secret.py:27 +#: assets/serializers/asset/common.py:124 assets/serializers/platform.py:89 +#: authentication/serializers/connect_token_secret.py:28 #: authentication/serializers/connect_token_secret.py:65 -#: perms/serializers/user_permission.py:22 xpack/plugins/cloud/models.py:107 +#: perms/serializers/user_permission.py:24 xpack/plugins/cloud/models.py:107 #: xpack/plugins/cloud/serializers/task.py:38 msgid "Protocols" msgstr "协议组" -#: assets/serializers/asset/common.py:88 +#: assets/serializers/asset/common.py:126 +#, fuzzy +#| msgid "Enabled" +msgid "Enabled info" +msgstr "启用" + +#: assets/serializers/asset/common.py:144 msgid "Address" msgstr "地址" -#: assets/serializers/asset/common.py:89 +#: assets/serializers/asset/common.py:145 msgid "Node path" msgstr "节点路径" -#: assets/serializers/asset/common.py:157 +#: assets/serializers/asset/common.py:205 msgid "Platform not exist" msgstr "平台不存在" -#: assets/serializers/asset/common.py:173 +#: assets/serializers/asset/common.py:221 msgid "Protocol is required: {}" msgstr "协议是必填的: {}" @@ -1319,34 +1569,6 @@ msgstr "主机名原始" msgid "Asset number" msgstr "资产编号" -#: assets/serializers/automations/change_secret.py:28 -msgid "SSH Key strategy" -msgstr "SSH 密钥策略" - -#: assets/serializers/automations/change_secret.py:70 -msgid "* Please enter the correct password length" -msgstr "* 请输入正确的密码长度" - -#: assets/serializers/automations/change_secret.py:73 -msgid "* Password length range 6-30 bits" -msgstr "* 密码长度范围 6-30 位" - -#: assets/serializers/automations/change_secret.py:117 -#: assets/serializers/automations/change_secret.py:145 audits/const.py:74 -#: audits/models.py:40 common/const/choices.py:18 ops/const.py:50 -#: ops/serializers/celery.py:39 terminal/models/session/sharing.py:103 -#: tickets/views/approve.py:114 -msgid "Success" -msgstr "成功" - -#: assets/serializers/automations/gather_accounts.py:23 -msgid "Executed amount" -msgstr "执行次数" - -#: assets/serializers/base.py:21 -msgid "Key password" -msgstr "密钥密码" - #: assets/serializers/cagegory.py:9 msgid "Constraints" msgstr "" @@ -1355,9 +1577,9 @@ msgstr "" msgid "Types" msgstr "类型" -#: assets/serializers/gathered_user.py:24 settings/serializers/terminal.py:9 -msgid "Hostname" -msgstr "主机名" +#: assets/serializers/gateway.py:24 common/validators.py:32 +msgid "This field must be unique." +msgstr "字段必须唯一" #: assets/serializers/label.py:12 msgid "Assets amount" @@ -1387,51 +1609,29 @@ msgstr "SFTP根路径" msgid "Primary" msgstr "主要的" -#: assets/serializers/utils.py:13 -msgid "Password can not contains `{{` " -msgstr "密码不能包含 `{{` 字符" - -#: assets/serializers/utils.py:16 -msgid "Password can not contains `'` " -msgstr "密码不能包含 `'` 字符" - -#: assets/serializers/utils.py:18 -msgid "Password can not contains `\"` " -msgstr "密码不能包含 `\"` 字符" - -#: assets/serializers/utils.py:24 -msgid "private key invalid or passphrase error" -msgstr "密钥不合法或密钥密码错误" - #: assets/tasks/automation.py:11 -msgid "Execute automation" +#, fuzzy +#| msgid "Execute automation" +msgid "Asset execute automation" msgstr "执行自动化任务" -#: assets/tasks/backup.py:13 -msgid "Execute account backup plan" -msgstr "执行账号备份计划" - -#: assets/tasks/gather_accounts.py:31 -msgid "Gather assets accounts" -msgstr "收集资产上的账号" - -#: assets/tasks/gather_facts.py:26 +#: assets/tasks/gather_facts.py:23 msgid "Update some assets hardware info. " msgstr "更新资产硬件信息. " -#: assets/tasks/gather_facts.py:44 +#: assets/tasks/gather_facts.py:53 msgid "Manually update the hardware information of assets" msgstr "手动更新资产信息" -#: assets/tasks/gather_facts.py:49 +#: assets/tasks/gather_facts.py:57 msgid "Update assets hardware info: " msgstr "更新资产硬件信息" -#: assets/tasks/gather_facts.py:53 +#: assets/tasks/gather_facts.py:61 msgid "Manually update the hardware information of assets under a node" msgstr "手动更新节点下资产信息" -#: assets/tasks/gather_facts.py:59 +#: assets/tasks/gather_facts.py:65 msgid "Update node asset hardware information: " msgstr "更新节点资产硬件信息: " @@ -1448,26 +1648,24 @@ msgstr "自检程序已经在运行,不能重复启动" msgid "Periodic check the amount of assets under the node" msgstr "周期性检查节点下资产数量" -#: assets/tasks/ping.py:21 assets/tasks/ping.py:39 +#: assets/tasks/ping.py:37 assets/tasks/ping.py:54 msgid "Test assets connectivity " msgstr "测试资产可连接性" -#: assets/tasks/ping.py:33 -msgid "Manually test the connectivity of a asset" +#: assets/tasks/ping.py:50 +#, fuzzy +#| msgid "Manually test the connectivity of a asset" +msgid "Manually test the connectivity of a asset" msgstr "手动测试资产连接性" -#: assets/tasks/ping.py:43 +#: assets/tasks/ping.py:58 msgid "Manually test the connectivity of assets under a node" msgstr "手动测试节点下资产连接性" -#: assets/tasks/ping.py:49 +#: assets/tasks/ping.py:62 msgid "Test if the assets under the node are connectable " msgstr "测试节点下资产是否可连接" -#: assets/tasks/push_account.py:17 assets/tasks/push_account.py:34 -msgid "Push accounts to assets" -msgstr "推送账号到资产" - #: assets/tasks/utils.py:17 msgid "Asset has been disabled, skipped: {}" msgstr "资产已经被禁用, 跳过: {}" @@ -1484,14 +1682,6 @@ msgstr "为了安全,禁止推送用户 {}" msgid "No assets matched, stop task" msgstr "没有匹配到资产,结束任务" -#: assets/tasks/verify_account.py:30 -msgid "Verify asset account availability" -msgstr "" - -#: assets/tasks/verify_account.py:37 -msgid "Verify accounts connectivity" -msgstr "测试账号可连接性" - #: audits/apps.py:9 msgid "Audits" msgstr "日志审计" @@ -1500,78 +1690,92 @@ msgstr "日志审计" msgid "The text content is too long. Use Elasticsearch to store operation logs" msgstr "文字内容太长。请使用 Elasticsearch 存储操作日志" -#: audits/backends/db.py:24 audits/backends/db.py:26 +#: audits/backends/db.py:25 audits/backends/db.py:27 msgid "Tips" msgstr "提示" -#: audits/const.py:45 +#: audits/const.py:12 msgid "Mkdir" msgstr "创建目录" -#: audits/const.py:46 +#: audits/const.py:13 msgid "Rmdir" msgstr "删除目录" -#: audits/const.py:47 audits/const.py:57 +#: audits/const.py:14 audits/const.py:24 #: authentication/templates/authentication/_access_key_modal.html:65 -#: rbac/tree.py:232 +#: rbac/tree.py:231 msgid "Delete" msgstr "删除" -#: audits/const.py:48 perms/const.py:13 +#: audits/const.py:15 perms/const.py:13 msgid "Upload" msgstr "上传文件" -#: audits/const.py:49 +#: audits/const.py:16 msgid "Rename" msgstr "重命名" -#: audits/const.py:50 +#: audits/const.py:17 msgid "Symlink" msgstr "建立软链接" -#: audits/const.py:51 perms/const.py:14 +#: audits/const.py:18 perms/const.py:14 msgid "Download" msgstr "下载文件" -#: audits/const.py:55 rbac/tree.py:230 +#: audits/const.py:22 rbac/tree.py:229 msgid "View" msgstr "查看" -#: audits/const.py:56 rbac/tree.py:231 templates/_csv_import_export.html:18 +#: audits/const.py:23 rbac/tree.py:230 templates/_csv_import_export.html:18 #: templates/_csv_update_modal.html:6 msgid "Update" msgstr "更新" -#: audits/const.py:58 +#: audits/const.py:25 #: authentication/templates/authentication/_access_key_modal.html:22 -#: rbac/tree.py:229 +#: rbac/tree.py:228 msgid "Create" msgstr "创建" -#: audits/const.py:63 settings/serializers/terminal.py:6 +#: audits/const.py:27 perms/const.py:12 +msgid "Connect" +msgstr "连接" + +#: audits/const.py:28 authentication/templates/authentication/login.html:254 +#: authentication/templates/authentication/login.html:327 +#: templates/_header_bar.html:89 +msgid "Login" +msgstr "登录" + +#: audits/const.py:29 ops/const.py:9 +msgid "Change password" +msgstr "改密" + +#: audits/const.py:34 settings/serializers/terminal.py:6 #: terminal/models/applet/host.py:24 terminal/models/component/terminal.py:156 msgid "Terminal" msgstr "终端" -#: audits/const.py:70 +#: audits/const.py:41 msgid "-" msgstr "-" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "Yes" msgstr "是" -#: audits/handler.py:134 +#: audits/handler.py:136 msgid "No" msgstr "否" -#: audits/models.py:32 audits/models.py:55 audits/models.py:96 +#: audits/models.py:32 audits/models.py:59 audits/models.py:102 #: terminal/models/session/session.py:37 terminal/models/session/sharing.py:95 msgid "Remote addr" msgstr "远端地址" -#: audits/models.py:37 audits/serializers.py:30 +#: audits/models.py:37 audits/serializers.py:32 msgid "Operate" msgstr "操作" @@ -1583,104 +1787,116 @@ msgstr "文件名" msgid "File transfer log" msgstr "文件管理" -#: audits/models.py:53 audits/serializers.py:84 +#: audits/models.py:53 audits/serializers.py:86 msgid "Resource Type" msgstr "资源类型" -#: audits/models.py:54 +#: audits/models.py:54 audits/models.py:57 msgid "Resource" msgstr "资源" -#: audits/models.py:56 audits/models.py:98 -#: terminal/backends/command/serializers.py:40 +#: audits/models.py:60 audits/models.py:104 +#: terminal/backends/command/serializers.py:41 msgid "Datetime" msgstr "日期" -#: audits/models.py:88 +#: audits/models.py:63 +#, fuzzy +#| msgid "Is active" +msgid "Is Activity" +msgstr "激活" + +#: audits/models.py:93 msgid "Operate log" msgstr "操作日志" -#: audits/models.py:94 +#: audits/models.py:100 msgid "Change by" msgstr "修改者" -#: audits/models.py:104 +#: audits/models.py:110 msgid "Password change log" msgstr "改密日志" -#: audits/models.py:111 +#: audits/models.py:117 msgid "Login type" msgstr "登录方式" -#: audits/models.py:113 tickets/models/ticket/login_confirm.py:10 +#: audits/models.py:119 tickets/models/ticket/login_confirm.py:10 msgid "Login ip" msgstr "登录IP" -#: audits/models.py:115 +#: audits/models.py:121 #: authentication/templates/authentication/_msg_different_city.html:11 #: tickets/models/ticket/login_confirm.py:11 msgid "Login city" msgstr "登录城市" -#: audits/models.py:118 audits/serializers.py:60 +#: audits/models.py:124 audits/serializers.py:62 msgid "User agent" msgstr "用户代理" -#: audits/models.py:121 audits/serializers.py:44 +#: audits/models.py:127 audits/serializers.py:46 #: authentication/templates/authentication/_mfa_confirm_modal.html:14 #: users/forms/profile.py:65 users/models/user.py:698 #: users/serializers/profile.py:126 msgid "MFA" msgstr "MFA" -#: audits/models.py:131 +#: audits/models.py:137 msgid "Date login" msgstr "登录日期" -#: audits/models.py:133 audits/serializers.py:62 +#: audits/models.py:139 audits/serializers.py:64 msgid "Authentication backend" msgstr "认证方式" -#: audits/models.py:174 +#: audits/models.py:180 msgid "User login log" msgstr "用户登录日志" -#: audits/serializers.py:61 +#: audits/serializers.py:63 msgid "Reason display" msgstr "原因描述" -#: audits/signal_handlers.py:48 +#: audits/serializers.py:112 +#, fuzzy +#| msgid "User {} {} it." +msgid "User {} {} this resource." +msgstr "用户 {} {}了它." + +#: audits/signal_handlers.py:50 msgid "SSH Key" msgstr "SSH 密钥" -#: audits/signal_handlers.py:50 settings/serializers/auth/sso.py:10 +#: audits/signal_handlers.py:52 settings/serializers/auth/sso.py:10 msgid "SSO" msgstr "SSO" -#: audits/signal_handlers.py:51 +#: audits/signal_handlers.py:53 msgid "Auth Token" msgstr "认证令牌" -#: audits/signal_handlers.py:52 authentication/notifications.py:73 -#: authentication/views/login.py:73 authentication/views/wecom.py:178 +#: audits/signal_handlers.py:54 authentication/notifications.py:73 +#: authentication/views/login.py:73 authentication/views/wecom.py:177 #: notifications/backends/__init__.py:11 settings/serializers/auth/wecom.py:10 #: users/models/user.py:736 msgid "WeCom" msgstr "企业微信" -#: audits/signal_handlers.py:53 authentication/views/feishu.py:145 +#: audits/signal_handlers.py:55 authentication/views/feishu.py:144 #: authentication/views/login.py:85 notifications/backends/__init__.py:14 #: settings/serializers/auth/feishu.py:10 users/models/user.py:738 msgid "FeiShu" msgstr "飞书" -#: audits/signal_handlers.py:54 authentication/views/dingtalk.py:180 +#: audits/signal_handlers.py:56 authentication/views/dingtalk.py:179 #: authentication/views/login.py:79 notifications/backends/__init__.py:12 #: settings/serializers/auth/dingtalk.py:10 users/models/user.py:737 msgid "DingTalk" msgstr "钉钉" -#: audits/signal_handlers.py:55 authentication/models/temp_token.py:16 +#: audits/signal_handlers.py:57 authentication/models/temp_token.py:16 msgid "Temporary token" msgstr "临时密码" @@ -1688,6 +1904,26 @@ msgstr "临时密码" msgid "This action require verify your MFA" msgstr "此操作需要验证您的 MFA" +#: authentication/api/connection_token.py:264 +#, fuzzy +#| msgid "Account template not found" +msgid "Account not found" +msgstr "账号模版未找到" + +#: authentication/api/connection_token.py:267 +#, fuzzy +#| msgid "Permission name" +msgid "Permission Expired" +msgstr "授权规则名称" + +#: authentication/api/connection_token.py:279 +msgid "ACL action is reject" +msgstr "" + +#: authentication/api/connection_token.py:283 +msgid "ACL action is review" +msgstr "" + #: authentication/api/mfa.py:59 msgid "Current user not support mfa type: {}" msgstr "当前用户不支持 MFA 类型: {}" @@ -1719,7 +1955,7 @@ msgstr "忘记密码" #: authentication/apps.py:7 settings/serializers/auth/base.py:10 #: settings/serializers/auth/cas.py:10 settings/serializers/auth/dingtalk.py:10 #: settings/serializers/auth/feishu.py:10 settings/serializers/auth/ldap.py:39 -#: settings/serializers/auth/oauth2.py:19 settings/serializers/auth/oidc.py:12 +#: settings/serializers/auth/oauth2.py:18 settings/serializers/auth/oidc.py:12 #: settings/serializers/auth/radius.py:13 settings/serializers/auth/saml2.py:11 #: settings/serializers/auth/sso.py:10 settings/serializers/auth/wecom.py:10 msgid "Authentication" @@ -1911,21 +2147,21 @@ msgstr "手机号没有设置" msgid "SSO auth closed" msgstr "SSO 认证关闭了" -#: authentication/errors/mfa.py:18 authentication/views/wecom.py:80 +#: authentication/errors/mfa.py:18 authentication/views/wecom.py:79 msgid "WeCom is already bound" msgstr "企业微信已经绑定" -#: authentication/errors/mfa.py:23 authentication/views/wecom.py:237 -#: authentication/views/wecom.py:291 +#: authentication/errors/mfa.py:23 authentication/views/wecom.py:236 +#: authentication/views/wecom.py:290 msgid "WeCom is not bound" msgstr "没有绑定企业微信" -#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:243 -#: authentication/views/dingtalk.py:297 +#: authentication/errors/mfa.py:28 authentication/views/dingtalk.py:242 +#: authentication/views/dingtalk.py:296 msgid "DingTalk is not bound" msgstr "钉钉没有绑定" -#: authentication/errors/mfa.py:33 authentication/views/feishu.py:204 +#: authentication/errors/mfa.py:33 authentication/views/feishu.py:203 msgid "FeiShu is not bound" msgstr "没有绑定飞书" @@ -2078,33 +2314,44 @@ msgid "Asset display" msgstr "资产名称" #: authentication/models/connection_token.py:41 -#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:73 +#: authentication/models/temp_token.py:13 perms/models/asset_permission.py:74 #: tickets/models/ticket/apply_application.py:31 #: tickets/models/ticket/apply_asset.py:20 users/models/user.py:719 msgid "Date expired" msgstr "失效日期" #: authentication/models/connection_token.py:45 +#: perms/models/asset_permission.py:77 +msgid "From ticket" +msgstr "来自工单" + +#: authentication/models/connection_token.py:51 msgid "Connection token" msgstr "连接令牌" -#: authentication/models/connection_token.py:47 +#: authentication/models/connection_token.py:53 msgid "Can view connection token secret" msgstr "可以查看连接令牌密文" -#: authentication/models/connection_token.py:94 +#: authentication/models/connection_token.py:100 +#, fuzzy +#| msgid "Connection token" +msgid "Connection token inactive" +msgstr "连接令牌" + +#: authentication/models/connection_token.py:103 msgid "Connection token expired at: {}" msgstr "连接令牌过期: {}" -#: authentication/models/connection_token.py:97 +#: authentication/models/connection_token.py:106 msgid "No user or invalid user" msgstr "没有用户或用户失效" -#: authentication/models/connection_token.py:101 +#: authentication/models/connection_token.py:110 msgid "No asset or inactive asset" msgstr "没有资产或资产未激活" -#: authentication/models/connection_token.py:248 +#: authentication/models/connection_token.py:257 msgid "Super connection token" msgstr "超级连接令牌" @@ -2148,9 +2395,9 @@ msgstr "组件" msgid "Expired now" msgstr "立刻过期" -#: authentication/serializers/connect_token_secret.py:146 +#: authentication/serializers/connect_token_secret.py:147 #: authentication/templates/authentication/_access_key_modal.html:30 -#: perms/models/perm_node.py:20 users/serializers/group.py:35 +#: perms/models/perm_node.py:21 users/serializers/group.py:35 msgid "ID" msgstr "ID" @@ -2158,6 +2405,12 @@ msgstr "ID" msgid "Expired time" msgstr "过期时间" +#: authentication/serializers/connection_token.py:18 +#, fuzzy +#| msgid "Ticket flow" +msgid "Ticket info" +msgstr "工单流程" + #: authentication/serializers/password_mfa.py:16 #: authentication/serializers/password_mfa.py:24 #: notifications/backends/__init__.py:10 settings/serializers/email.py:19 @@ -2174,7 +2427,7 @@ msgid "The {} cannot be empty" msgstr "{} 不能为空" #: authentication/serializers/token.py:79 perms/serializers/permission.py:30 -#: perms/serializers/permission.py:61 users/serializers/user.py:159 +#: perms/serializers/permission.py:62 users/serializers/user.py:159 msgid "Is valid" msgstr "账号是否有效" @@ -2355,12 +2608,6 @@ msgstr "如果这次公钥更新不是由你发起的,那么你的账号可能 msgid "Cancel" msgstr "取消" -#: authentication/templates/authentication/login.html:254 -#: authentication/templates/authentication/login.html:327 -#: templates/_header_bar.html:89 -msgid "Login" -msgstr "登录" - #: authentication/templates/authentication/login.html:334 msgid "More login options" msgstr "其他方式登录" @@ -2402,73 +2649,73 @@ msgstr "复制成功" msgid "LAN" msgstr "局域网" -#: authentication/views/dingtalk.py:42 +#: authentication/views/dingtalk.py:41 msgid "DingTalk Error, Please contact your system administrator" msgstr "钉钉错误,请联系系统管理员" -#: authentication/views/dingtalk.py:45 +#: authentication/views/dingtalk.py:44 msgid "DingTalk Error" msgstr "钉钉错误" -#: authentication/views/dingtalk.py:57 authentication/views/feishu.py:52 -#: authentication/views/wecom.py:56 +#: authentication/views/dingtalk.py:56 authentication/views/feishu.py:51 +#: authentication/views/wecom.py:55 msgid "" "The system configuration is incorrect. Please contact your administrator" msgstr "企业配置错误,请联系系统管理员" -#: authentication/views/dingtalk.py:81 +#: authentication/views/dingtalk.py:80 msgid "DingTalk is already bound" msgstr "钉钉已经绑定" -#: authentication/views/dingtalk.py:149 authentication/views/wecom.py:148 +#: authentication/views/dingtalk.py:148 authentication/views/wecom.py:147 msgid "Invalid user_id" msgstr "无效的 user_id" -#: authentication/views/dingtalk.py:165 +#: authentication/views/dingtalk.py:164 msgid "DingTalk query user failed" msgstr "钉钉查询用户失败" -#: authentication/views/dingtalk.py:174 +#: authentication/views/dingtalk.py:173 msgid "The DingTalk is already bound to another user" msgstr "该钉钉已经绑定其他用户" -#: authentication/views/dingtalk.py:181 +#: authentication/views/dingtalk.py:180 msgid "Binding DingTalk successfully" msgstr "绑定 钉钉 成功" -#: authentication/views/dingtalk.py:237 authentication/views/dingtalk.py:291 +#: authentication/views/dingtalk.py:236 authentication/views/dingtalk.py:290 msgid "Failed to get user from DingTalk" msgstr "从钉钉获取用户失败" -#: authentication/views/dingtalk.py:244 authentication/views/dingtalk.py:298 +#: authentication/views/dingtalk.py:243 authentication/views/dingtalk.py:297 msgid "Please login with a password and then bind the DingTalk" msgstr "请使用密码登录,然后绑定钉钉" -#: authentication/views/feishu.py:40 +#: authentication/views/feishu.py:39 msgid "FeiShu Error" msgstr "飞书错误" -#: authentication/views/feishu.py:88 +#: authentication/views/feishu.py:87 msgid "FeiShu is already bound" msgstr "飞书已经绑定" -#: authentication/views/feishu.py:130 +#: authentication/views/feishu.py:129 msgid "FeiShu query user failed" msgstr "飞书查询用户失败" -#: authentication/views/feishu.py:139 +#: authentication/views/feishu.py:138 msgid "The FeiShu is already bound to another user" msgstr "该飞书已经绑定其他用户" -#: authentication/views/feishu.py:146 +#: authentication/views/feishu.py:145 msgid "Binding FeiShu successfully" msgstr "绑定 飞书 成功" -#: authentication/views/feishu.py:198 +#: authentication/views/feishu.py:197 msgid "Failed to get user from FeiShu" msgstr "从飞书获取用户失败" -#: authentication/views/feishu.py:205 +#: authentication/views/feishu.py:204 msgid "Please login with a password and then bind the FeiShu" msgstr "请使用密码登录,然后绑定飞书" @@ -2504,34 +2751,38 @@ msgstr "退出登录成功" msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: authentication/views/wecom.py:41 +#: authentication/views/wecom.py:40 msgid "WeCom Error, Please contact your system administrator" msgstr "企业微信错误,请联系系统管理员" -#: authentication/views/wecom.py:44 +#: authentication/views/wecom.py:43 msgid "WeCom Error" msgstr "企业微信错误" -#: authentication/views/wecom.py:163 +#: authentication/views/wecom.py:162 msgid "WeCom query user failed" msgstr "企业微信查询用户失败" -#: authentication/views/wecom.py:172 +#: authentication/views/wecom.py:171 msgid "The WeCom is already bound to another user" msgstr "该企业微信已经绑定其他用户" -#: authentication/views/wecom.py:179 +#: authentication/views/wecom.py:178 msgid "Binding WeCom successfully" msgstr "绑定 企业微信 成功" -#: authentication/views/wecom.py:231 authentication/views/wecom.py:285 +#: authentication/views/wecom.py:230 authentication/views/wecom.py:284 msgid "Failed to get user from WeCom" msgstr "从企业微信获取用户失败" -#: authentication/views/wecom.py:238 authentication/views/wecom.py:292 +#: authentication/views/wecom.py:237 authentication/views/wecom.py:291 msgid "Please login with a password and then bind the WeCom" msgstr "请使用密码登录,然后绑定企业微信" +#: common/api/action.py:52 +msgid "Request file format may be wrong" +msgstr "上传的文件格式错误 或 其它类型资源的文件" + #: common/const/__init__.py:6 #, python-format msgid "%(name)s was created successfully" @@ -2554,11 +2805,12 @@ msgstr "定时触发" msgid "Ready" msgstr "准备" -#: common/const/choices.py:16 tickets/const.py:29 tickets/const.py:39 +#: common/const/choices.py:16 terminal/const.py:58 tickets/const.py:29 +#: tickets/const.py:39 msgid "Pending" msgstr "待定的" -#: common/const/choices.py:17 ops/const.py:49 +#: common/const/choices.py:17 ops/const.py:50 msgid "Running" msgstr "运行中" @@ -2570,66 +2822,57 @@ msgstr "取消" msgid "ugettext_lazy" msgstr "ugettext_lazy" -#: common/db/fields.py:94 +#: common/db/fields.py:97 msgid "Marshal dict data to char field" msgstr "编码 dict 为 char" -#: common/db/fields.py:98 +#: common/db/fields.py:101 msgid "Marshal dict data to text field" msgstr "编码 dict 为 text" -#: common/db/fields.py:110 +#: common/db/fields.py:113 msgid "Marshal list data to char field" msgstr "编码 list 为 char" -#: common/db/fields.py:114 +#: common/db/fields.py:117 msgid "Marshal list data to text field" msgstr "编码 list 为 text" -#: common/db/fields.py:118 +#: common/db/fields.py:121 msgid "Marshal data to char field" msgstr "编码数据为 char" -#: common/db/fields.py:122 +#: common/db/fields.py:125 msgid "Marshal data to text field" msgstr "编码数据为 text" -#: common/db/fields.py:164 +#: common/db/fields.py:167 msgid "Encrypt field using Secret Key" msgstr "加密的字段" -#: common/db/models.py:75 +#: common/db/mixins.py:32 +msgid "is discard" +msgstr "忽略的" + +#: common/db/mixins.py:33 +msgid "discard time" +msgstr "忽略时间" + +#: common/db/models.py:34 msgid "Updated by" msgstr "更新人" -#: common/drf/exc_handlers.py:25 +#: common/db/validators.py:9 +#, fuzzy +#| msgid "Invalid data type, should be list" +msgid "Invalid port range, should be like and within {}-{}" +msgstr "错误的数据类型,应该是列表" + +#: common/drf/exc_handlers.py:26 msgid "Object" msgstr "对象" -#: common/drf/fields.py:77 tickets/serializers/ticket/common.py:58 -#: xpack/plugins/cloud/serializers/account_attrs.py:56 -msgid "This field is required." -msgstr "该字段是必填项。" - -#: common/drf/fields.py:78 -#, python-brace-format -msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "错误的 id \"{pk_value}\" - 对象不存在" - -#: common/drf/fields.py:79 -#, python-brace-format -msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "错误类型。期望 pk 值,收到 {data_type}。" - -#: common/drf/fields.py:141 -msgid "Invalid data type, should be list" -msgstr "错误的数据类型,应该是列表" - -#: common/drf/fields.py:156 -msgid "Invalid choice: {}" -msgstr "无效选项: {}" - -#: common/drf/metadata.py:130 +#: common/drf/metadata.py:127 msgid "Organization ID" msgstr "组织 ID" @@ -2641,14 +2884,6 @@ msgstr "文件内容太大 (最大长度 `{}` 字节)" msgid "Parse file error: {}" msgstr "解析文件错误: {}" -#: common/drf/serializers/common.py:86 -msgid "Children" -msgstr "节点" - -#: common/drf/serializers/common.py:94 -msgid "File" -msgstr "文件" - #: common/exceptions.py:15 #, python-format msgid "%s object does not exist." @@ -2678,31 +2913,6 @@ msgstr "此操作需要确认当前用户" msgid "Unexpect error occur" msgstr "发生意外错误" -#: common/mixins/api/action.py:52 -msgid "Request file format may be wrong" -msgstr "上传的文件格式错误 或 其它类型资源的文件" - -#: common/mixins/models.py:32 -msgid "is discard" -msgstr "忽略的" - -#: common/mixins/models.py:33 -msgid "discard time" -msgstr "忽略时间" - -#: common/mixins/views.py:58 -msgid "Export all" -msgstr "导出所有" - -#: common/mixins/views.py:60 -msgid "Export only selected items" -msgstr "仅导出选择项" - -#: common/mixins/views.py:65 -#, python-format -msgid "Export filtered: %s" -msgstr "导出搜素: %s" - #: common/plugins/es.py:28 msgid "Invalid elasticsearch config" msgstr "无效的 Elasticsearch 配置" @@ -2767,6 +2977,37 @@ msgstr "验证码错误" msgid "Please wait {} seconds before sending" msgstr "请在 {} 秒后发送" +#: common/serializers/common.py:86 +msgid "Children" +msgstr "节点" + +#: common/serializers/common.py:94 +msgid "File" +msgstr "文件" + +#: common/serializers/fields.py:100 tickets/serializers/ticket/common.py:58 +#: xpack/plugins/cloud/serializers/account_attrs.py:56 +msgid "This field is required." +msgstr "该字段是必填项。" + +#: common/serializers/fields.py:101 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "错误的 id \"{pk_value}\" - 对象不存在" + +#: common/serializers/fields.py:102 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "错误类型。期望 pk 值,收到 {data_type}。" + +#: common/serializers/fields.py:172 +msgid "Invalid data type, should be list" +msgstr "错误的数据类型,应该是列表" + +#: common/serializers/fields.py:187 +msgid "Invalid choice: {}" +msgstr "无效选项: {}" + #: common/tasks.py:13 msgid "Send email" msgstr "发件邮件" @@ -2787,10 +3028,6 @@ msgstr "无效地址" msgid "Special char not allowed" msgstr "不能包含特殊字符" -#: common/validators.py:32 -msgid "This field must be unique." -msgstr "字段必须唯一" - #: common/validators.py:40 msgid "Should not contains special characters" msgstr "不能包含特殊字符" @@ -2799,6 +3036,19 @@ msgstr "不能包含特殊字符" msgid "The mobile phone number format is incorrect" msgstr "手机号格式不正确" +#: common/views/mixins.py:57 +msgid "Export all" +msgstr "导出所有" + +#: common/views/mixins.py:59 +msgid "Export only selected items" +msgstr "仅导出选择项" + +#: common/views/mixins.py:64 +#, python-format +msgid "Export filtered: %s" +msgstr "导出搜素: %s" + #: jumpserver/conf.py:415 msgid "Create account successfully" msgstr "创建账号成功" @@ -2882,7 +3132,7 @@ msgstr "跳过以下主机: " msgid "Waiting task start" msgstr "等待任务开始" -#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:55 +#: ops/apps.py:9 ops/notifications.py:16 rbac/tree.py:56 msgid "App ops" msgstr "作业中心" @@ -2898,19 +3148,23 @@ msgstr "校验" msgid "Collect" msgstr "收集" -#: ops/const.py:9 -msgid "Change password" -msgstr "改密" - #: ops/const.py:19 msgid "Custom password" msgstr "自定义密码" +#: ops/const.py:20 +msgid "All assets use the same random password" +msgstr "随机一个" + +#: ops/const.py:21 +msgid "All assets use different random password" +msgstr "各自随机" + #: ops/const.py:33 msgid "Adhoc" msgstr "命令" -#: ops/const.py:34 ops/models/job.py:31 +#: ops/const.py:34 ops/models/job.py:32 msgid "Playbook" msgstr "Playbook" @@ -2926,10 +3180,20 @@ msgstr "特权账号优先" msgid "Skip" msgstr "跳过" -#: ops/const.py:45 ops/models/adhoc.py:20 +#: ops/const.py:45 msgid "Powershell" msgstr "PowerShell" +#: ops/const.py:46 +msgid "Python" +msgstr "" + +#: ops/const.py:52 +#, fuzzy +#| msgid "Test timeout" +msgid "Timeout" +msgstr "测试超时时间" + #: ops/exception.py:6 msgid "no valid program entry found." msgstr "没有可用程序入口" @@ -2959,26 +3223,26 @@ msgstr "输入在 {} - {} 范围之间" msgid "Require periodic or regularly perform setting" msgstr "需要周期或定期设置" -#: ops/models/adhoc.py:24 +#: ops/models/adhoc.py:23 msgid "Pattern" msgstr "模式" -#: ops/models/adhoc.py:26 ops/models/job.py:28 +#: ops/models/adhoc.py:25 ops/models/job.py:29 msgid "Module" msgstr "模块" -#: ops/models/adhoc.py:27 ops/models/celery.py:55 ops/models/job.py:26 +#: ops/models/adhoc.py:26 ops/models/celery.py:58 ops/models/job.py:27 #: terminal/models/component/task.py:16 msgid "Args" msgstr "参数" -#: ops/models/adhoc.py:28 ops/models/base.py:16 ops/models/base.py:53 -#: ops/models/job.py:33 ops/models/job.py:104 ops/models/playbook.py:16 +#: ops/models/adhoc.py:27 ops/models/base.py:16 ops/models/base.py:53 +#: ops/models/job.py:34 ops/models/job.py:105 ops/models/playbook.py:16 #: terminal/models/session/sharing.py:23 msgid "Creator" msgstr "创建者" -#: ops/models/adhoc.py:46 +#: ops/models/adhoc.py:45 msgid "AdHoc" msgstr "任务各版本" @@ -2990,83 +3254,92 @@ msgstr "账号策略" msgid "Last execution" msgstr "最后执行" -#: ops/models/base.py:22 +#: ops/models/base.py:22 ops/serializers/job.py:16 msgid "Date last run" msgstr "最后运行日期" -#: ops/models/base.py:51 ops/models/job.py:102 +#: ops/models/base.py:51 ops/models/job.py:103 #: xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "结果" -#: ops/models/base.py:52 ops/models/job.py:103 +#: ops/models/base.py:52 ops/models/job.py:104 msgid "Summary" msgstr "汇总" +#: ops/models/celery.py:16 +msgid "Date last publish" +msgstr "发布日期" + #: ops/models/celery.py:47 msgid "Celery Task" msgstr "Celery 任务" -#: ops/models/celery.py:56 terminal/models/component/task.py:17 +#: ops/models/celery.py:50 +msgid "Can view task monitor" +msgstr "可以查看任务监控" + +#: ops/models/celery.py:59 terminal/models/component/task.py:17 msgid "Kwargs" msgstr "其它参数" -#: ops/models/celery.py:57 tickets/models/comment.py:13 -#: tickets/models/ticket/general.py:44 tickets/models/ticket/general.py:279 +#: ops/models/celery.py:60 tickets/models/comment.py:13 +#: tickets/models/ticket/general.py:45 tickets/models/ticket/general.py:279 +#: tickets/serializers/super_ticket.py:14 #: tickets/serializers/ticket/ticket.py:21 msgid "State" msgstr "状态" -#: ops/models/celery.py:58 terminal/models/session/sharing.py:110 +#: ops/models/celery.py:61 terminal/models/session/sharing.py:110 #: tickets/const.py:25 msgid "Finished" msgstr "结束" -#: ops/models/celery.py:59 +#: ops/models/celery.py:62 msgid "Date published" msgstr "发布日期" -#: ops/models/celery.py:83 +#: ops/models/celery.py:86 msgid "Celery Task Execution" msgstr "Celery 任务执行" -#: ops/models/job.py:29 +#: ops/models/job.py:30 msgid "Chdir" msgstr "运行目录" -#: ops/models/job.py:30 +#: ops/models/job.py:31 msgid "Timeout (Seconds)" -msgstr "超市时间(秒)" +msgstr "超时时间(秒)" -#: ops/models/job.py:35 +#: ops/models/job.py:36 msgid "Runas" msgstr "运行用户" -#: ops/models/job.py:37 +#: ops/models/job.py:38 msgid "Runas policy" msgstr "用户策略" -#: ops/models/job.py:38 +#: ops/models/job.py:39 msgid "Use Parameter Define" msgstr "" -#: ops/models/job.py:39 +#: ops/models/job.py:40 msgid "Parameters define" msgstr "" -#: ops/models/job.py:91 +#: ops/models/job.py:92 msgid "Job" msgstr "作业" -#: ops/models/job.py:101 +#: ops/models/job.py:102 msgid "Parameters" msgstr "" -#: ops/models/job.py:300 +#: ops/models/job.py:311 msgid "Job Execution" msgstr "作业执行" -#: ops/models/job.py:311 +#: ops/models/job.py:322 msgid "Job audit log" msgstr "作业审计日志" @@ -3102,13 +3375,17 @@ msgstr "CPU 使用率超过 {max_threshold}: => {value}" msgid "Run after save" msgstr "保存后执行" -#: ops/serializers/job.py:43 +#: ops/serializers/job.py:52 msgid "Job type" msgstr "任务类型" -#: ops/serializers/job.py:44 -msgid "Material" -msgstr "" +#: ops/serializers/job.py:55 terminal/serializers/session.py:53 +msgid "Is finished" +msgstr "是否完成" + +#: ops/serializers/job.py:56 +msgid "Time cost" +msgstr "花费时间" #: ops/signal_handlers.py:74 terminal/models/applet/host.py:111 #: terminal/models/component/task.py:24 @@ -3147,10 +3424,6 @@ msgstr "周期检测服务性能" msgid "Task log" msgstr "任务列表" -#: ops/utils.py:64 -msgid "Update task content: {}" -msgstr "更新任务内容: {}" - #: ops/variables.py:24 msgid "The current user`s username of JumpServer" msgstr "" @@ -3187,17 +3460,17 @@ msgstr "" msgid "Name of the job" msgstr "" -#: orgs/api.py:67 +#: orgs/api.py:63 msgid "The current organization ({}) cannot be deleted" msgstr "当前组织 ({}) 不能被删除" -#: orgs/api.py:72 +#: orgs/api.py:68 msgid "" "LDAP synchronization is set to the current organization. Please switch to " "another organization before deleting" msgstr "LDAP 同步设置组织为当前组织,请切换其他组织后再进行删除操作" -#: orgs/api.py:81 +#: orgs/api.py:78 msgid "The organization have resource ({}) cannot be deleted" msgstr "组织存在资源 ({}) 不能被删除" @@ -3205,10 +3478,10 @@ msgstr "组织存在资源 ({}) 不能被删除" msgid "App organizations" msgstr "组织管理" -#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:82 +#: orgs/mixins/models.py:57 orgs/mixins/serializers.py:25 orgs/models.py:84 #: rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:63 -#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:62 +#: tickets/models/ticket/general.py:302 tickets/serializers/ticket/ticket.py:60 msgid "Organization" msgstr "组织" @@ -3216,27 +3489,27 @@ msgstr "组织" msgid "Org name" msgstr "组织名称" -#: orgs/models.py:68 rbac/models/role.py:36 terminal/models/applet/applet.py:28 +#: orgs/models.py:70 rbac/models/role.py:36 terminal/models/applet/applet.py:29 msgid "Builtin" msgstr "内置的" -#: orgs/models.py:74 +#: orgs/models.py:76 msgid "GLOBAL" msgstr "全局组织" -#: orgs/models.py:76 +#: orgs/models.py:78 msgid "DEFAULT" msgstr "默认组织" -#: orgs/models.py:78 +#: orgs/models.py:80 msgid "SYSTEM" msgstr "系统组织" -#: orgs/models.py:84 +#: orgs/models.py:86 msgid "Can view root org" msgstr "可以查看全局组织" -#: orgs/models.py:85 +#: orgs/models.py:87 msgid "Can view all joined org" msgstr "可以查看所有加入的组织" @@ -3248,10 +3521,6 @@ msgstr "刷新组织缓存" msgid "App permissions" msgstr "授权管理" -#: perms/const.py:12 -msgid "Connect" -msgstr "连接" - #: perms/const.py:15 msgid "Copy" msgstr "复制" @@ -3268,46 +3537,42 @@ msgstr "文件传输" msgid "Clipboard" msgstr "剪贴板" -#: perms/models/asset_permission.py:70 perms/serializers/permission.py:29 -#: perms/serializers/permission.py:59 +#: perms/models/asset_permission.py:71 perms/serializers/permission.py:29 +#: perms/serializers/permission.py:60 #: tickets/models/ticket/apply_application.py:28 #: tickets/models/ticket/apply_asset.py:18 msgid "Actions" msgstr "动作" -#: perms/models/asset_permission.py:76 -msgid "From ticket" -msgstr "来自工单" - -#: perms/models/asset_permission.py:82 +#: perms/models/asset_permission.py:83 msgid "Asset permission" msgstr "资产授权" -#: perms/models/perm_node.py:67 +#: perms/models/perm_node.py:68 msgid "Ungrouped" msgstr "未分组" -#: perms/models/perm_node.py:69 +#: perms/models/perm_node.py:70 msgid "Favorite" msgstr "收藏夹" -#: perms/models/perm_node.py:120 +#: perms/models/perm_node.py:121 msgid "Permed asset" msgstr "授权的资产" -#: perms/models/perm_node.py:122 +#: perms/models/perm_node.py:123 msgid "Can view my assets" msgstr "可以查看我的资产" -#: perms/models/perm_node.py:123 +#: perms/models/perm_node.py:124 msgid "Can view user assets" msgstr "可以查看用户授权的资产" -#: perms/models/perm_node.py:124 +#: perms/models/perm_node.py:125 msgid "Can view usergroup assets" msgstr "可以查看用户组授权的资产" -#: perms/models/perm_node.py:135 +#: perms/models/perm_node.py:136 msgid "Permed account" msgstr "授权账号" @@ -3331,7 +3596,7 @@ msgstr "资产授权规则将要过期" msgid "asset permissions of organization {}" msgstr "组织 ({}) 的资产授权" -#: perms/serializers/permission.py:31 perms/serializers/permission.py:60 +#: perms/serializers/permission.py:31 perms/serializers/permission.py:61 #: users/serializers/user.py:91 users/serializers/user.py:161 msgid "Is expired" msgstr "已过期" @@ -3372,27 +3637,27 @@ msgstr "{} 至少有一个系统角色" msgid "RBAC" msgstr "RBAC" -#: rbac/builtin.py:111 +#: rbac/builtin.py:109 msgid "SystemAdmin" msgstr "系统管理员" -#: rbac/builtin.py:114 +#: rbac/builtin.py:112 msgid "SystemAuditor" msgstr "系统审计员" -#: rbac/builtin.py:117 +#: rbac/builtin.py:115 msgid "SystemComponent" msgstr "系统组件" -#: rbac/builtin.py:123 +#: rbac/builtin.py:121 msgid "OrgAdmin" msgstr "组织管理员" -#: rbac/builtin.py:126 +#: rbac/builtin.py:124 msgid "OrgAuditor" msgstr "组织审计员" -#: rbac/builtin.py:129 +#: rbac/builtin.py:127 msgid "OrgUser" msgstr "组织用户" @@ -3425,7 +3690,7 @@ msgid "Permissions" msgstr "授权" #: rbac/models/role.py:31 rbac/models/rolebinding.py:38 -#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:37 +#: rbac/serializers/role.py:12 settings/serializers/auth/oauth2.py:36 msgid "Scope" msgstr "范围" @@ -3472,7 +3737,7 @@ msgstr "权限" msgid "Users amount" msgstr "用户数量" -#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:23 +#: rbac/serializers/role.py:28 terminal/models/applet/applet.py:24 msgid "Display name" msgstr "显示名称" @@ -3524,24 +3789,24 @@ msgstr "备份账号" msgid "Gather account" msgstr "收集账号" -#: rbac/tree.py:51 +#: rbac/tree.py:52 msgid "Asset change auth" msgstr "资产改密" -#: rbac/tree.py:52 +#: rbac/tree.py:53 msgid "Terminal setting" msgstr "终端设置" -#: rbac/tree.py:53 +#: rbac/tree.py:54 msgid "Task Center" msgstr "任务中心" -#: rbac/tree.py:54 +#: rbac/tree.py:55 msgid "My assets" msgstr "我的资产" -#: rbac/tree.py:56 terminal/models/applet/applet.py:38 -#: terminal/models/applet/applet.py:127 terminal/models/applet/host.py:27 +#: rbac/tree.py:57 terminal/models/applet/applet.py:39 +#: terminal/models/applet/applet.py:133 terminal/models/applet/host.py:27 msgid "Applet" msgstr "远程应用" @@ -3561,10 +3826,6 @@ msgstr "一般设置" msgid "View permission tree" msgstr "查看授权树" -#: rbac/tree.py:124 -msgid "Execute batch command" -msgstr "执行批量命令" - #: settings/api/dingtalk.py:31 settings/api/feishu.py:36 #: settings/api/sms.py:148 settings/api/wecom.py:37 msgid "Test success" @@ -3706,7 +3967,7 @@ msgstr "服务端地址" msgid "Proxy server url" msgstr "回调地址" -#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:55 +#: settings/serializers/auth/cas.py:18 settings/serializers/auth/oauth2.py:54 #: settings/serializers/auth/saml2.py:34 msgid "Logout completely" msgstr "同步注销" @@ -3768,7 +4029,7 @@ msgstr "用户过滤器" msgid "Choice may be (cn|uid|sAMAccountName)=%(user)s)" msgstr "可能的选项是(cn或uid或sAMAccountName=%(user)s)" -#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:57 +#: settings/serializers/auth/ldap.py:58 settings/serializers/auth/oauth2.py:56 #: settings/serializers/auth/oidc.py:37 msgid "User attr map" msgstr "用户属性映射" @@ -3793,52 +4054,52 @@ msgstr "搜索分页数量" msgid "Enable LDAP auth" msgstr "启用 LDAP 认证" -#: settings/serializers/auth/oauth2.py:19 +#: settings/serializers/auth/oauth2.py:18 msgid "OAuth2" msgstr "OAuth2" -#: settings/serializers/auth/oauth2.py:22 +#: settings/serializers/auth/oauth2.py:21 msgid "Enable OAuth2 Auth" msgstr "启用 OAuth2 认证" -#: settings/serializers/auth/oauth2.py:25 +#: settings/serializers/auth/oauth2.py:24 msgid "Logo" msgstr "图标" -#: settings/serializers/auth/oauth2.py:28 +#: settings/serializers/auth/oauth2.py:27 msgid "Service provider" msgstr "服务提供商" -#: settings/serializers/auth/oauth2.py:31 settings/serializers/auth/oidc.py:19 +#: settings/serializers/auth/oauth2.py:30 settings/serializers/auth/oidc.py:19 msgid "Client Id" msgstr "客户端 ID" -#: settings/serializers/auth/oauth2.py:34 settings/serializers/auth/oidc.py:22 +#: settings/serializers/auth/oauth2.py:33 settings/serializers/auth/oidc.py:22 #: xpack/plugins/cloud/serializers/account_attrs.py:38 msgid "Client Secret" msgstr "客户端密钥" -#: settings/serializers/auth/oauth2.py:40 settings/serializers/auth/oidc.py:68 +#: settings/serializers/auth/oauth2.py:39 settings/serializers/auth/oidc.py:68 msgid "Provider auth endpoint" msgstr "授权端点地址" -#: settings/serializers/auth/oauth2.py:43 settings/serializers/auth/oidc.py:71 +#: settings/serializers/auth/oauth2.py:42 settings/serializers/auth/oidc.py:71 msgid "Provider token endpoint" msgstr "token 端点地址" -#: settings/serializers/auth/oauth2.py:46 settings/serializers/auth/oidc.py:30 +#: settings/serializers/auth/oauth2.py:45 settings/serializers/auth/oidc.py:30 msgid "Client authentication method" msgstr "客户端认证方式" -#: settings/serializers/auth/oauth2.py:50 settings/serializers/auth/oidc.py:77 +#: settings/serializers/auth/oauth2.py:49 settings/serializers/auth/oidc.py:77 msgid "Provider userinfo endpoint" msgstr "用户信息端点地址" -#: settings/serializers/auth/oauth2.py:53 settings/serializers/auth/oidc.py:80 +#: settings/serializers/auth/oauth2.py:52 settings/serializers/auth/oidc.py:80 msgid "Provider end session endpoint" msgstr "注销会话端点地址" -#: settings/serializers/auth/oauth2.py:60 settings/serializers/auth/oidc.py:98 +#: settings/serializers/auth/oauth2.py:59 settings/serializers/auth/oidc.py:98 #: settings/serializers/auth/saml2.py:35 msgid "Always update user" msgstr "总是更新用户信息" @@ -3973,7 +4234,7 @@ msgstr "短信服务商 / 协议" #: settings/serializers/auth/sms.py:22 settings/serializers/auth/sms.py:45 #: settings/serializers/auth/sms.py:53 settings/serializers/auth/sms.py:62 -#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:68 +#: settings/serializers/auth/sms.py:73 settings/serializers/email.py:69 msgid "Signature" msgstr "签名" @@ -4045,7 +4306,7 @@ msgstr "其它系统可以使用 SSO Token 对接 JumpServer, 免去登录的过 msgid "SSO auth key TTL" msgstr "令牌有效期" -#: settings/serializers/auth/sso.py:17 +#: settings/serializers/auth/sso.py:17 settings/serializers/security.py:117 #: xpack/plugins/cloud/serializers/account_attrs.py:176 msgid "Unit: second" msgstr "单位: 秒" @@ -4222,7 +4483,7 @@ msgstr "提示: 创建用户时,发送设置密码邮件的敬语 (例如: 你 msgid "Create user email content" msgstr "邮件的内容" -#: settings/serializers/email.py:65 +#: settings/serializers/email.py:66 #, python-brace-format msgid "" "Tips: When creating a user, send the content of the email, support " @@ -4230,7 +4491,7 @@ msgid "" msgstr "" "提示: 创建用户时,发送设置密码邮件的内容, 支持 {username} {name} {email} 标签" -#: settings/serializers/email.py:69 +#: settings/serializers/email.py:70 msgid "Tips: Email signature (eg:jumpserver)" msgstr "邮件署名 (如:jumpserver)" @@ -4446,10 +4707,16 @@ msgid "" msgstr "单位: 秒, 目前仅在查看账号密码校验 MFA 时生效" #: settings/serializers/security.py:116 +#, fuzzy +#| msgid "Verify code" +msgid "Verify code TTL" +msgstr "验证码" + +#: settings/serializers/security.py:121 msgid "Enable Login dynamic code" msgstr "启用登录附加码" -#: settings/serializers/security.py:117 +#: settings/serializers/security.py:122 msgid "" "The password and additional code are sent to a third party authentication " "system for verification" @@ -4457,93 +4724,93 @@ msgstr "" "密码和附加码一并发送给第三方认证系统进行校验, 如:有的第三方认证系统,需要 密" "码+6位数字 完成认证" -#: settings/serializers/security.py:122 +#: settings/serializers/security.py:127 msgid "MFA in login page" msgstr "MFA 在登录页面输入" -#: settings/serializers/security.py:123 +#: settings/serializers/security.py:128 msgid "Eu security regulations(GDPR) require MFA to be on the login page" msgstr "欧盟数据安全法规(GDPR) 要求 MFA 在登录页面,来确保系统登录安全" -#: settings/serializers/security.py:126 +#: settings/serializers/security.py:131 msgid "Enable Login captcha" msgstr "启用登录验证码" -#: settings/serializers/security.py:127 +#: settings/serializers/security.py:132 msgid "Enable captcha to prevent robot authentication" msgstr "开启验证码,防止机器人登录" -#: settings/serializers/security.py:146 +#: settings/serializers/security.py:151 msgid "Security" msgstr "安全" -#: settings/serializers/security.py:149 +#: settings/serializers/security.py:154 msgid "Enable terminal register" msgstr "终端注册" -#: settings/serializers/security.py:151 +#: settings/serializers/security.py:156 msgid "" "Allow terminal register, after all terminal setup, you should disable this " "for security" msgstr "是否允许终端注册,当所有终端启动后,为了安全应该关闭" -#: settings/serializers/security.py:155 +#: settings/serializers/security.py:160 msgid "Enable watermark" msgstr "开启水印" -#: settings/serializers/security.py:156 +#: settings/serializers/security.py:161 msgid "Enabled, the web session and replay contains watermark information" msgstr "启用后,Web 会话和录像将包含水印信息" -#: settings/serializers/security.py:160 +#: settings/serializers/security.py:165 msgid "Connection max idle time" msgstr "连接最大空闲时间" -#: settings/serializers/security.py:161 +#: settings/serializers/security.py:166 msgid "If idle time more than it, disconnect connection Unit: minute" msgstr "提示:如果超过该配置没有操作,连接会被断开 (单位:分)" -#: settings/serializers/security.py:164 +#: settings/serializers/security.py:169 msgid "Remember manual auth" msgstr "保存手动输入密码" -#: settings/serializers/security.py:167 +#: settings/serializers/security.py:172 msgid "Enable change auth secure mode" msgstr "启用改密安全模式" -#: settings/serializers/security.py:170 +#: settings/serializers/security.py:175 msgid "Insecure command alert" msgstr "危险命令告警" -#: settings/serializers/security.py:173 +#: settings/serializers/security.py:178 msgid "Email recipient" msgstr "邮件收件人" -#: settings/serializers/security.py:174 +#: settings/serializers/security.py:179 msgid "Multiple user using , split" msgstr "多个用户,使用 , 分割" -#: settings/serializers/security.py:177 -msgid "Batch command execution" -msgstr "批量命令执行" +#: settings/serializers/security.py:182 +msgid "Operation center" +msgstr "作业中心" -#: settings/serializers/security.py:178 +#: settings/serializers/security.py:183 msgid "Allow user run batch command or not using ansible" msgstr "是否允许用户使用 ansible 执行批量命令" -#: settings/serializers/security.py:181 +#: settings/serializers/security.py:186 msgid "Session share" msgstr "会话分享" -#: settings/serializers/security.py:182 +#: settings/serializers/security.py:187 msgid "Enabled, Allows user active session to be shared with other users" msgstr "开启后允许用户分享已连接的资产会话给他人,协同工作" -#: settings/serializers/security.py:185 +#: settings/serializers/security.py:190 msgid "Remote Login Protection" msgstr "异地登录保护" -#: settings/serializers/security.py:187 +#: settings/serializers/security.py:192 msgid "" "The system determines whether the login IP address belongs to a common login " "city. If the account is logged in from a common login city, the system sends " @@ -4552,6 +4819,10 @@ msgstr "" "根据登录 IP 是否所属常用登录城市进行判断,若账号在非常用城市登录,会发送异地" "登录提醒" +#: settings/serializers/terminal.py:9 +msgid "Hostname" +msgstr "主机名" + #: settings/serializers/terminal.py:15 msgid "Auto" msgstr "自动" @@ -4979,7 +5250,7 @@ msgid "Input" msgstr "输入" #: terminal/backends/command/models.py:24 -#: terminal/backends/command/serializers.py:38 +#: terminal/backends/command/serializers.py:39 msgid "Output" msgstr "输出" @@ -4999,22 +5270,22 @@ msgstr "风险等级" msgid "Session ID" msgstr "会话ID" -#: terminal/backends/command/serializers.py:37 +#: terminal/backends/command/serializers.py:38 msgid "Account " msgstr "账号" -#: terminal/backends/command/serializers.py:39 +#: terminal/backends/command/serializers.py:40 msgid "Timestamp" msgstr "时间戳" -#: terminal/backends/command/serializers.py:41 +#: terminal/backends/command/serializers.py:42 #: terminal/models/component/terminal.py:84 msgid "Remote Address" msgstr "远端地址" -#: terminal/connect_methods.py:46 terminal/connect_methods.py:47 -#: terminal/connect_methods.py:48 terminal/connect_methods.py:49 -#: terminal/connect_methods.py:50 +#: terminal/connect_methods.py:47 terminal/connect_methods.py:48 +#: terminal/connect_methods.py:49 terminal/connect_methods.py:50 +#: terminal/connect_methods.py:51 msgid "DB Client" msgstr "数据库客户端" @@ -5035,6 +5306,10 @@ msgstr "正常" msgid "Offline" msgstr "离线" +#: terminal/const.py:61 +msgid "Mismatch" +msgstr "" + #: terminal/exceptions.py:8 msgid "Bulk create not support" msgstr "不支持批量创建" @@ -5043,19 +5318,24 @@ msgstr "不支持批量创建" msgid "Storage is invalid" msgstr "存储无效" -#: terminal/models/applet/applet.py:25 +#: terminal/models/applet/applet.py:26 msgid "Author" msgstr "作者" -#: terminal/models/applet/applet.py:30 +#: terminal/models/applet/applet.py:31 msgid "Tags" msgstr "标签" -#: terminal/models/applet/applet.py:34 terminal/serializers/storage.py:157 +#: terminal/models/applet/applet.py:35 terminal/serializers/storage.py:157 msgid "Hosts" msgstr "主机" -#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:38 +#: terminal/models/applet/applet.py:135 terminal/models/applet/host.py:33 +#: terminal/models/applet/host.py:105 +msgid "Hosting" +msgstr "宿主机" + +#: terminal/models/applet/host.py:18 terminal/serializers/applet_host.py:40 msgid "Deploy options" msgstr "部署参数" @@ -5071,47 +5351,55 @@ msgstr "初始化日期" msgid "Date synced" msgstr "同步日期" -#: terminal/models/applet/host.py:33 -msgid "Applet host" -msgstr "远程应用发布机" - -#: terminal/models/applet/host.py:105 -msgid "Hosting" -msgstr "宿主机" - #: terminal/models/applet/host.py:106 msgid "Initial" msgstr "初始化" #: terminal/models/component/endpoint.py:15 -msgid "HTTPS Port" +msgid "HTTPS port" msgstr "HTTPS 端口" #: terminal/models/component/endpoint.py:16 -msgid "HTTP Port" +msgid "HTTP port" msgstr "HTTP 端口" #: terminal/models/component/endpoint.py:17 -msgid "SSH Port" +msgid "SSH port" msgstr "SSH 端口" #: terminal/models/component/endpoint.py:18 -msgid "RDP Port" +msgid "RDP port" msgstr "RDP 端口" -#: terminal/models/component/endpoint.py:25 -#: terminal/models/component/endpoint.py:94 terminal/serializers/endpoint.py:57 +#: terminal/models/component/endpoint.py:19 +msgid "MySQL port" +msgstr "MySQL 端口" + +#: terminal/models/component/endpoint.py:20 +msgid "MariaDB port" +msgstr "MariaDB 端口" + +#: terminal/models/component/endpoint.py:21 +msgid "PostgreSQL port" +msgstr "PostgreSQL 端口" + +#: terminal/models/component/endpoint.py:22 +msgid "Redis port" +msgstr "Redis 端口" + +#: terminal/models/component/endpoint.py:29 +#: terminal/models/component/endpoint.py:98 terminal/serializers/endpoint.py:64 #: terminal/serializers/storage.py:38 terminal/serializers/storage.py:50 #: terminal/serializers/storage.py:80 terminal/serializers/storage.py:90 #: terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "端点" -#: terminal/models/component/endpoint.py:87 +#: terminal/models/component/endpoint.py:91 msgid "IP group" msgstr "IP 组" -#: terminal/models/component/endpoint.py:99 +#: terminal/models/component/endpoint.py:103 msgid "Endpoint rule" msgstr "端点规则" @@ -5277,73 +5565,63 @@ msgstr "级别" msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/serializers/applet.py:16 -msgid "Published" -msgstr "已发布" - -#: terminal/serializers/applet.py:17 -msgid "Unpublished" -msgstr "未发布" - -#: terminal/serializers/applet.py:18 -msgid "Not match" -msgstr "没有匹配的" - -#: terminal/serializers/applet.py:32 +#: terminal/serializers/applet.py:27 msgid "Icon" msgstr "图标" -#: terminal/serializers/applet_host.py:21 +#: terminal/serializers/applet_host.py:23 msgid "Per Session" msgstr "每会话" -#: terminal/serializers/applet_host.py:22 +#: terminal/serializers/applet_host.py:24 msgid "Per Device" msgstr "每设备" -#: terminal/serializers/applet_host.py:28 +#: terminal/serializers/applet_host.py:30 msgid "RDS Licensing" msgstr "RDS 许可证" -#: terminal/serializers/applet_host.py:29 +#: terminal/serializers/applet_host.py:31 msgid "RDS License Server" msgstr "RDS 许可服务器" -#: terminal/serializers/applet_host.py:30 +#: terminal/serializers/applet_host.py:32 msgid "RDS Licensing Mode" msgstr "RDS 授权模式" -#: terminal/serializers/applet_host.py:32 +#: terminal/serializers/applet_host.py:34 msgid "RDS fSingleSessionPerUser" msgstr "" -#: terminal/serializers/applet_host.py:33 +#: terminal/serializers/applet_host.py:35 msgid "RDS Max Disconnection Time" msgstr "" -#: terminal/serializers/applet_host.py:34 +#: terminal/serializers/applet_host.py:36 msgid "RDS Remote App Logoff Time Limit" msgstr "" -#: terminal/serializers/applet_host.py:40 terminal/serializers/terminal.py:41 +#: terminal/serializers/applet_host.py:42 terminal/serializers/terminal.py:41 msgid "Load status" msgstr "负载状态" #: terminal/serializers/endpoint.py:14 -msgid "Magnus listen db port" -msgstr "Magnus 监听的数据库端口" +msgid "Oracle port" +msgstr "Oracle 端口" #: terminal/serializers/endpoint.py:17 -msgid "Magnus Listen port range" -msgstr "Magnus 监听的端口范围" +msgid "Oracle port range" +msgstr "Oracle 端口范围" #: terminal/serializers/endpoint.py:19 msgid "" -"The range of ports that Magnus listens on is modified in the configuration " -"file" -msgstr "请在配置文件中修改 Magnus 监听的端口范围" +"Oracle proxy server listen port is dynamic, Each additional Oracle database " +"instance adds a port listener" +msgstr "" +"Oracle 代理服务器监听端口是动态的,每增加一个 Oracle 数据库实例,就会增加一个" +"端口监听" -#: terminal/serializers/endpoint.py:51 +#: terminal/serializers/endpoint.py:58 msgid "" "If asset IP addresses under different endpoints conflict, use asset labels" msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" @@ -5352,43 +5630,39 @@ msgstr "如果不同端点下的资产 IP 有冲突,使用资产标签实现" msgid "Tunnel" msgstr "" -#: terminal/serializers/session.py:41 -msgid "User ID" -msgstr "用户 ID" - -#: terminal/serializers/session.py:42 -msgid "Asset ID" -msgstr "资产 ID" - -#: terminal/serializers/session.py:43 -msgid "Login from display" -msgstr "登录来源名称" - -#: terminal/serializers/session.py:45 +#: terminal/serializers/session.py:28 terminal/serializers/session.py:50 msgid "Can replay" msgstr "是否可重放" -#: terminal/serializers/session.py:46 +#: terminal/serializers/session.py:29 terminal/serializers/session.py:51 msgid "Can join" msgstr "是否可加入" -#: terminal/serializers/session.py:47 -msgid "Terminal ID" -msgstr "终端 ID" - -#: terminal/serializers/session.py:48 -msgid "Is finished" -msgstr "是否完成" - -#: terminal/serializers/session.py:49 +#: terminal/serializers/session.py:30 terminal/serializers/session.py:54 msgid "Can terminate" msgstr "是否可中断" -#: terminal/serializers/session.py:50 +#: terminal/serializers/session.py:46 +msgid "User ID" +msgstr "用户 ID" + +#: terminal/serializers/session.py:47 +msgid "Asset ID" +msgstr "资产 ID" + +#: terminal/serializers/session.py:48 +msgid "Login from display" +msgstr "登录来源名称" + +#: terminal/serializers/session.py:52 +msgid "Terminal ID" +msgstr "终端 ID" + +#: terminal/serializers/session.py:55 msgid "Terminal display" msgstr "终端显示" -#: terminal/serializers/session.py:55 +#: terminal/serializers/session.py:60 msgid "Command amount" msgstr "命令数量" @@ -5403,12 +5677,12 @@ msgstr "桶名称" #: terminal/serializers/storage.py:30 #: xpack/plugins/cloud/serializers/account_attrs.py:17 msgid "Access key id" -msgstr "访问密钥 ID(AK)" +msgstr "Access key ID(AK)" #: terminal/serializers/storage.py:34 #: xpack/plugins/cloud/serializers/account_attrs.py:20 msgid "Access key secret" -msgstr "访问密钥密文(SK)" +msgstr "Access key secret(SK)" #: terminal/serializers/storage.py:65 xpack/plugins/cloud/models.py:217 msgid "Region" @@ -5466,7 +5740,7 @@ msgstr "没有发现" msgid "view" msgstr "查看" -#: terminal/utils/db_port_mapper.py:77 +#: terminal/utils/db_port_mapper.py:84 msgid "" "No available port is matched. The number of databases may have exceeded the " "number of ports open to the database agent service, Contact the " @@ -5475,13 +5749,13 @@ msgstr "" "未匹配到可用端口,数据库的数量可能已经超过数据库代理服务开放的端口数量,请联" "系管理员开放更多端口。" -#: terminal/utils/db_port_mapper.py:103 +#: terminal/utils/db_port_mapper.py:112 msgid "" "No ports can be used, check and modify the limit on the number of ports that " "Magnus listens on in the configuration file." msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听的端口数量限制。" -#: terminal/utils/db_port_mapper.py:105 +#: terminal/utils/db_port_mapper.py:114 msgid "All available port count: {}, Already use port count: {}" msgstr "所有可用端口数量:{},已使用端口数量:{}" @@ -5581,15 +5855,15 @@ msgid "Body" msgstr "内容" #: tickets/models/flow.py:19 tickets/models/flow.py:61 -#: tickets/models/ticket/general.py:40 +#: tickets/models/ticket/general.py:41 msgid "Approve level" msgstr "审批级别" -#: tickets/models/flow.py:24 tickets/serializers/flow.py:18 +#: tickets/models/flow.py:24 tickets/serializers/flow.py:17 msgid "Approve strategy" msgstr "审批策略" -#: tickets/models/flow.py:29 tickets/serializers/flow.py:20 +#: tickets/models/flow.py:29 tickets/serializers/flow.py:19 msgid "Assignees" msgstr "受理人" @@ -5623,20 +5897,14 @@ msgstr "申请的系统用户" msgid "Select at least one asset or node" msgstr "资产或者节点至少选择一项" -#: tickets/models/ticket/apply_asset.py:14 -#: tickets/serializers/ticket/apply_asset.py:26 -msgid "Apply nodes" -msgstr "申请节点" - -#: tickets/models/ticket/apply_asset.py:16 -#: tickets/serializers/ticket/apply_asset.py:22 -msgid "Apply assets" -msgstr "申请资产" - #: tickets/models/ticket/apply_asset.py:17 msgid "Apply accounts" msgstr "申请账号" +#: tickets/models/ticket/apply_asset.py:26 +msgid "Apply Asset Ticket" +msgstr "申请资产" + #: tickets/models/ticket/command_confirm.py:9 msgid "Run user" msgstr "运行的用户" @@ -5653,11 +5921,11 @@ msgstr "运行的命令" msgid "Command filter acl" msgstr "命令过滤器" -#: tickets/models/ticket/general.py:75 +#: tickets/models/ticket/general.py:76 msgid "Ticket step" msgstr "工单步骤" -#: tickets/models/ticket/general.py:93 +#: tickets/models/ticket/general.py:94 msgid "Ticket assignee" msgstr "工单受理人" @@ -5685,7 +5953,7 @@ msgstr "工单快照" msgid "Please try again" msgstr "请再次尝试" -#: tickets/models/ticket/general.py:425 +#: tickets/models/ticket/general.py:458 msgid "Super ticket" msgstr "超级工单" @@ -5729,19 +5997,19 @@ msgstr "你的工单已被处理, 处理人 - {}" msgid "Ticket has processed - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})" -#: tickets/serializers/flow.py:21 +#: tickets/serializers/flow.py:20 msgid "Assignees display" msgstr "受理人名称" -#: tickets/serializers/flow.py:47 +#: tickets/serializers/flow.py:46 msgid "Please select the Assignees" msgstr "请选择受理人" -#: tickets/serializers/flow.py:75 +#: tickets/serializers/flow.py:74 msgid "The current organization type already exists" msgstr "当前组织已存在该类型" -#: tickets/serializers/super_ticket.py:11 +#: tickets/serializers/super_ticket.py:15 msgid "Processor" msgstr "处理人" @@ -5749,6 +6017,14 @@ msgstr "处理人" msgid "Support fuzzy search, and display up to 10 items" msgstr "支持模糊搜索,最多显示10项" +#: tickets/serializers/ticket/apply_asset.py:22 +msgid "Apply assets" +msgstr "申请资产" + +#: tickets/serializers/ticket/apply_asset.py:26 +msgid "Apply nodes" +msgstr "申请节点" + #: tickets/serializers/ticket/apply_asset.py:28 msgid "Apply actions" msgstr "申请动作" @@ -5766,7 +6042,7 @@ msgstr "过期时间要大于开始时间" msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" -#: tickets/serializers/ticket/ticket.py:95 +#: tickets/serializers/ticket/ticket.py:88 msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" @@ -5935,10 +6211,6 @@ msgstr "SSH公钥" msgid "Force enable" msgstr "强制启用" -#: users/models/user.py:631 -msgid "Local" -msgstr "数据库" - #: users/models/user.py:687 users/serializers/user.py:160 msgid "Is service account" msgstr "服务账号" @@ -5957,7 +6229,7 @@ msgstr "手机" #: users/models/user.py:701 msgid "OTP secret key" -msgstr "OTP 秘钥" +msgstr "OTP 密钥" #: users/models/user.py:705 msgid "Private key" @@ -5972,10 +6244,6 @@ msgstr "Secret key" msgid "Is first login" msgstr "首次登录" -#: users/models/user.py:727 -msgid "Source" -msgstr "来源" - #: users/models/user.py:731 msgid "Date password last updated" msgstr "最后更新密码日期" @@ -6780,7 +7048,7 @@ msgstr "证书文件" #: xpack/plugins/cloud/serializers/account_attrs.py:118 msgid "Key File" -msgstr "秘钥文件" +msgstr "密钥文件" #: xpack/plugins/cloud/serializers/account_attrs.py:134 msgid "Service account key" @@ -6897,26 +7165,94 @@ msgstr "许可证导入成功" msgid "License is invalid" msgstr "无效的许可证" -#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:127 +#: xpack/plugins/license/meta.py:11 xpack/plugins/license/models.py:135 msgid "License" msgstr "许可证" -#: xpack/plugins/license/models.py:71 +#: xpack/plugins/license/models.py:79 msgid "Standard edition" msgstr "标准版" -#: xpack/plugins/license/models.py:73 +#: xpack/plugins/license/models.py:81 msgid "Enterprise edition" msgstr "企业版" -#: xpack/plugins/license/models.py:75 +#: xpack/plugins/license/models.py:83 msgid "Ultimate edition" msgstr "旗舰版" -#: xpack/plugins/license/models.py:77 +#: xpack/plugins/license/models.py:85 msgid "Community edition" msgstr "社区版" +#~ msgid "Dynamic username" +#~ msgstr "同名账号" + +#~ msgid "Username already exists" +#~ msgstr "用户名 `{}` 已存在" + +#~ msgid "Dynamic username already exists" +#~ msgstr "动态用户名已存在" + +#~ msgid "Commands" +#~ msgstr "命令" + +#~ msgid "Change password enabled" +#~ msgstr "开启账号改密" + +#~ msgid "Change password method" +#~ msgstr "更改密码方式" + +#~ msgid "{} used account[{}], login method[{}] login the asset." +#~ msgstr "{} 使用账户[{}], 登录方式[{}]登录了这个资产." + +#~ msgid "User {} has executed change auth plan for this account.({})" +#~ msgstr "用户 {} 为这个账号执行了改密计划.({})" + +#~ msgid "Applet host" +#~ msgstr "远程应用发布机" + +#~ msgid "" +#~ "The range of ports that Magnus listens on is modified in the " +#~ "configuration file" +#~ msgstr "请在配置文件中修改 Magnus 监听的端口范围" + +#~ msgid "Magnus Listen port range" +#~ msgstr "Magnus 监听的端口范围" + +#~ msgid "Published" +#~ msgstr "已发布" + +#~ msgid "Unpublished" +#~ msgstr "未发布" + +#~ msgid "Not match" +#~ msgstr "没有匹配的" + +#~ msgid "Magnus listen db port" +#~ msgstr "Magnus 监听的数据库端口" + +#~ msgid "Update task content: {}" +#~ msgstr "更新任务内容: {}" + +#~ msgid "Execute batch command" +#~ msgstr "执行批量命令" + +#~ msgid "Create account" +#~ msgstr "创建账号" + +#~ msgid "Present" +#~ msgstr "存在" + +#~ msgid "Date last login" +#~ msgstr "最后登录日期" + +#~ msgid "IP last login" +#~ msgstr "最后登录IP" + +#~ msgid "GatherUser" +#~ msgstr "收集用户" + #~ msgid "Welcome back, please enter username and password to login" #~ msgstr "欢迎回来,请输入用户名和密码登录" @@ -7179,9 +7515,6 @@ msgstr "社区版" #~ msgid "Default Cluster" #~ msgstr "默认Cluster" -#~ msgid "Test gateway" -#~ msgstr "测试网关" - #~ msgid "User groups" #~ msgstr "用户组" @@ -7254,12 +7587,6 @@ msgstr "社区版" #~ msgid "Only system users with automatic login are allowed" #~ msgstr "仅允许自动登录的系统用户" -#~ msgid "System user name" -#~ msgstr "系统用户名称" - -#~ msgid "Asset hostname" -#~ msgstr "资产主机名" - #~ msgid "The asset {} system platform {} does not support run Ansible tasks" #~ msgstr "资产 {} 系统平台 {} 不支持运行 Ansible 任务" @@ -7358,9 +7685,6 @@ msgstr "社区版" #~ msgid "Callback" #~ msgstr "回调" -#~ msgid "Can view task monitor" -#~ msgstr "可以查看任务监控" - #~ msgid "Tasks" #~ msgstr "任务" diff --git a/apps/notifications/api/notifications.py b/apps/notifications/api/notifications.py index 5aa404896..bbac4b49a 100644 --- a/apps/notifications/api/notifications.py +++ b/apps/notifications/api/notifications.py @@ -2,7 +2,7 @@ from rest_framework.mixins import ListModelMixin, UpdateModelMixin, RetrieveMode from rest_framework.views import APIView from rest_framework.response import Response -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from common.permissions import IsValidUser from notifications.notifications import system_msgs from notifications.models import SystemMsgSubscription, UserMsgSubscription diff --git a/apps/notifications/api/site_msgs.py b/apps/notifications/api/site_msgs.py index 29bd785d5..5c9257c25 100644 --- a/apps/notifications/api/site_msgs.py +++ b/apps/notifications/api/site_msgs.py @@ -2,10 +2,10 @@ from rest_framework.response import Response from rest_framework.mixins import ListModelMixin, RetrieveModelMixin from rest_framework.decorators import action -from common.http import is_true +from common.utils.http import is_true from common.permissions import IsValidUser from common.const.http import GET, PATCH, POST -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from ..serializers import ( SiteMessageDetailSerializer, SiteMessageIdsSerializer, SiteMessageSendSerializer, @@ -13,7 +13,7 @@ from ..serializers import ( from ..site_msg import SiteMessageUtil from ..filters import SiteMsgFilter -__all__ = ('SiteMessageViewSet', ) +__all__ = ('SiteMessageViewSet',) class SiteMessageViewSet(ListModelMixin, RetrieveModelMixin, JMSGenericViewSet): diff --git a/apps/notifications/serializers/notifications.py b/apps/notifications/serializers/notifications.py index fcf8c811c..04ba2560b 100644 --- a/apps/notifications/serializers/notifications.py +++ b/apps/notifications/serializers/notifications.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.serializers import BulkModelSerializer +from common.serializers import BulkModelSerializer from notifications.models import SystemMsgSubscription, UserMsgSubscription diff --git a/apps/ops/ansible/ansible.cfg b/apps/ops/ansible/ansible.cfg new file mode 100644 index 000000000..c51b2d733 --- /dev/null +++ b/apps/ops/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +forks = 10 +host_key_checking = False +library = /opt/jumpserver/apps/ops/ansible/modules:./modules +[inventory] +[privilege_escalation] +[paramiko_connection] +[ssh_connection] +[persistent_connection] +[accelerate] +[selinux] +[colors] +[diff] diff --git a/apps/ops/ansible/modules_utils/oracle_common.py b/apps/ops/ansible/modules_utils/oracle_common.py index c88f04373..8036a8c1e 100644 --- a/apps/ops/ansible/modules_utils/oracle_common.py +++ b/apps/ops/ansible/modules_utils/oracle_common.py @@ -56,7 +56,7 @@ class OracleClient(object): def cursor(self): if self._cursor is None: try: - oracledb.init_oracle_client(lib_dir='/Users/jiangweidong/Downloads/instantclient_19_8') + oracledb.init_oracle_client(lib_dir='/opt/oracle/instantclient_19_10') self._conn = oracledb.connect(**self.connect_params) self._cursor = self._conn.cursor() except DatabaseError as err: diff --git a/apps/ops/ansible/runner.py b/apps/ops/ansible/runner.py index 280f8f05c..18df1a702 100644 --- a/apps/ops/ansible/runner.py +++ b/apps/ops/ansible/runner.py @@ -1,5 +1,5 @@ -import uuid import os +import uuid import ansible_runner from django.conf import settings @@ -78,6 +78,7 @@ class PlaybookRunner: verbosity=verbosity, event_handler=self.cb.event_handler, status_handler=self.cb.status_handler, + host_cwd=self.project_dir, **kwargs ) return self.cb diff --git a/apps/ops/api/celery.py b/apps/ops/api/celery.py index 8d58c1981..f55c296f9 100644 --- a/apps/ops/api/celery.py +++ b/apps/ops/api/celery.py @@ -17,7 +17,7 @@ from ..models import CeleryTaskExecution, CeleryTask from ..serializers import CeleryResultSerializer, CeleryPeriodTaskSerializer from ..celery.utils import get_celery_task_log_path from ..ansible.utils import get_ansible_task_log_path -from common.mixins.api import CommonApiMixin +from common.api import CommonApiMixin __all__ = [ 'CeleryTaskExecutionLogApi', 'CeleryResultApi', 'CeleryPeriodTaskViewSet', diff --git a/apps/ops/api/job.py b/apps/ops/api/job.py index 817e5040e..30bd0ee18 100644 --- a/apps/ops/api/job.py +++ b/apps/ops/api/job.py @@ -13,7 +13,7 @@ from ops.tasks import run_ops_job_execution from ops.variables import JMS_JOB_VARIABLE_HELP from orgs.mixins.api import OrgBulkModelViewSet from orgs.utils import tmp_to_org, get_current_org_id, get_current_org -from assets.models import Account +from accounts.models import Account def set_task_to_serializer_data(serializer, task): @@ -105,7 +105,7 @@ class JobExecutionTaskDetail(APIView): def get(self, request, **kwargs): org = get_current_org() - task_id = request.query_params.get('task_id') + task_id = str(kwargs.get('task_id')) if task_id: with tmp_to_org(org): execution = get_object_or_404(JobExecution, task_id=task_id) @@ -114,6 +114,7 @@ class JobExecutionTaskDetail(APIView): 'is_finished': execution.is_finished, 'is_success': execution.is_success, 'time_cost': execution.time_cost, + 'job_id': execution.job.id, }) @@ -122,5 +123,6 @@ class FrequentUsernames(APIView): permission_classes = () def get(self, request, **kwargs): - top_accounts = Account.objects.all().values('username').annotate(total=Count('username')).order_by('total') + top_accounts = Account.objects.exclude(username='root').exclude(username__startswith='jms_').values('username').annotate( + total=Count('username')).order_by('total')[:5] return Response(data=top_accounts) diff --git a/apps/ops/celery/logger.py b/apps/ops/celery/logger.py index 46b7e626c..4c1462583 100644 --- a/apps/ops/celery/logger.py +++ b/apps/ops/celery/logger.py @@ -1,13 +1,14 @@ from logging import StreamHandler from threading import get_ident -from django.conf import settings from celery import current_task from celery.signals import task_prerun, task_postrun +from django.conf import settings from kombu import Connection, Exchange, Queue, Producer from kombu.mixins import ConsumerMixin from .utils import get_celery_task_log_path +from ..const import CELERY_LOG_MAGIC_MARK routing_key = 'celery_log' celery_log_exchange = Exchange('celery_log_exchange', type='direct') @@ -198,8 +199,8 @@ class CeleryThreadTaskFileHandler(CeleryThreadingLoggerHandler): if not f: raise ValueError('Not found thread task file') msg = self.format(record) - f.write(msg) - f.write(self.terminator) + f.write(msg.encode()) + f.write(self.terminator.encode()) f.flush() def flush(self): @@ -210,12 +211,13 @@ class CeleryThreadTaskFileHandler(CeleryThreadingLoggerHandler): log_path = get_celery_task_log_path(task_id) thread_id = self.get_current_thread_id() self.task_id_thread_id_mapper[task_id] = thread_id - f = open(log_path, 'a') + f = open(log_path, 'ab') self.thread_id_fd_mapper[thread_id] = f def handle_task_end(self, task_id): ident_id = self.task_id_thread_id_mapper.get(task_id, '') f = self.thread_id_fd_mapper.pop(ident_id, None) if f and not f.closed: + f.write(CELERY_LOG_MAGIC_MARK) f.close() self.task_id_thread_id_mapper.pop(task_id, None) diff --git a/apps/ops/const.py b/apps/ops/const.py index c383ef3c7..8838c31c5 100644 --- a/apps/ops/const.py +++ b/apps/ops/const.py @@ -51,3 +51,7 @@ class JobStatus(models.TextChoices): success = 'success', _('Success') timeout = 'timeout', _('Timeout') failed = 'failed', _('Failed') + + +# celery 日志完成之后,写入的魔法字符,作为结束标记 +CELERY_LOG_MAGIC_MARK = b'\x00\x00\x00\x00\x00' diff --git a/apps/ops/migrations/0022_auto_20220817_1346.py b/apps/ops/migrations/0022_auto_20220817_1346.py index b06c85a18..2e1c8be47 100644 --- a/apps/ops/migrations/0022_auto_20220817_1346.py +++ b/apps/ops/migrations/0022_auto_20220817_1346.py @@ -3,45 +3,51 @@ from django.db import migrations, models -def migrate_run_system_user_to_account(apps, schema_editor): - execution_model = apps.get_model('ops', 'CommandExecution') - count = 0 - bulk_size = 1000 - - while True: - executions = execution_model.objects.all().prefetch_related('run_as')[count:bulk_size] - if not executions: - break - count += len(executions) - updated = [] - for obj in executions: - run_as = obj.run_as - if not run_as: - continue - obj.account = run_as.username - updated.append(obj) - execution_model.objects.bulk_update(updated, ['account']) - - class Migration(migrations.Migration): - dependencies = [ ('ops', '0021_auto_20211130_1037'), ] operations = [ migrations.RemoveField( - model_name='adhoc', - name='run_system_user', + model_name='adhocexecution', + name='adhoc', ), - migrations.AddField( - model_name='commandexecution', - name='account', - field=models.CharField(default='', max_length=128, verbose_name='account'), + migrations.RemoveField( + model_name='adhocexecution', + name='task', ), - migrations.RunPython(migrate_run_system_user_to_account), migrations.RemoveField( model_name='commandexecution', - name='run_as', + name='hosts', ), + migrations.RemoveField( + model_name='commandexecution', + name='user', + ), + migrations.AlterUniqueTogether( + name='task', + unique_together=None, + ), + migrations.RemoveField( + model_name='task', + name='latest_adhoc', + ), + migrations.RemoveField( + model_name='task', + name='latest_execution', + ), + migrations.DeleteModel( + name='AdHoc', + ), + migrations.DeleteModel( + name='AdHocExecution', + ), + migrations.DeleteModel( + name='CommandExecution', + ), + migrations.DeleteModel( + name='Task', + ), + migrations.DeleteModel('CeleryTask'), ] diff --git a/apps/ops/migrations/0023_auto_20220912_0021.py b/apps/ops/migrations/0023_auto_20220912_0021.py new file mode 100644 index 000000000..edf8b0eeb --- /dev/null +++ b/apps/ops/migrations/0023_auto_20220912_0021.py @@ -0,0 +1,245 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import simple_history.models +import uuid + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('ops', '0022_auto_20220817_1346'), + ] + + operations = [ + migrations.CreateModel( + name='CeleryTask', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024, verbose_name='Name')), + ('date_last_publish', models.DateTimeField(null=True, verbose_name='Date last publish')), + ], + options={ + 'verbose_name': 'Celery Task', + 'ordering': ('name',), + 'permissions': [('view_taskmonitor', 'Can view task monitor')], + }, + ), + migrations.CreateModel( + name='CeleryTaskExecution', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=1024)), + ('args', models.JSONField(verbose_name='Args')), + ('kwargs', models.JSONField(verbose_name='Kwargs')), + ('state', models.CharField(max_length=16, verbose_name='State')), + ('is_finished', models.BooleanField(default=False, verbose_name='Finished')), + ('date_published', models.DateTimeField(auto_now_add=True, verbose_name='Date published')), + ('date_start', models.DateTimeField(null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ], + options={ + 'verbose_name': 'Celery Task Execution', + }, + ), + migrations.CreateModel( + name='Job', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('instant', models.BooleanField(default=False)), + ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, null=True, verbose_name='Module')), + ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), + ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), + ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', + max_length=128, verbose_name='Type')), + ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), + ('runas_policy', models.CharField( + choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), + ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), + ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), + ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('version', models.IntegerField(default=0)), + ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'verbose_name': 'Job', + 'ordering': ['date_created'], + }, + ), + migrations.CreateModel( + name='Playbook', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('path', models.FileField(upload_to='playbooks/')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='JobExecution', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('task_id', models.UUIDField(null=True)), + ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), + ('job_version', models.IntegerField(default=0)), + ('parameters', models.JSONField(default=dict, verbose_name='Parameters')), + ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), + ('summary', models.JSONField(default=dict, verbose_name='Summary')), + ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ('job', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', + to='ops.job')), + ], + options={ + 'verbose_name': 'Job Execution', + 'ordering': ['-date_created'], + }, + ), + migrations.AddField( + model_name='job', + name='playbook', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', + verbose_name='Playbook'), + ), + migrations.CreateModel( + name='HistoricalJob', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', + models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), + ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), + ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), + ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), + ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), + ('instant', models.BooleanField(default=False)), + ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, null=True, verbose_name='Module')), + ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), + ('timeout', models.IntegerField(default=-1, verbose_name='Timeout (Seconds)')), + ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', + max_length=128, verbose_name='Type')), + ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), + ('runas_policy', models.CharField( + choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), + ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), + ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), + ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('version', models.IntegerField(default=0)), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', + models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to=settings.AUTH_USER_MODEL, verbose_name='Creator')), + ('history_user', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', + to=settings.AUTH_USER_MODEL)), + ('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to='ops.playbook', verbose_name='Playbook')), + ], + options={ + 'verbose_name': 'historical Job', + 'verbose_name_plural': 'historical Jobs', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='AdHoc', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('org_id', + models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, verbose_name='Name')), + ('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')), + ('module', + models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], + default='shell', max_length=128, verbose_name='Module')), + ('args', models.CharField(default='', max_length=1024, verbose_name='Args')), + ('comment', + models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), + ('creator', + models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, + verbose_name='Creator')), + ], + options={ + 'verbose_name': 'AdHoc', + }, + ), + migrations.CreateModel( + name='JobAuditLog', + fields=[ + ], + options={ + 'verbose_name': 'Job audit log', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('ops.jobexecution',), + ), + ] diff --git a/apps/ops/migrations/0023_auto_20220929_2025.py b/apps/ops/migrations/0023_auto_20220929_2025.py deleted file mode 100644 index b5c7475f4..000000000 --- a/apps/ops/migrations/0023_auto_20220929_2025.py +++ /dev/null @@ -1,44 +0,0 @@ -# Generated by Django 3.2.14 on 2022-09-29 12:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0022_auto_20220817_1346'), - ] - - operations = [ - migrations.RemoveField( - model_name='celerytask', - name='log_path', - ), - migrations.RemoveField( - model_name='celerytask', - name='status', - ), - migrations.AddField( - model_name='celerytask', - name='args', - field=models.JSONField(default=[], verbose_name='Args'), - preserve_default=False, - ), - migrations.AddField( - model_name='celerytask', - name='is_finished', - field=models.BooleanField(default=False, verbose_name='Finished'), - ), - migrations.AddField( - model_name='celerytask', - name='kwargs', - field=models.JSONField(default={}, verbose_name='Kwargs'), - preserve_default=False, - ), - migrations.AddField( - model_name='celerytask', - name='state', - field=models.CharField(default='SUCCESS', max_length=16, verbose_name='State'), - preserve_default=False, - ), - ] diff --git a/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py new file mode 100644 index 000000000..9c09af231 --- /dev/null +++ b/apps/ops/migrations/0024_alter_celerytask_date_last_publish.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.16 on 2022-12-30 08:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ops', '0023_auto_20220912_0021'), + ] + + operations = [ + migrations.AlterField( + model_name='celerytask', + name='date_last_publish', + field=models.DateTimeField(null=True, verbose_name='Date last publish'), + ), + migrations.AlterField( + model_name='celerytaskexecution', + name='name', + field=models.CharField(max_length=1024, verbose_name='Name'), + ), + ] diff --git a/apps/ops/migrations/0024_auto_20221008_1514.py b/apps/ops/migrations/0024_auto_20221008_1514.py deleted file mode 100644 index e208af96e..000000000 --- a/apps/ops/migrations/0024_auto_20221008_1514.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-08 07:19 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0106_auto_20220916_1556'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0023_auto_20220929_2025'), - ] - - operations = [ - migrations.RemoveField( - model_name='adhocexecution', - name='adhoc', - ), - migrations.RemoveField( - model_name='adhocexecution', - name='task', - ), - migrations.RemoveField( - model_name='commandexecution', - name='hosts', - ), - migrations.RemoveField( - model_name='commandexecution', - name='user', - ), - migrations.AlterUniqueTogether( - name='task', - unique_together=None, - ), - migrations.RemoveField( - model_name='task', - name='latest_adhoc', - ), - migrations.RemoveField( - model_name='task', - name='latest_execution', - ), - migrations.DeleteModel( - name='AdHoc', - ), - migrations.DeleteModel( - name='AdHocExecution', - ), - migrations.DeleteModel( - name='CommandExecution', - ), - migrations.DeleteModel( - name='Task', - ), - ] diff --git a/apps/ops/migrations/0025_auto_20221008_1631.py b/apps/ops/migrations/0025_auto_20221008_1631.py deleted file mode 100644 index 7e814c3d1..000000000 --- a/apps/ops/migrations/0025_auto_20221008_1631.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-08 08:31 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - ('assets', '0106_auto_20220916_1556'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0024_auto_20221008_1514'), - ] - - operations = [ - migrations.CreateModel( - name='AdHoc', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False)), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), - ('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')), - ('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')), - ('pattern', models.CharField(default='all', max_length=1024, verbose_name='Pattern')), - ('module', models.CharField(default='shell', max_length=128, verbose_name='Module')), - ('args', models.CharField(default='', max_length=1024, verbose_name='Args')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='AdHocExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True)), - ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='executions', to='ops.adhoc', verbose_name='Adhoc')), - ], - options={ - 'verbose_name': 'AdHoc execution', - 'db_table': 'ops_adhoc_execution', - 'get_latest_by': 'date_start', - }, - ), - migrations.AddField( - model_name='adhoc', - name='last_execution', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.adhocexecution', verbose_name='Last execution'), - ), - migrations.AddField( - model_name='adhoc', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Creator'), - ), - ] diff --git a/apps/ops/migrations/0026_auto_20221009_2050.py b/apps/ops/migrations/0026_auto_20221009_2050.py deleted file mode 100644 index b9965c2bd..000000000 --- a/apps/ops/migrations/0026_auto_20221009_2050.py +++ /dev/null @@ -1,108 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-09 12:50 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0106_auto_20220916_1556'), - ('ops', '0025_auto_20221008_1631'), - ] - - operations = [ - migrations.CreateModel( - name='Playbook', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('is_periodic', models.BooleanField(default=False)), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('account', models.CharField(default='root', max_length=128, verbose_name='Account')), - ('account_policy', models.CharField(default='root', max_length=128, verbose_name='Account policy')), - ('date_last_run', models.DateTimeField(null=True, verbose_name='Date last run')), - ('path', models.FilePathField(max_length=1024, verbose_name='Playbook')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AlterField( - model_name='adhocexecution', - name='date_finished', - field=models.DateTimeField(null=True, verbose_name='Date finished'), - ), - migrations.CreateModel( - name='PlaybookTemplate', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('name', models.CharField(max_length=128, verbose_name='Name')), - ('path', models.FilePathField(verbose_name='Path')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ], - options={ - 'verbose_name': 'Playbook template', - 'ordering': ['name'], - 'unique_together': {('org_id', 'name')}, - }, - ), - migrations.CreateModel( - name='PlaybookExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('path', models.FilePathField(max_length=1024, verbose_name='Run dir')), - ('creator', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ops.playbook', - verbose_name='Task')), - ], - options={ - 'ordering': ['-date_start'], - 'abstract': False, - }, - ), - migrations.AddField( - model_name='playbook', - name='last_execution', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='ops.playbookexecution', verbose_name='Last execution'), - ), - migrations.AddField( - model_name='playbook', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, verbose_name='Owner'), - ), - migrations.AddField( - model_name='playbook', - name='template', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbooktemplate', - verbose_name='Template'), - ), - ] diff --git a/apps/ops/migrations/0027_auto_20221024_1709.py b/apps/ops/migrations/0027_auto_20221024_1709.py deleted file mode 100644 index 2ded6ca58..000000000 --- a/apps/ops/migrations/0027_auto_20221024_1709.py +++ /dev/null @@ -1,273 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 03:23 - -import uuid - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('assets', '0112_gateway_to_asset'), - ('ops', '0026_auto_20221009_2050'), - ] - - operations = [ - migrations.CreateModel( - name='CeleryTaskExecution', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=1024)), - ('args', models.JSONField(verbose_name='Args')), - ('kwargs', models.JSONField(verbose_name='Kwargs')), - ('state', models.CharField(max_length=16, verbose_name='State')), - ('is_finished', models.BooleanField(default=False, verbose_name='Finished')), - ('date_published', models.DateTimeField(auto_now_add=True, verbose_name='Date published')), - ('date_start', models.DateTimeField(null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ], - ), - migrations.CreateModel( - name='Job', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), - ('instant', models.BooleanField(default=False)), - ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), - ('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', - max_length=128, null=True, verbose_name='Module')), - ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), - ('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', - max_length=128, verbose_name='Type')), - ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), - ('runas_policy', models.CharField( - choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), - ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), - ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), - ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), - ('comment', - models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), - ('assets', models.ManyToManyField(to='assets.Asset', verbose_name='Assets')), - ('owner', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ], - options={ - 'ordering': ['date_created'], - }, - ), - migrations.CreateModel( - name='JobExecution', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('org_id', - models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('task_id', models.UUIDField(null=True)), - ('status', models.CharField(default='running', max_length=16, verbose_name='Status')), - ('parameters', models.JSONField(default=dict, verbose_name='Parameters')), - ('result', models.JSONField(blank=True, null=True, verbose_name='Result')), - ('summary', models.JSONField(default=dict, verbose_name='Summary')), - ('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')), - ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), - ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), - ('creator', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, - verbose_name='Creator')), - ('job', - models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='executions', - to='ops.job')), - ], - options={ - 'ordering': ['-date_created'], - }, - ), - migrations.RemoveField( - model_name='playbookexecution', - name='creator', - ), - migrations.RemoveField( - model_name='playbookexecution', - name='task', - ), - migrations.AlterUniqueTogether( - name='playbooktemplate', - unique_together=None, - ), - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',)}, - ), - migrations.RenameField( - model_name='adhoc', - old_name='owner', - new_name='creator', - ), - migrations.RenameField( - model_name='celerytask', - old_name='date_finished', - new_name='last_published_time', - ), - migrations.RemoveField( - model_name='adhoc', - name='account', - ), - migrations.RemoveField( - model_name='adhoc', - name='account_policy', - ), - migrations.RemoveField( - model_name='adhoc', - name='assets', - ), - migrations.RemoveField( - model_name='adhoc', - name='crontab', - ), - migrations.RemoveField( - model_name='adhoc', - name='date_last_run', - ), - migrations.RemoveField( - model_name='adhoc', - name='interval', - ), - migrations.RemoveField( - model_name='adhoc', - name='is_periodic', - ), - migrations.RemoveField( - model_name='adhoc', - name='last_execution', - ), - migrations.RemoveField( - model_name='celerytask', - name='args', - ), - migrations.RemoveField( - model_name='celerytask', - name='date_published', - ), - migrations.RemoveField( - model_name='celerytask', - name='date_start', - ), - migrations.RemoveField( - model_name='celerytask', - name='is_finished', - ), - migrations.RemoveField( - model_name='celerytask', - name='kwargs', - ), - migrations.RemoveField( - model_name='celerytask', - name='state', - ), - migrations.RemoveField( - model_name='playbook', - name='account', - ), - migrations.RemoveField( - model_name='playbook', - name='account_policy', - ), - migrations.RemoveField( - model_name='playbook', - name='assets', - ), - migrations.RemoveField( - model_name='playbook', - name='crontab', - ), - migrations.RemoveField( - model_name='playbook', - name='date_last_run', - ), - migrations.RemoveField( - model_name='playbook', - name='interval', - ), - migrations.RemoveField( - model_name='playbook', - name='is_periodic', - ), - migrations.RemoveField( - model_name='playbook', - name='last_execution', - ), - migrations.RemoveField( - model_name='playbook', - name='owner', - ), - migrations.RemoveField( - model_name='playbook', - name='template', - ), - migrations.AddField( - model_name='adhoc', - name='comment', - field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), - ), - migrations.AddField( - model_name='playbook', - name='creator', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, verbose_name='Creator'), - ), - migrations.AlterField( - model_name='adhoc', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', - max_length=128, verbose_name='Module'), - ), - migrations.AlterField( - model_name='celerytask', - name='name', - field=models.CharField(max_length=1024, verbose_name='Name'), - ), - migrations.AlterField( - model_name='playbook', - name='comment', - field=models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment'), - ), - migrations.AlterField( - model_name='playbook', - name='name', - field=models.CharField(max_length=128, null=True, verbose_name='Name'), - ), - migrations.AlterField( - model_name='playbook', - name='path', - field=models.FileField(upload_to='playbooks/'), - ), - migrations.DeleteModel( - name='AdHocExecution', - ), - migrations.DeleteModel( - name='PlaybookExecution', - ), - migrations.DeleteModel( - name='PlaybookTemplate', - ), - migrations.AddField( - model_name='job', - name='playbook', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ops.playbook', - verbose_name='Playbook'), - ), - ] diff --git a/apps/ops/migrations/0028_auto_20221205_1627.py b/apps/ops/migrations/0028_auto_20221205_1627.py deleted file mode 100644 index 5b04102a7..000000000 --- a/apps/ops/migrations/0028_auto_20221205_1627.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 08:27 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0027_auto_20221024_1709'), - ] - - operations = [ - migrations.RenameField( - model_name='job', - old_name='owner', - new_name='creator', - ), - migrations.RemoveField( - model_name='adhoc', - name='org_id', - ), - migrations.RemoveField( - model_name='job', - name='org_id', - ), - migrations.RemoveField( - model_name='jobexecution', - name='org_id', - ), - migrations.RemoveField( - model_name='playbook', - name='org_id', - ), - ] diff --git a/apps/ops/migrations/0029_auto_20221215_1712.py b/apps/ops/migrations/0029_auto_20221215_1712.py deleted file mode 100644 index b7dc3ea6d..000000000 --- a/apps/ops/migrations/0029_auto_20221215_1712.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-15 09:12 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0028_auto_20221205_1627'), - ] - - operations = [ - migrations.AddField( - model_name='adhoc', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='job', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='jobexecution', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - migrations.AddField( - model_name='playbook', - name='org_id', - field=models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization'), - ), - ] diff --git a/apps/ops/migrations/0030_auto_20221220_1941.py b/apps/ops/migrations/0030_auto_20221220_1941.py deleted file mode 100644 index cebbd8aa1..000000000 --- a/apps/ops/migrations/0030_auto_20221220_1941.py +++ /dev/null @@ -1,80 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:41 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion -import simple_history.models -import uuid - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('ops', '0029_auto_20221215_1712'), - ] - - operations = [ - migrations.CreateModel( - name='JobAuditLog', - fields=[ - ], - options={ - 'proxy': True, - 'indexes': [], - 'constraints': [], - }, - bases=('ops.jobexecution',), - ), - migrations.AddField( - model_name='job', - name='version', - field=models.IntegerField(default=0), - ), - migrations.AddField( - model_name='jobexecution', - name='job_version', - field=models.IntegerField(default=0), - ), - migrations.CreateModel( - name='HistoricalJob', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(blank=True, editable=False, verbose_name='Date updated')), - ('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')), - ('is_periodic', models.BooleanField(default=False, verbose_name='Periodic perform')), - ('interval', models.IntegerField(blank=True, default=24, null=True, verbose_name='Cycle perform')), - ('crontab', models.CharField(blank=True, max_length=128, null=True, verbose_name='Regularly perform')), - ('id', models.UUIDField(db_index=True, default=uuid.uuid4)), - ('name', models.CharField(max_length=128, null=True, verbose_name='Name')), - ('instant', models.BooleanField(default=False)), - ('args', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Args')), - ('module', models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell')], default='shell', max_length=128, null=True, verbose_name='Module')), - ('chdir', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Chdir')), - ('timeout', models.IntegerField(default=60, verbose_name='Timeout (Seconds)')), - ('type', models.CharField(choices=[('adhoc', 'Adhoc'), ('playbook', 'Playbook')], default='adhoc', max_length=128, verbose_name='Type')), - ('runas', models.CharField(default='root', max_length=128, verbose_name='Runas')), - ('runas_policy', models.CharField(choices=[('privileged_only', 'Privileged Only'), ('privileged_first', 'Privileged First'), ('skip', 'Skip')], default='skip', max_length=128, verbose_name='Runas policy')), - ('use_parameter_define', models.BooleanField(default=False, verbose_name='Use Parameter Define')), - ('parameters_define', models.JSONField(default=dict, verbose_name='Parameters define')), - ('comment', models.CharField(blank=True, default='', max_length=1024, null=True, verbose_name='Comment')), - ('version', models.IntegerField(default=0)), - ('history_id', models.AutoField(primary_key=True, serialize=False)), - ('history_date', models.DateTimeField(db_index=True)), - ('history_change_reason', models.CharField(max_length=100, null=True)), - ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), - ('creator', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Creator')), - ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), - ('playbook', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='ops.playbook', verbose_name='Playbook')), - ], - options={ - 'verbose_name': 'historical job', - 'verbose_name_plural': 'historical jobs', - 'ordering': ('-history_date', '-history_id'), - 'get_latest_by': ('history_date', 'history_id'), - }, - bases=(simple_history.models.HistoricalChanges, models.Model), - ), - ] diff --git a/apps/ops/migrations/0031_auto_20221220_1956.py b/apps/ops/migrations/0031_auto_20221220_1956.py deleted file mode 100644 index 9cbc21547..000000000 --- a/apps/ops/migrations/0031_auto_20221220_1956.py +++ /dev/null @@ -1,57 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('ops', '0030_auto_20221220_1941'), - ] - - operations = [ - migrations.AddField( - model_name='jobexecution', - name='comment', - field=models.TextField(blank=True, default='', verbose_name='Comment'), - ), - migrations.AlterField( - model_name='adhoc', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='adhoc', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='job', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='job', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='jobexecution', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='jobexecution', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='playbook', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='playbook', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - ] diff --git a/apps/ops/migrations/0032_auto_20221221_1513.py b/apps/ops/migrations/0032_auto_20221221_1513.py deleted file mode 100644 index 3a706f4b7..000000000 --- a/apps/ops/migrations/0032_auto_20221221_1513.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-21 07:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0031_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterField( - model_name='historicaljob', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='historicaljob', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - ] diff --git a/apps/ops/migrations/0033_auto_20221223_1536.py b/apps/ops/migrations/0033_auto_20221223_1536.py deleted file mode 100644 index f46955209..000000000 --- a/apps/ops/migrations/0033_auto_20221223_1536.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0032_auto_20221221_1513'), - ] - - operations = [ - migrations.AlterModelOptions( - name='adhoc', - options={'verbose_name': 'AdHoc'}, - ), - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',), 'verbose_name': 'Celery Task'}, - ), - migrations.AlterModelOptions( - name='celerytaskexecution', - options={'verbose_name': 'Celery Task Execution'}, - ), - migrations.AlterModelOptions( - name='historicaljob', - options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Job', 'verbose_name_plural': 'historical Jobs'}, - ), - migrations.AlterModelOptions( - name='job', - options={'ordering': ['date_created'], 'verbose_name': 'Job'}, - ), - migrations.AlterModelOptions( - name='jobauditlog', - options={'verbose_name': 'Job audit log'}, - ), - migrations.AlterModelOptions( - name='jobexecution', - options={'ordering': ['-date_created'], 'verbose_name': 'Job Execution'}, - ), - ] diff --git a/apps/ops/migrations/0034_alter_celerytask_options.py b/apps/ops/migrations/0034_alter_celerytask_options.py deleted file mode 100644 index 9645782c4..000000000 --- a/apps/ops/migrations/0034_alter_celerytask_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-27 06:07 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0033_auto_20221223_1536'), - ] - - operations = [ - migrations.AlterModelOptions( - name='celerytask', - options={'ordering': ('name',), 'permissions': [('view_taskmonitor', 'Can view task monitor')], 'verbose_name': 'Celery Task'}, - ), - ] diff --git a/apps/ops/migrations/0035_auto_20221227_1520.py b/apps/ops/migrations/0035_auto_20221227_1520.py deleted file mode 100644 index 36752a9bb..000000000 --- a/apps/ops/migrations/0035_auto_20221227_1520.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-27 07:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('ops', '0034_alter_celerytask_options'), - ] - - operations = [ - migrations.AlterField( - model_name='historicaljob', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), - ), - migrations.AlterField( - model_name='historicaljob', - name='timeout', - field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), - ), - migrations.AlterField( - model_name='job', - name='module', - field=models.CharField(choices=[('shell', 'Shell'), ('win_shell', 'Powershell'), ('python', 'Python')], default='shell', max_length=128, null=True, verbose_name='Module'), - ), - migrations.AlterField( - model_name='job', - name='timeout', - field=models.IntegerField(default=-1, verbose_name='Timeout (Seconds)'), - ), - ] diff --git a/apps/ops/models/adhoc.py b/apps/ops/models/adhoc.py index 4e8219ee1..b6c84183d 100644 --- a/apps/ops/models/adhoc.py +++ b/apps/ops/models/adhoc.py @@ -9,15 +9,14 @@ from common.utils import get_logger __all__ = ["AdHoc"] +from ops.const import Modules + from orgs.mixins.models import JMSOrgBaseModel logger = get_logger(__file__) class AdHoc(JMSOrgBaseModel): - class Modules(models.TextChoices): - shell = 'shell', _('Shell') - winshell = 'win_shell', _('Powershell') id = models.UUIDField(default=uuid.uuid4, primary_key=True) name = models.CharField(max_length=128, verbose_name=_('Name')) diff --git a/apps/ops/models/celery.py b/apps/ops/models/celery.py index 316eca876..e0f566ff1 100644 --- a/apps/ops/models/celery.py +++ b/apps/ops/models/celery.py @@ -13,7 +13,7 @@ from ops.celery import app class CeleryTask(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) name = models.CharField(max_length=1024, verbose_name=_('Name')) - last_published_time = models.DateTimeField(null=True) + date_last_publish = models.DateTimeField(null=True, verbose_name=_("Date last publish")) @property def meta(self): @@ -54,7 +54,7 @@ class CeleryTask(models.Model): class CeleryTaskExecution(models.Model): LOG_DIR = os.path.join(settings.PROJECT_DIR, 'data', 'celery') id = models.UUIDField(primary_key=True, default=uuid.uuid4) - name = models.CharField(max_length=1024) + name = models.CharField(max_length=1024, verbose_name=_('Name')) args = models.JSONField(verbose_name=_("Args")) kwargs = models.JSONField(verbose_name=_("Kwargs")) state = models.CharField(max_length=16, verbose_name=_("State")) diff --git a/apps/ops/models/job.py b/apps/ops/models/job.py index 85f960df1..894ff47d1 100644 --- a/apps/ops/models/job.py +++ b/apps/ops/models/job.py @@ -1,3 +1,4 @@ +import datetime import json import logging import os @@ -159,17 +160,29 @@ class JobExecution(JMSOrgBaseModel): @property def job_type(self): - return self.current_job.type + return Types[self.job.type].label def compile_shell(self): if self.current_job.type != 'adhoc': return - result = self.current_job.args - if self.current_job.chdir: - result += " chdir={}".format(self.current_job.chdir) + + module = self.current_job.module + # replace win_shell + if module == 'win_shell': + module = 'ansible.windows.win_shell' + if self.current_job.module in ['python']: - result += " executable={}".format(self.current_job.module) - return result + module = "shell" + + shell = self.current_job.args + if self.current_job.chdir: + if module == self.current_job.module: + shell += " path={}".format(self.current_job.chdir) + else: + shell += " chdir={}".format(self.current_job.chdir) + if self.current_job.module in ['python']: + shell += " executable={}".format(self.current_job.module) + return module, shell def get_runner(self): inv = self.current_job.inventory @@ -189,10 +202,8 @@ class JobExecution(JMSOrgBaseModel): extra_vars.update(static_variables) if self.current_job.type == 'adhoc': - args = self.compile_shell() - module = "shell" - if self.current_job.module not in ['python']: - module = self.current_job.module + + module, args = self.compile_shell() runner = AdHocRunner( self.inventory_path, @@ -226,9 +237,9 @@ class JobExecution(JMSOrgBaseModel): @property def time_cost(self): - if self.date_finished and self.date_start: + if self.is_finished: return (self.date_finished - self.date_start).total_seconds() - return None + return (timezone.now() - self.date_start).total_seconds() @property def timedelta(self): diff --git a/apps/ops/serializers/adhoc.py b/apps/ops/serializers/adhoc.py index 7db49fdcd..58cbaad98 100644 --- a/apps/ops/serializers/adhoc.py +++ b/apps/ops/serializers/adhoc.py @@ -3,17 +3,15 @@ from __future__ import unicode_literals from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AdHoc class AdHocSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) - row_count = serializers.IntegerField(read_only=True) - size = serializers.IntegerField(read_only=True) class Meta: model = AdHoc - read_only_field = ["id", "row_count", "size", "creator", "date_created", "date_updated"] + read_only_field = ["id", "creator", "date_created", "date_updated"] fields = read_only_field + ["id", "name", "module", "args", "comment"] diff --git a/apps/ops/serializers/celery.py b/apps/ops/serializers/celery.py index 776b1fd5b..12efdbea8 100644 --- a/apps/ops/serializers/celery.py +++ b/apps/ops/serializers/celery.py @@ -31,7 +31,7 @@ class CeleryPeriodTaskSerializer(serializers.ModelSerializer): class CeleryTaskSerializer(serializers.ModelSerializer): class Meta: model = CeleryTask - read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'last_published_time'] + read_only_fields = ['id', 'name', 'meta', 'summary', 'state', 'date_last_publish'] fields = read_only_fields diff --git a/apps/ops/serializers/job.py b/apps/ops/serializers/job.py index 4b6ea82d1..f5e4ee5d6 100644 --- a/apps/ops/serializers/job.py +++ b/apps/ops/serializers/job.py @@ -1,11 +1,11 @@ -from django.utils.translation import ugettext as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers from assets.models import Node -from common.drf.fields import ReadableHiddenField +from perms.utils.user_perm import UserPermAssetUtil +from common.serializers.fields import ReadableHiddenField from ops.mixin import PeriodTaskSerializerMixin from ops.models import Job, JobExecution -from ops.models.job import JobAuditLog from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -13,42 +13,54 @@ class JobSerializer(BulkOrgResourceModelSerializer, PeriodTaskSerializerMixin): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) run_after_save = serializers.BooleanField(label=_("Run after save"), default=False, required=False) nodes = serializers.ListField(required=False, child=serializers.CharField()) + date_last_run = serializers.DateTimeField(label=_('Date last run'), read_only=True) + + def get_request_user(self): + request = self.context.get('request') + user = request.user if request else None + return user def create(self, validated_data): assets = validated_data.__getitem__('assets') - node_ids = validated_data.pop('nodes') + node_ids = validated_data.pop('nodes', None) if node_ids: - nodes = Node.objects.filter(id__in=node_ids) - assets.extend( - Node.get_nodes_all_assets(*nodes).exclude(id__in=[asset.id for asset in assets])) + user = self.get_request_user() + perm_util = UserPermAssetUtil(user=user) + for node_id in node_ids: + node, node_assets = perm_util.get_node_all_assets(node_id) + assets.extend(node_assets.exclude(id__in=[asset.id for asset in assets])) return super().create(validated_data) class Meta: model = Job - read_only_fields = ["id", "date_last_run", "date_created", "date_updated", "average_time_cost"] + read_only_fields = [ + "id", "date_last_run", "date_created", "date_updated", "average_time_cost" + ] fields = read_only_fields + [ - "name", "instant", "type", "module", "args", "playbook", "assets", "runas_policy", "runas", "creator", - "use_parameter_define", - "parameters_define", - "timeout", - "chdir", - "comment", - "summary", - "is_periodic", "interval", "crontab", "run_after_save", "nodes" + "name", "instant", "type", "module", + "args", "playbook", "assets", + "runas_policy", "runas", "creator", + "use_parameter_define", "parameters_define", + "timeout", "chdir", "comment", "summary", + "is_periodic", "interval", "crontab", "nodes", + "run_after_save", ] class JobExecutionSerializer(BulkOrgResourceModelSerializer): creator = ReadableHiddenField(default=serializers.CurrentUserDefault()) job_type = serializers.ReadOnlyField(label=_("Job type")) - material = serializers.ReadOnlyField(label=_("Material")) + material = serializers.ReadOnlyField(label=_("Command")) + is_success = serializers.ReadOnlyField(label=_("Is success")) + is_finished = serializers.ReadOnlyField(label=_("Is finished")) + time_cost = serializers.ReadOnlyField(label=_("Time cost")) class Meta: model = JobExecution - read_only_fields = ["id", "task_id", "timedelta", "time_cost", 'is_finished', 'date_start', - 'date_finished', - 'date_created', - 'is_success', 'task_id', 'short_id', 'job_type', 'summary', 'material'] + read_only_fields = ["id", "task_id", "timedelta", "time_cost", + 'is_finished', 'date_start', 'date_finished', + 'date_created', 'is_success', 'task_id', 'job_type', + 'summary', 'material'] fields = read_only_fields + [ "job", "parameters", "creator" ] diff --git a/apps/ops/serializers/playbook.py b/apps/ops/serializers/playbook.py index bcfd75acd..a69bab3c9 100644 --- a/apps/ops/serializers/playbook.py +++ b/apps/ops/serializers/playbook.py @@ -2,7 +2,7 @@ import os from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from ops.models import Playbook from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/ops/signal_handlers.py b/apps/ops/signal_handlers.py index 29add5bcb..e72b88f25 100644 --- a/apps/ops/signal_handlers.py +++ b/apps/ops/signal_handlers.py @@ -105,4 +105,4 @@ def task_sent_handler(headers=None, body=None, **kwargs): 'kwargs': kwargs } CeleryTaskExecution.objects.create(**data) - CeleryTask.objects.filter(name=task).update(last_published_time=timezone.now()) + CeleryTask.objects.filter(name=task).update(date_last_publish=timezone.now()) diff --git a/apps/ops/tasks.py b/apps/ops/tasks.py index 19eb488e7..4ba139577 100644 --- a/apps/ops/tasks.py +++ b/apps/ops/tasks.py @@ -2,10 +2,9 @@ import os import subprocess -from django.conf import settings from celery import shared_task - from celery.exceptions import SoftTimeLimitExceeded +from django.conf import settings from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -32,6 +31,15 @@ def run_ops_job(job_id): run_ops_job_execution(execution) +# +# @shared_task(soft_time_limit=60, queue="ansible") +# def show_env(): +# import json +# print(os.environ) +# data = json.dumps(dict(os.environ), indent=4) +# return data + + @shared_task(soft_time_limit=60, queue="ansible", verbose_name=_("Run ansible task execution")) def run_ops_job_execution(execution_id, **kwargs): execution = get_object_or_none(JobExecution, id=execution_id) diff --git a/apps/ops/urls/api_urls.py b/apps/ops/urls/api_urls.py index 5d859b801..b0a4ccb14 100644 --- a/apps/ops/urls/api_urls.py +++ b/apps/ops/urls/api_urls.py @@ -25,7 +25,7 @@ router.register(r'task-executions', api.CeleryTaskExecutionViewSet, 'task-execut urlpatterns = [ path('variables/help/', api.JobRunVariableHelpAPIView.as_view(), name='variable-help'), path('job-execution/asset-detail/', api.JobAssetDetail.as_view(), name='asset-detail'), - path('job-execution/task-detail/', api.JobExecutionTaskDetail.as_view(), name='task-detail'), + path('job-execution/task-detail//', api.JobExecutionTaskDetail.as_view(), name='task-detail'), path('frequent-username/', api.FrequentUsernames.as_view(), name='frequent-usernames'), path('ansible/job-execution//log/', api.AnsibleTaskLogApi.as_view(), name='job-execution-log'), diff --git a/apps/ops/utils.py b/apps/ops/utils.py index e8e6cadca..c2fb7e643 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -13,61 +13,6 @@ from .const import DEFAULT_PASSWORD_RULES logger = get_logger(__file__) -DEFAULT_TASK_OPTIONS = { - 'timeout': 10, - 'forks': 10, -} - - -def get_task_by_id(task_id): - return get_object_or_none(Task, id=task_id) - - -@org_aware_func("hosts") -def update_or_create_ansible_task( - task_name, hosts, tasks, - interval=None, crontab=None, is_periodic=False, - callback=None, pattern='all', options=None, - run_as_admin=False, run_as=None, system_user=None, become_info=None, -): - if not hosts or not tasks or not task_name: - return None, None - if options is None: - options = DEFAULT_TASK_OPTIONS - defaults = { - 'name': task_name, - 'interval': interval, - 'crontab': crontab, - 'is_periodic': is_periodic, - 'callback': callback, - } - - created = False - task, ok = Task.objects.update_or_create( - defaults=defaults, name=task_name - ) - adhoc = task.get_latest_adhoc() - new_adhoc = AdHoc(task=task, pattern=pattern, - run_as_admin=run_as_admin, - run_as=run_as, run_system_user=system_user) - new_adhoc.tasks = tasks - new_adhoc.options = options - new_adhoc.become = become_info - - hosts_same = True - if adhoc: - old_hosts = set([str(asset.id) for asset in adhoc.hosts.all()]) - new_hosts = set([str(asset.id) for asset in hosts]) - hosts_same = old_hosts == new_hosts - - if not adhoc or not adhoc.same_with(new_adhoc) or not hosts_same: - logger.debug(_("Update task content: {}").format(task_name)) - new_adhoc.save() - new_adhoc.hosts.set(hosts) - task.latest_adhoc = new_adhoc - created = True - return task, created - def get_task_log_path(base_path, task_id, level=2): task_id = str(task_id) @@ -81,14 +26,3 @@ def get_task_log_path(base_path, task_id, level=2): make_dirs(os.path.dirname(path), exist_ok=True) return path - -def generate_random_password(**kwargs): - import random - import string - length = int(kwargs.get('length', DEFAULT_PASSWORD_RULES['length'])) - symbol_set = kwargs.get('symbol_set') - if symbol_set is None: - symbol_set = DEFAULT_PASSWORD_RULES['symbol_set'] - chars = string.ascii_letters + string.digits + symbol_set - password = ''.join([random.choice(chars) for _ in range(length)]) - return password diff --git a/apps/ops/views.py b/apps/ops/views.py index fee4a0b9f..bb07d8cac 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -3,7 +3,7 @@ from django.views.generic import TemplateView from django.conf import settings -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from rbac.permissions import RBACPermission __all__ = ['CeleryTaskLogView'] diff --git a/apps/ops/ws.py b/apps/ops/ws.py index 5c261f76f..c2386c77a 100644 --- a/apps/ops/ws.py +++ b/apps/ops/ws.py @@ -8,6 +8,7 @@ from common.db.utils import close_old_connections from common.utils import get_logger from .ansible.utils import get_ansible_task_log_path from .celery.utils import get_celery_task_log_path +from .const import CELERY_LOG_MAGIC_MARK logger = get_logger(__name__) @@ -52,7 +53,6 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer): await self.send_json({'message': '\r\n'}) try: logger.debug('Task log path: {}'.format(log_path)) - task_end_mark = [] async with aiofiles.open(log_path, 'rb') as task_log_f: while not self.disconnected: data = await task_log_f.read(4096) @@ -61,18 +61,15 @@ class TaskLogWebsocket(AsyncJsonWebsocketConsumer): await self.send_json( {'message': data.decode(errors='ignore'), 'task': task_id} ) - if data.find(b'succeeded in') != -1: - task_end_mark.append(1) - if data.find(bytes(task_id, 'utf8')) != -1: - task_end_mark.append(1) - elif len(task_end_mark) == 2: - logger.debug('Task log end: {}'.format(task_id)) - await self.send_json({'event': 'end', 'task': task_id}) - break + if data.find(CELERY_LOG_MAGIC_MARK) != -1: + await self.send_json( + {'event': 'end', 'task': task_id, 'message': ''} + ) + logger.debug("Task log file magic mark found") + break await asyncio.sleep(0.2) except OSError as e: logger.warn('Task log path open failed: {}'.format(e)) - # await self.close() async def disconnect(self, close_code): self.disconnected = True diff --git a/apps/orgs/api.py b/apps/orgs/api.py index e18ddf55e..0452f2bff 100644 --- a/apps/orgs/api.py +++ b/apps/orgs/api.py @@ -7,28 +7,25 @@ from rest_framework_bulk import BulkModelViewSet from rest_framework.generics import RetrieveAPIView from rest_framework.exceptions import PermissionDenied +from common.utils import get_logger from common.permissions import IsValidUser +from users.models import User, UserGroup +from assets.models import ( + Asset, Domain, Label, Node, +) +from perms.models import AssetPermission +from orgs.utils import current_org, tmp_to_root_org from .models import Organization from .serializers import ( OrgSerializer, CurrentOrgSerializer ) -from users.models import User, UserGroup -from assets.models import ( - Asset, Domain, Label, Node, - CommandFilter, CommandFilterRule -) -from perms.models import AssetPermission -from orgs.utils import current_org, tmp_to_root_org -from common.utils import get_logger - logger = get_logger(__file__) - # 部分 org 相关的 model,需要清空这些数据之后才能删除该组织 org_related_models = [ User, UserGroup, Asset, Label, Domain, Node, Label, - CommandFilter, CommandFilterRule, AssetPermission, + AssetPermission, ] @@ -38,7 +35,7 @@ class OrgViewSet(BulkModelViewSet): queryset = Organization.objects.all() serializer_class = OrgSerializer ordering_fields = ('name',) - ordering = ('name', ) + ordering = ('name',) def get_serializer_class(self): mapper = { @@ -68,7 +65,8 @@ class OrgViewSet(BulkModelViewSet): if str(instance.id) == settings.AUTH_LDAP_SYNC_ORG_ID: msg = _( - 'LDAP synchronization is set to the current organization. Please switch to another organization before deleting' + 'LDAP synchronization is set to the current organization. ' + 'Please switch to another organization before deleting' ) raise PermissionDenied(detail=msg) diff --git a/apps/orgs/caches.py b/apps/orgs/caches.py index cd67984a3..1eb54e037 100644 --- a/apps/orgs/caches.py +++ b/apps/orgs/caches.py @@ -7,7 +7,8 @@ from common.cache import Cache, IntegerField from common.utils import get_logger from common.utils.timezone import local_zero_hour, local_monday from users.models import UserGroup, User -from assets.models import Node, Domain, Asset, Account +from assets.models import Node, Domain, Asset +from accounts.models import Account from terminal.models import Session from perms.models import AssetPermission diff --git a/apps/orgs/mixins/api.py b/apps/orgs/mixins/api.py index 7d9839a20..a0a5daddd 100644 --- a/apps/orgs/mixins/api.py +++ b/apps/orgs/mixins/api.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- # +from django.db.models import QuerySet from rest_framework.viewsets import ModelViewSet, GenericViewSet from rest_framework_bulk import BulkModelViewSet -from common.mixins import CommonApiMixin, RelationMixin +from common.api import CommonApiMixin, RelationMixin from orgs.utils import current_org - from ..utils import set_to_root_org __all__ = [ @@ -21,6 +21,10 @@ class RootOrgViewMixin: class OrgQuerySetMixin: + queryset: QuerySet + get_serializer_class: callable + action: str + def get_queryset(self): if hasattr(self, 'model'): queryset = self.model.objects.all() @@ -32,11 +36,7 @@ class OrgQuerySetMixin: queryset = super().get_queryset() if hasattr(self, 'swagger_fake_view'): - return queryset[:1] - if hasattr(self, 'action') and self.action == 'list': - serializer_class = self.get_serializer_class() - if serializer_class and hasattr(serializer_class, 'setup_eager_loading'): - queryset = serializer_class.setup_eager_loading(queryset) + return queryset.none() return queryset diff --git a/apps/orgs/mixins/serializers.py b/apps/orgs/mixins/serializers.py index 8f70814bc..7ff66cf59 100644 --- a/apps/orgs/mixins/serializers.py +++ b/apps/orgs/mixins/serializers.py @@ -5,17 +5,17 @@ from rest_framework import serializers from rest_framework.validators import UniqueTogetherValidator from common.validators import ProjectUniqueValidator -from common.drf.serializers import BulkSerializerMixin, CommonSerializerMixin +from common.serializers import BulkSerializerMixin, CommonSerializerMixin, CommonModelSerializer, \ + CommonBulkModelSerializer from ..utils import get_current_org_id_for_serializer - __all__ = [ - "OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin", - "BulkOrgResourceModelSerializer", "OrgResourceModelSerializerMixin", + "OrgResourceSerializerMixin", "BulkOrgResourceModelSerializer", + "OrgResourceModelSerializerMixin", ] -class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): +class OrgResourceSerializerMixin(serializers.Serializer): """ 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id (同时为serializer.is_valid()对Model的unique_together校验做准备) @@ -43,13 +43,9 @@ class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer): return fields -class OrgResourceModelSerializerMixin(OrgResourceSerializerMixin, serializers.ModelSerializer): +class OrgResourceModelSerializerMixin(OrgResourceSerializerMixin, CommonModelSerializer): pass -class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerMixin): - pass - - -class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer): +class BulkOrgResourceModelSerializer(OrgResourceSerializerMixin, CommonBulkModelSerializer): pass diff --git a/apps/orgs/models.py b/apps/orgs/models.py index be88ec242..ba4669cce 100644 --- a/apps/orgs/models.py +++ b/apps/orgs/models.py @@ -3,7 +3,9 @@ from django.utils.translation import ugettext_lazy as _ from common.db.models import JMSBaseModel from common.tree import TreeNode -from common.utils import lazyproperty, settings +from common.utils import lazyproperty, settings, get_logger + +logger = get_logger(__name__) class OrgRoleMixin: @@ -126,7 +128,6 @@ class Organization(OrgRoleMixin, JMSBaseModel): @classmethod def expire_orgs_mapping(cls): - print("Expire orgs mapping: ") cls.orgs_mapping = None def org_id(self): @@ -141,6 +142,15 @@ class Organization(OrgRoleMixin, JMSBaseModel): obj.save() return obj + @classmethod + def system(cls): + defaults = dict(id=cls.SYSTEM_ID, name=cls.SYSTEM_NAME) + obj, created = cls.objects.get_or_create(defaults=defaults, id=cls.SYSTEM_ID) + if not obj.builtin: + obj.builtin = True + obj.save() + return obj + @classmethod def root(cls): name = settings.GLOBAL_ORG_DISPLAY_NAME or cls.ROOT_NAME diff --git a/apps/orgs/signal_handlers/cache.py b/apps/orgs/signal_handlers/cache.py index 505422377..0acebf6b9 100644 --- a/apps/orgs/signal_handlers/cache.py +++ b/apps/orgs/signal_handlers/cache.py @@ -2,7 +2,8 @@ from django.db.models.signals import post_save, pre_delete, pre_save, post_delet from django.dispatch import receiver from orgs.models import Organization -from assets.models import Node, Account +from assets.models import Node +from accounts.models import Account from perms.models import AssetPermission from audits.models import UserLoginLog from users.models import UserGroup, User diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 38cd0e764..6813e9f0b 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -48,6 +48,10 @@ def set_to_root_org(): set_current_org(Organization.root()) +def set_to_system_org(): + set_current_org(Organization.system()) + + def _find(attr): return getattr(thread_local, attr, None) @@ -114,6 +118,7 @@ def filter_org_queryset(queryset): else: kwargs = {'org_id': org.id} + # import traceback # lines = traceback.format_stack() # print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>") # for line in lines[-10:-1]: diff --git a/apps/perms/api/asset_permission_relation.py b/apps/perms/api/asset_permission_relation.py index 5b85cb971..f2bd3cf30 100644 --- a/apps/perms/api/asset_permission_relation.py +++ b/apps/perms/api/asset_permission_relation.py @@ -10,7 +10,7 @@ from orgs.utils import current_org from perms import serializers from perms import models from perms.utils import AssetPermissionPermAssetUtil -from assets.serializers import AccountSerializer +from accounts.serializers import AccountSerializer __all__ = [ 'AssetPermissionUserRelationViewSet', 'AssetPermissionUserGroupRelationViewSet', @@ -123,5 +123,3 @@ class AssetPermissionAccountListApi(generics.ListAPIView): perm = get_object_or_404(models.AssetPermission, pk=pk) accounts = perm.get_all_accounts() return accounts - - diff --git a/apps/perms/api/user_permission/tree/asset.py b/apps/perms/api/user_permission/tree/asset.py index 081dbc756..c620356ad 100644 --- a/apps/perms/api/user_permission/tree/asset.py +++ b/apps/perms/api/user_permission/tree/asset.py @@ -5,14 +5,14 @@ from assets.models import Asset from assets.api import SerializeToTreeNodeMixin from common.utils import get_logger -from ..assets import UserDirectPermedAssetsApi +from ..assets import UserAllPermedAssetsApi from .mixin import RebuildTreeMixin logger = get_logger(__name__) __all__ = [ - 'UserDirectPermedAssetsAsTreeApi', + 'UserAllPermedAssetsAsTreeApi', 'UserUngroupAssetsAsTreeApi', ] @@ -35,12 +35,12 @@ class AssetTreeMixin(RebuildTreeMixin, SerializeToTreeNodeMixin): return Response(data=data) -class UserDirectPermedAssetsAsTreeApi(AssetTreeMixin, UserDirectPermedAssetsApi): +class UserAllPermedAssetsAsTreeApi(AssetTreeMixin, UserAllPermedAssetsApi): """ 用户 '直接授权的资产' 作为树 """ pass -class UserUngroupAssetsAsTreeApi(UserDirectPermedAssetsAsTreeApi): +class UserUngroupAssetsAsTreeApi(UserAllPermedAssetsAsTreeApi): """ 用户 '未分组节点的资产(直接授权的资产)' 作为树 """ def get_assets(self): if settings.PERM_SINGLE_ASSET_TO_UNGROUP_NODE: diff --git a/apps/perms/api/user_permission/tree/mixin.py b/apps/perms/api/user_permission/tree/mixin.py index c0cafe37c..46714ce05 100644 --- a/apps/perms/api/user_permission/tree/mixin.py +++ b/apps/perms/api/user_permission/tree/mixin.py @@ -2,7 +2,7 @@ from django.core.cache import cache from rest_framework.request import Request -from common.http import is_true +from common.utils.http import is_true from common.utils import lazyproperty from perms.utils import UserPermTreeRefreshUtil from users.models import User @@ -36,5 +36,5 @@ class RebuildTreeMixin: cache.delete(force_cache_key) else: force = False - cache.set(force_cache_key, count+1, force_timeout) + cache.set(force_cache_key, count + 1, force_timeout) return force diff --git a/apps/perms/api/user_permission/tree/node_with_asset.py b/apps/perms/api/user_permission/tree/node_with_asset.py index 9aa73963b..576a5615d 100644 --- a/apps/perms/api/user_permission/tree/node_with_asset.py +++ b/apps/perms/api/user_permission/tree/node_with_asset.py @@ -10,9 +10,10 @@ from rest_framework.generics import get_object_or_404 from rest_framework.exceptions import PermissionDenied, NotFound from assets.utils import KubernetesTree -from assets.models import Asset, Account -from assets.const import AliasAccount +from assets.models import Asset +from accounts.const import AliasAccount from assets.api import SerializeToTreeNodeMixin +from accounts.models import Account from authentication.models import ConnectionToken from common.utils import get_object_or_none, lazyproperty from common.utils.common import timeit @@ -151,13 +152,13 @@ class UserGrantedK8sAsTreeApi(SelfOrPKUserMixin, ListAPIView): def get_account_secret(self, token: ConnectionToken): util = PermAccountUtil() accounts = util.get_permed_accounts_for_user(self.user, token.asset) - account_username = token.account - accounts = filter(lambda x: x.username == account_username, accounts) + account_name = token.account + accounts = filter(lambda x: x.name == account_name, accounts) accounts = list(accounts) if not accounts: raise NotFound('Account is not found') account = accounts[0] - if account.username in [ + if account.name in [ AliasAccount.INPUT, AliasAccount.USER ]: return token.input_secret diff --git a/apps/perms/filters.py b/apps/perms/filters.py index 8743c38be..56382cf2d 100644 --- a/apps/perms/filters.py +++ b/apps/perms/filters.py @@ -2,7 +2,7 @@ from django_filters import rest_framework as filters from django.db.models import QuerySet, Q from common.drf.filters import BaseFilterSet -from common.utils import get_object_or_none +from common.utils import get_object_or_none, is_uuid from users.models import User, UserGroup from assets.models import Node, Asset from perms.models import AssetPermission @@ -91,7 +91,7 @@ class PermissionBaseFilter(BaseFilterSet): class AssetPermissionFilter(PermissionBaseFilter): is_effective = filters.BooleanFilter(method='do_nothing') node_id = filters.UUIDFilter(method='do_nothing') - node = filters.CharFilter(method='do_nothing') + node_name = filters.CharFilter(method='do_nothing') asset_id = filters.UUIDFilter(method='do_nothing') asset_name = filters.CharFilter(method='do_nothing') ip = filters.CharFilter(method='do_nothing') @@ -100,8 +100,9 @@ class AssetPermissionFilter(PermissionBaseFilter): model = AssetPermission fields = ( 'user_id', 'username', 'user_group_id', - 'user_group', 'node_id', 'node', 'asset_id', 'name', 'ip', 'name', - 'all', 'asset_id', 'is_valid', 'is_effective', 'from_ticket' + 'user_group', 'node_id', 'node_name', 'asset_id', 'asset_name', + 'name', 'ip', 'name', + 'all', 'is_valid', 'is_effective', 'from_ticket' ) @property @@ -116,7 +117,7 @@ class AssetPermissionFilter(PermissionBaseFilter): def filter_node(self, queryset: QuerySet): is_query_all = self.get_query_param('all', True) node_id = self.get_query_param('node_id') - node_name = self.get_query_param('node') + node_name = self.get_query_param('node_name') if node_id: _nodes = Node.objects.filter(pk=node_id) elif node_name: diff --git a/apps/perms/hands.py b/apps/perms/hands.py index dabb9f7c0..dd6372ed9 100644 --- a/apps/perms/hands.py +++ b/apps/perms/hands.py @@ -2,12 +2,11 @@ # from users.models import User, UserGroup -from assets.models import Asset, Node, Label, FavoriteAsset, Account +from assets.models import Asset, Node, Label, FavoriteAsset from assets.serializers import NodeSerializer __all__ = [ 'User', 'UserGroup', 'Asset', 'Node', 'Label', 'FavoriteAsset', - 'NodeSerializer', 'Account' + 'NodeSerializer', ] - diff --git a/apps/perms/migrations/0032_auto_20221111_1919.py b/apps/perms/migrations/0032_auto_20221111_1919.py index 3f3c56533..6ab787df0 100644 --- a/apps/perms/migrations/0032_auto_20221111_1919.py +++ b/apps/perms/migrations/0032_auto_20221111_1919.py @@ -4,9 +4,9 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('assets', '0111_alter_automationexecution_status'), + ('assets', '0101_auto_20220811_1511'), + ('accounts', '0001_initial'), ('perms', '0031_auto_20220816_1600'), ] @@ -21,11 +21,11 @@ class Migration(migrations.Migration): 'indexes': [], 'constraints': [], }, - bases=('assets.account',), + bases=('accounts.account',), ), migrations.AlterField( model_name='assetpermission', name='actions', - field=models.IntegerField(default=0, verbose_name='Actions'), + field=models.IntegerField(default=1, verbose_name='Actions'), ), ] diff --git a/apps/perms/migrations/0033_alter_assetpermission_actions.py b/apps/perms/migrations/0033_alter_assetpermission_actions.py deleted file mode 100644 index cfa39f6e3..000000000 --- a/apps/perms/migrations/0033_alter_assetpermission_actions.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-18 02:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('perms', '0032_auto_20221111_1919'), - ] - - operations = [ - migrations.AlterField( - model_name='assetpermission', - name='actions', - field=models.IntegerField(default=1, verbose_name='Actions'), - ), - ] diff --git a/apps/perms/migrations/0034_auto_20221220_1956.py b/apps/perms/migrations/0033_auto_20221220_1956.py similarity index 96% rename from apps/perms/migrations/0034_auto_20221220_1956.py rename to apps/perms/migrations/0033_auto_20221220_1956.py index 41f6528d1..1c10257d2 100644 --- a/apps/perms/migrations/0034_auto_20221220_1956.py +++ b/apps/perms/migrations/0033_auto_20221220_1956.py @@ -5,9 +5,8 @@ import uuid class Migration(migrations.Migration): - dependencies = [ - ('perms', '0033_alter_assetpermission_actions'), + ('perms', '0032_auto_20221111_1919'), ] operations = [ diff --git a/apps/perms/models/asset_permission.py b/apps/perms/models/asset_permission.py index 2af68832c..1f14c86fa 100644 --- a/apps/perms/models/asset_permission.py +++ b/apps/perms/models/asset_permission.py @@ -6,14 +6,14 @@ from django.utils import timezone from django.utils.translation import ugettext_lazy as _ from users.models import User -from assets.models import Asset, Account +from assets.models import Asset +from accounts.models import Account from orgs.mixins.models import JMSOrgBaseModel from orgs.mixins.models import OrgManager from common.utils import date_expired_default from common.utils.timezone import local_now - from perms.const import ActionChoices -from assets.const import AliasAccount +from accounts.const import AliasAccount __all__ = ['AssetPermission', 'ActionChoices'] diff --git a/apps/perms/models/perm_node.py b/apps/perms/models/perm_node.py index 1db0f2fe7..e51851db9 100644 --- a/apps/perms/models/perm_node.py +++ b/apps/perms/models/perm_node.py @@ -2,7 +2,8 @@ from django.db import models from django.db.models import F, TextChoices from django.utils.translation import ugettext_lazy as _ -from assets.models import Asset, Node, FamilyMixin, Account +from assets.models import Asset, Node, FamilyMixin +from accounts.models import Account from common.utils import lazyproperty from orgs.mixins.models import JMSOrgBaseModel diff --git a/apps/perms/serializers/permission.py b/apps/perms/serializers/permission.py index 673b567c5..63ba3a765 100644 --- a/apps/perms/serializers/permission.py +++ b/apps/perms/serializers/permission.py @@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.models import Asset, Node -from common.drf.fields import BitChoicesField, ObjectRelatedField +from common.serializers.fields import BitChoicesField, ObjectRelatedField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from perms.models import ActionChoices, AssetPermission from users.models import User, UserGroup @@ -34,26 +34,27 @@ class AssetPermissionSerializer(BulkOrgResourceModelSerializer): class Meta: model = AssetPermission fields_mini = ["id", "name"] - fields_small = fields_mini + [ + fields_generic = [ "accounts", - "is_active", - "is_expired", - "is_valid", "actions", "created_by", "date_created", - "date_expired", "date_start", + "date_expired", + "is_active", + "is_expired", + "is_valid", "comment", "from_ticket", ] + fields_small = fields_mini + fields_generic fields_m2m = [ "users", "user_groups", "assets", "nodes", ] - fields = fields_small + fields_m2m + fields = fields_mini + fields_m2m + fields_generic read_only_fields = ["created_by", "date_created", "from_ticket"] extra_kwargs = { "actions": {"label": _("Actions")}, diff --git a/apps/perms/serializers/permission_relation.py b/apps/perms/serializers/permission_relation.py index 3e469106a..c91726a14 100644 --- a/apps/perms/serializers/permission_relation.py +++ b/apps/perms/serializers/permission_relation.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from common.drf.serializers import BulkSerializerMixin +from common.serializers import BulkSerializerMixin from perms.models import AssetPermission __all__ = [ diff --git a/apps/perms/serializers/user_permission.py b/apps/perms/serializers/user_permission.py index 9b4ad26f0..8e22623fc 100644 --- a/apps/perms/serializers/user_permission.py +++ b/apps/perms/serializers/user_permission.py @@ -5,10 +5,12 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.const import Category, AllTypes -from assets.models import Node, Asset, Platform, Account +from assets.models import Node, Asset, Platform +from accounts.models import Account from assets.serializers.asset.common import AssetProtocolsSerializer -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField from perms.serializers.permission import ActionChoicesField +from orgs.mixins.serializers import OrgResourceModelSerializerMixin __all__ = [ 'NodePermedSerializer', 'AssetPermedSerializer', @@ -16,12 +18,13 @@ __all__ = [ ] -class AssetPermedSerializer(serializers.ModelSerializer): +class AssetPermedSerializer(OrgResourceModelSerializerMixin): """ 被授权资产的数据结构 """ platform = ObjectRelatedField(required=False, queryset=Platform.objects, label=_('Platform')) protocols = AssetProtocolsSerializer(many=True, required=False, label=_('Protocols')) category = LabeledChoiceField(choices=Category.choices, read_only=True, label=_('Category')) type = LabeledChoiceField(choices=AllTypes.choices(), read_only=True, label=_('Type')) + domain = ObjectRelatedField(required=False, queryset=Node.objects, label=_('Domain')) class Meta: model = Asset diff --git a/apps/perms/urls/user_permission.py b/apps/perms/urls/user_permission.py index 63ed4f636..ba172747c 100644 --- a/apps/perms/urls/user_permission.py +++ b/apps/perms/urls/user_permission.py @@ -21,7 +21,7 @@ user_permission_urlpatterns = [ name='user-node-children'), # tree-asset - path('/assets/tree/', api.UserDirectPermedAssetsAsTreeApi.as_view(), + path('/assets/tree/', api.UserAllPermedAssetsAsTreeApi.as_view(), name='user-direct-assets-as-tree'), path('/ungroup/assets/tree/', api.UserUngroupAssetsAsTreeApi.as_view(), name='user-ungroup-assets-as-tree'), diff --git a/apps/perms/utils/account.py b/apps/perms/utils/account.py index bdec2b934..5973ea4d7 100644 --- a/apps/perms/utils/account.py +++ b/apps/perms/utils/account.py @@ -1,7 +1,7 @@ from collections import defaultdict -from assets.models import Account -from assets.const import AliasAccount +from accounts.models import Account +from accounts.const import AliasAccount from .permission import AssetPermissionUtil __all__ = ['PermAccountUtil'] diff --git a/apps/perms/utils/permission.py b/apps/perms/utils/permission.py index bbec44343..d3d1562c0 100644 --- a/apps/perms/utils/permission.py +++ b/apps/perms/utils/permission.py @@ -25,9 +25,9 @@ class AssetPermissionUtil(object): groups = user.groups.all() group_perm_ids = self.get_permissions_for_user_groups(groups, flat=True) perm_ids.update(group_perm_ids) - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_user_groups(self, user_groups, flat=False): @@ -39,9 +39,9 @@ class AssetPermissionUtil(object): perm_ids = AssetPermission.user_groups.through.objects \ .filter(usergroup_id__in=group_ids) \ .values_list('assetpermission_id', flat=True).distinct() - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_assets(self, assets, with_node=True, flat=False): @@ -56,9 +56,9 @@ class AssetPermissionUtil(object): nodes = Asset.get_all_nodes_for_assets(assets) node_perm_ids = self.get_permissions_for_nodes(nodes, flat=True) perm_ids.update(node_perm_ids) - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_nodes(self, nodes, with_ancestor=False, flat=False): @@ -69,9 +69,9 @@ class AssetPermissionUtil(object): node_ids = nodes.values_list('id', flat=True).distinct() relations = AssetPermission.nodes.through.objects.filter(node_id__in=node_ids) perm_ids = relations.values_list('assetpermission_id', flat=True).distinct() - if flat: - return perm_ids perms = self.get_permissions(ids=perm_ids) + if flat: + return perms.values_list('id', flat=True) return perms def get_permissions_for_user_asset(self, user, asset): @@ -103,5 +103,5 @@ class AssetPermissionUtil(object): @staticmethod def get_permissions(ids): - perms = AssetPermission.objects.filter(id__in=ids).order_by('-date_expired') + perms = AssetPermission.objects.filter(id__in=ids).valid().order_by('-date_expired') return perms diff --git a/apps/perms/utils/user_perm.py b/apps/perms/utils/user_perm.py index ceea59b22..d7a3ea06a 100644 --- a/apps/perms/utils/user_perm.py +++ b/apps/perms/utils/user_perm.py @@ -82,7 +82,7 @@ class UserPermAssetUtil(AssetPermissionPermAssetUtil): node = PermNode.objects.get(id=node_id) node.compute_node_from_and_assets_amount(self.user) if node.node_from == node.NodeFrom.granted: - assets = PermNode.get_nodes_all_assets() + assets = PermNode.get_nodes_all_assets(node) elif node.node_from in (node.NodeFrom.asset, node.NodeFrom.child): node.assets_amount = node.granted_assets_amount assets = self._get_indirect_perm_node_all_assets(node) diff --git a/apps/rbac/api/permission.py b/apps/rbac/api/permission.py index 722deb556..9500af3ac 100644 --- a/apps/rbac/api/permission.py +++ b/apps/rbac/api/permission.py @@ -3,7 +3,7 @@ from rest_framework.decorators import action from django.shortcuts import get_object_or_404 from common.tree import TreeNodeSerializer -from common.drf.api import JMSModelViewSet +from common.api import JMSModelViewSet from ..models import Permission, Role from ..serializers import PermissionSerializer diff --git a/apps/rbac/api/role.py b/apps/rbac/api/role.py index be8274e91..0a4d52706 100644 --- a/apps/rbac/api/role.py +++ b/apps/rbac/api/role.py @@ -3,8 +3,8 @@ from django.utils.translation import ugettext as _ from rest_framework.exceptions import PermissionDenied from rest_framework.decorators import action -from common.drf.api import JMSModelViewSet -from common.mixins.api import PaginatedResponseMixin +from common.api import JMSModelViewSet +from common.api import PaginatedResponseMixin from ..filters import RoleFilter from ..serializers import RoleSerializer, RoleUserSerializer from ..models import Role, SystemRole, OrgRole @@ -117,4 +117,3 @@ class OrgRolePermissionsViewSet(BaseRolePermissionsViewSet): rbac_perms = ( ('get_tree', 'rbac.view_permission'), ) - diff --git a/apps/rbac/backends.py b/apps/rbac/backends.py index 76ebd1d70..aac44da36 100644 --- a/apps/rbac/backends.py +++ b/apps/rbac/backends.py @@ -20,7 +20,12 @@ class RBACBackend(JMSBaseAuthBackend): raise PermissionDenied() if perm == '*': return True - perm_set = set(i.strip() for i in perm.split('|')) + if isinstance(perm, str): + perm_set = set(i.strip() for i in perm.split('|')) + elif isinstance(perm, (list, tuple, set)): + perm_set = set(perm) + else: + raise ValueError('perm must be str, list, tuple or set') has_perm = bool(perm_set & set(user_obj.perms)) if not has_perm: raise PermissionDenied() diff --git a/apps/rbac/builtin.py b/apps/rbac/builtin.py index 08e54374c..c3f748a27 100644 --- a/apps/rbac/builtin.py +++ b/apps/rbac/builtin.py @@ -19,7 +19,6 @@ user_perms = ( ('assets', 'systemuser', 'match', 'systemuser'), ('assets', 'node', 'match', 'node'), ('applications', 'application', 'match', 'application'), - ('ops', 'commandexecution', 'add', 'commandexecution'), ) system_user_perms = ( @@ -36,7 +35,6 @@ _auditor_perms = ( ('terminal', 'sessionreplay', 'view,download', 'sessionreplay'), ('terminal', 'session', '*', '*'), ('terminal', 'command', '*', '*'), - ('ops', 'commandexecution', 'view', 'commandexecution'), ) auditor_perms = user_perms + _auditor_perms diff --git a/apps/rbac/const.py b/apps/rbac/const.py index b81ec1bf9..bbdfe1f28 100644 --- a/apps/rbac/const.py +++ b/apps/rbac/const.py @@ -30,33 +30,32 @@ exclude_permissions = ( ('assets', 'adminuser', '*', '*'), ('assets', 'assetgroup', '*', '*'), ('assets', 'cluster', '*', '*'), + ('assets', 'systemuser', '*', '*'), ('assets', 'favoriteasset', '*', '*'), - ('assets', 'historicalaccount', '*', '*'), ('assets', 'assetuser', '*', '*'), - ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), - ('assets', 'accountbackupplanexecution', 'delete,change', 'accountbackupplanexecution'), - ('assets', 'gathereduser', 'add,delete,change', 'gathereduser'), ('assets', 'web', '*', '*'), ('assets', 'host', '*', '*'), ('assets', 'cloud', '*', '*'), ('assets', 'device', '*', '*'), ('assets', 'database', '*', '*'), ('assets', 'protocol', '*', '*'), - ('assets', 'systemuser', '*', '*'), ('assets', 'baseautomation', '*', '*'), + ('assets', 'assetbaseautomation', '*', '*'), + ('assets', 'automationexecution', '*', '*'), ('assets', 'pingautomation', '*', '*'), ('assets', 'platformprotocol', '*', '*'), ('assets', 'platformautomation', '*', '*'), - ('assets', 'gatherfactsautomation', '*', '*'), - ('assets', 'pushaccountautomation', '*', '*'), ('assets', 'verifyaccountautomation', '*', '*'), - ('assets', 'changesecretrecord', 'add,delete,change', 'changesecretrecord'), - ('assets', 'automationexecution', '*', 'automationexecution'), + ('assets', 'gatherfactsautomation', '*', '*'), ('assets', 'commandfilter', '*', '*'), ('assets', 'commandfilterrule', '*', '*'), - # TODO 暂时去掉历史账号的权限 - ('assets', 'account', '*', 'assethistoryaccount'), - ('assets', 'account', '*', 'assethistoryaccountsecret'), + + ('accounts', 'historicalaccount', '*', '*'), + ('accounts', 'accountbaseautomation', '*', '*'), + ('accounts', 'verifyaccountautomation', '*', '*'), + ('accounts', 'automationexecution', '*', 'automationexecution'), + ('accounts', 'accountbackupexecution', 'delete,change', 'accountbackupexecution'), + ('accounts', 'changesecretrecord', 'add,delete,change', 'changesecretrecord'), ('perms', 'userassetgrantedtreenoderelation', '*', '*'), ('perms', 'usergrantedmappingnode', '*', '*'), @@ -69,6 +68,7 @@ exclude_permissions = ( ('rbac', 'rolebinding', '*', '*'), ('rbac', 'systemrolebinding', 'change', 'systemrolebinding'), ('rbac', 'orgrolebinding', 'change', 'orgrolebinding'), + ('rbac', 'menupermission', '*', 'menupermission'), ('rbac', 'role', '*', '*'), ('ops', 'adhoc', 'delete,change', '*'), ('ops', 'adhocexecution', 'add,delete,change', '*'), @@ -76,7 +76,6 @@ exclude_permissions = ( ('ops', 'historicaljob', '*', '*'), ('ops', 'celerytask', 'add,change,delete', 'celerytask'), ('ops', 'celerytaskexecution', 'add,change,delete', 'celerytaskexecution'), - ('ops', 'commandexecution', 'delete,change', 'commandexecution'), ('orgs', 'organizationmember', '*', '*'), ('settings', 'setting', 'add,change,delete', 'setting'), ('audits', 'operatelog', 'add,delete,change', 'operatelog'), @@ -114,7 +113,6 @@ exclude_permissions = ( ('applications', '*', '*', '*'), ) - only_system_permissions = ( ('assets', 'platform', 'add,change,delete', 'platform'), ('users', 'user', 'delete', 'user'), diff --git a/apps/rbac/migrations/0011_remove_redundant_permission.py b/apps/rbac/migrations/0011_remove_redundant_permission.py index 74d8412d4..26b5df847 100644 --- a/apps/rbac/migrations/0011_remove_redundant_permission.py +++ b/apps/rbac/migrations/0011_remove_redundant_permission.py @@ -6,7 +6,9 @@ from django.db import migrations def migrate_remove_redundant_permission(apps, *args): model = apps.get_model('rbac', 'ContentType') model.objects.filter(app_label='applications').delete() - model.objects.filter(app_label='ops', model='task').delete() + model.objects.filter(app_label='ops', model__in=[ + 'task', 'commandexecution' + ]).delete() model.objects.filter(app_label='xpack', model__in=[ 'applicationchangeauthplan', 'applicationchangeauthplanexecution', @@ -15,13 +17,19 @@ def migrate_remove_redundant_permission(apps, *args): ]).delete() model.objects.filter(app_label='assets', model__in=[ - 'authbook', 'historicalauthbook' + 'authbook', 'historicalauthbook', 'test_gateway', + 'accountbackupplan', 'accountbackupplanexecution', 'gathereduser', 'systemuser' ]).delete() model.objects.filter(app_label='perms', model__in=[ 'applicationpermission', 'permedapplication', 'commandfilterrule', 'historicalauthbook' ]).delete() + perm_model = apps.get_model('auth', 'Permission') + perm_model.objects.filter(codename__in=[ + 'view_permusergroupasset', 'view_permuserasset', 'push_assetsystemuser' + ]).delete() + class Migration(migrations.Migration): dependencies = [ diff --git a/apps/rbac/serializers/role.py b/apps/rbac/serializers/role.py index 140a01401..78724c18a 100644 --- a/apps/rbac/serializers/role.py +++ b/apps/rbac/serializers/role.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from users.models import User from ..models import Role diff --git a/apps/rbac/tree.py b/apps/rbac/tree.py index 14ed53343..f8309da36 100644 --- a/apps/rbac/tree.py +++ b/apps/rbac/tree.py @@ -48,6 +48,7 @@ extra_nodes_data = [ {"id": "cloud_import", "name": _("Cloud import"), "pId": "assets"}, {"id": "backup_account_node", "name": _("Backup account"), "pId": "accounts"}, {"id": "gather_account_node", "name": _("Gather account"), "pId": "accounts"}, + {"id": "push_account_node", "name": _("Push account"), "pId": "accounts"}, {"id": "asset_change_plan_node", "name": _("Asset change auth"), "pId": "accounts"}, {"id": "terminal_node", "name": _("Terminal setting"), "pId": "view_setting"}, {'id': "task_center", "name": _("Task Center"), "pId": "view_console"}, @@ -71,19 +72,18 @@ special_pid_mapper = { 'xpack.syncinstancetaskexecution': 'cloud_import', 'terminal.applet': 'remote_application', 'terminal.applethost': 'remote_application', - 'assets.accountbackupplan': "backup_account_node", - 'assets.accountbackupplanexecution': "backup_account_node", - 'xpack.changeauthplan': 'asset_change_plan_node', - 'xpack.changeauthplanexecution': 'asset_change_plan_node', - 'xpack.changeauthplantask': 'asset_change_plan_node', - "assets.gathereduser": "gather_account_node", - "assets.gatheraccountsautomation": "gather_account_node", - "assets.view_gatheraccountsexecution": "gather_account_node", - "assets.add_gatheraccountsexecution": "gather_account_node", - "assets.changesecretautomation": "asset_change_plan_node", - "assets.view_changesecretexecution": "asset_change_plan_node", - "assets.add_changesecretexection": "asset_change_plan_node", - "assets.view_changesecretrecord": "asset_change_plan_node", + 'accounts.accountbackupautomation': "backup_account_node", + 'accounts.accountbackupexecution': "backup_account_node", + "accounts.pushaccountautomation": "push_account_node", + "accounts.view_pushaccountexecution": "push_account_node", + "accounts.add_pushaccountexecution": "push_account_node", + "accounts.gatheraccountsautomation": "gather_account_node", + "accounts.view_gatheraccountsexecution": "gather_account_node", + "accounts.add_gatheraccountsexecution": "gather_account_node", + "accounts.changesecretautomation": "asset_change_plan_node", + "accounts.view_changesecretexecution": "asset_change_plan_node", + "accounts.add_changesecretexection": "asset_change_plan_node", + "accounts.view_changesecretrecord": "asset_change_plan_node", 'orgs.organization': 'view_setting', 'settings.setting': 'view_setting', 'terminal.terminal': 'terminal_node', @@ -95,8 +95,6 @@ special_pid_mapper = { 'terminal.endpointrule': 'terminal_node', 'audits.ftplog': 'terminal', 'perms.view_myassets': 'my_assets', - 'ops.add_commandexecution': 'view_workbench', - 'ops.view_commandexecution': 'audits', 'ops.jobauditlog': 'audits', 'ops.view_celerytask': 'task_center', 'ops.view_celerytaskexecution': 'task_center', @@ -123,7 +121,6 @@ verbose_name_mapper = { 'tickets.view_ticket': _("Ticket"), 'settings.setting': _("Common setting"), 'rbac.view_permission': _('View permission tree'), - 'ops.add_commandexecution': _('Execute batch command') } xpack_nodes = [ diff --git a/apps/settings/serializers/auth/dingtalk.py b/apps/settings/serializers/auth/dingtalk.py index ac22cc056..8df199eea 100644 --- a/apps/settings/serializers/auth/dingtalk.py +++ b/apps/settings/serializers/auth/dingtalk.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['DingTalkSettingSerializer'] diff --git a/apps/settings/serializers/auth/feishu.py b/apps/settings/serializers/auth/feishu.py index 0f5ba93d6..580cdb12a 100644 --- a/apps/settings/serializers/auth/feishu.py +++ b/apps/settings/serializers/auth/feishu.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['FeiShuSettingSerializer'] @@ -12,4 +12,3 @@ class FeiShuSettingSerializer(serializers.Serializer): FEISHU_APP_ID = serializers.CharField(max_length=256, required=True, label='App ID') FEISHU_APP_SECRET = EncryptedField(max_length=256, required=False, label='App Secret') AUTH_FEISHU = serializers.BooleanField(default=False, label=_('Enable FeiShu Auth')) - diff --git a/apps/settings/serializers/auth/ldap.py b/apps/settings/serializers/auth/ldap.py index 2e371c388..a6cc22455 100644 --- a/apps/settings/serializers/auth/ldap.py +++ b/apps/settings/serializers/auth/ldap.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'LDAPTestConfigSerializer', 'LDAPUserSerializer', 'LDAPTestLoginSerializer', diff --git a/apps/settings/serializers/auth/oauth2.py b/apps/settings/serializers/auth/oauth2.py index 42c310333..a689243ef 100644 --- a/apps/settings/serializers/auth/oauth2.py +++ b/apps/settings/serializers/auth/oauth2.py @@ -1,8 +1,7 @@ - from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from common.utils import static_or_direct __all__ = [ diff --git a/apps/settings/serializers/auth/oidc.py b/apps/settings/serializers/auth/oidc.py index 259cf9712..f5c71d7a8 100644 --- a/apps/settings/serializers/auth/oidc.py +++ b/apps/settings/serializers/auth/oidc.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = [ 'OIDCSettingSerializer', 'KeycloakSettingSerializer', @@ -97,4 +97,3 @@ class OIDCSettingSerializer(KeycloakSettingSerializer): AUTH_OPENID_ALWAYS_UPDATE_USER = serializers.BooleanField( required=False, label=_('Always update user') ) - diff --git a/apps/settings/serializers/auth/radius.py b/apps/settings/serializers/auth/radius.py index 6ef574eef..c9eb2e83e 100644 --- a/apps/settings/serializers/auth/radius.py +++ b/apps/settings/serializers/auth/radius.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['RadiusSettingSerializer'] diff --git a/apps/settings/serializers/auth/sms.py b/apps/settings/serializers/auth/sms.py index 1696be723..cb5085386 100644 --- a/apps/settings/serializers/auth/sms.py +++ b/apps/settings/serializers/auth/sms.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from common.validators import PhoneValidator from common.sdk.sms import BACKENDS diff --git a/apps/settings/serializers/auth/wecom.py b/apps/settings/serializers/auth/wecom.py index 38516e790..a0b216a9d 100644 --- a/apps/settings/serializers/auth/wecom.py +++ b/apps/settings/serializers/auth/wecom.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['WeComSettingSerializer'] diff --git a/apps/settings/serializers/email.py b/apps/settings/serializers/email.py index 6604eba6a..ab8b10415 100644 --- a/apps/settings/serializers/email.py +++ b/apps/settings/serializers/email.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField __all__ = ['MailTestSerializer', 'EmailSettingSerializer', 'EmailContentSettingSerializer'] @@ -62,7 +62,8 @@ class EmailContentSettingSerializer(serializers.Serializer): EMAIL_CUSTOM_USER_CREATED_BODY = serializers.CharField( max_length=4096, allow_blank=True, required=False, label=_('Create user email content'), - help_text=_('Tips: When creating a user, send the content of the email, support {username} {name} {email} label') + help_text=_( + 'Tips: When creating a user, send the content of the email, support {username} {name} {email} label') ) EMAIL_CUSTOM_USER_CREATED_SIGNATURE = serializers.CharField( max_length=512, allow_blank=True, required=False, label=_('Signature'), diff --git a/apps/settings/serializers/security.py b/apps/settings/serializers/security.py index 25f1d887f..f18b6f2a5 100644 --- a/apps/settings/serializers/security.py +++ b/apps/settings/serializers/security.py @@ -111,6 +111,11 @@ class SecurityAuthSerializer(serializers.Serializer): "Unit: second, The verification MFA takes effect only when you view the account password" ) ) + VERIFY_CODE_TTL = serializers.IntegerField( + min_value=5, max_value=60 * 60 * 10, + label=_("Verify code TTL"), + help_text=_("Unit: second") + ) SECURITY_LOGIN_CHALLENGE_ENABLED = serializers.BooleanField( required=False, default=False, label=_("Enable Login dynamic code"), @@ -174,7 +179,7 @@ class SecuritySettingSerializer(SecurityPasswordRuleSerializer, SecurityAuthSeri help_text=_('Multiple user using , split') ) SECURITY_COMMAND_EXECUTION = serializers.BooleanField( - required=False, label=_('Batch command execution'), + required=False, label=_('Operation center'), help_text=_('Allow user run batch command or not using ansible') ) SECURITY_SESSION_SHARE = serializers.BooleanField( diff --git a/apps/static/img/login_image.png b/apps/static/img/login_image.png index 936bcd81bf96de65787ce86b938cea308fd1831a..1cb86eb5f9b208a2ed0ed96a2020edb0edc8fa8d 100644 GIT binary patch literal 488955 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

{KE1kRO`k-Rv0nT4 z?>u215ckP|L|&OteZIg6h;*J7ur8W($9LUM6Jz7FbnFz(yzYBQRI9Z5_%qaw;`BIG zzx4!h)cE3=vs8QH2$i366|FycBwN^IcIWxfZI|~2YkD=dsDJelIm$JzV$n~XK23LA zb3MI>+uE|)?%dGG(cI2)@n0!9koE^6YcS8;P(F75J?igg|Je`Hn_m1+XrB~X!9f*! z9|4ihuM2YEhW{qyHNtbK4{Q^QIuBZL0=~gggBI6T)nHIw40>k7H402QD6A>n+6_e6 zRSNeUr_a3@*A+keOibSU?bFMIAZG;^<0<&|%j;!Co^JrQ8kh$iY-{-9XJOt7{0wZ7 zE`;zzie5#O2z9W%P;ut9by}&flcR~P_i3jhy=u}VZIr|WrX3cm151<|Je&>{5w*E% zi%>D6+=OF{$Zka0{RHbH`+LRj`8Tj{tbL*{c5T1^aGXW@lYjlU^wod-qO|^$a{J=7 zeKQnQ8&r~(zgEOki4|$AP*Or$DZ!$4XV=zlp{?3fh!TsgvJz9A{hh7iNg>F&DNOS!B&i(snY-Wn)o;^klDIy03;@Ihetfx3WN&~xRl~p~-@UG`aH-iHv zIb2b-FV4KJtnb{`Vr+#se>`uxuq;mn+X`iMblHV{Xql%m)53XP4p_*~&M9OODC?{3 znxfY5AeB!^>pF{YeL#i2mRb}KRtPQFl^B<86yqZ#=9g5tmcCw0d4AU<{aFv{@N%}G ze|q_Z6pqbw;6cQ$?nx(J*3I+O51v^}22a!=0@tDEO6T)5Jv>VP%dP)}My2?F%Zp#$ zHm>;W+KT#1E|!oGPY;ayIj?L;)aQwD`_->Ev@iXhGr6;<`0@YsXX(>l_&ini?V|d* z1$E3+EP4(s(2G+OR9Rf5Yvmx)LwEk8n%<7C*&}}+>m-g^R`hZ9@L;!bx$hm5@to;L zPRD;XVoV1#AmiFX~+nr9~_CwuwTAb^} z#=`4cHOR6=MqON2?*2>zOy`zT14X3k6V$d|`+9f9x&KRu_dH%6Oy=wj!CBkCaOj)L zy7R!jU!~9e-TzRonKt%**gc!oVrhOs3Gd;;0F4v|QtO!#=J8zHi6z~{p_{*AEY8g~ zLVy2;w1P*qI+f0^XrUcvaA(0>l@=(r?$7SuOA|Y1<@;3?@}d%*?V6FAdqBIhTbbG^ z&Q4y?$>NGuym?fxW6%q#9y6DEJl&f~C|68Gy7^1K2YU|G8gco>6^f+rj;!OrH01GG zea;>=<=K!FV+CVLwe(Vawyd~wbN#WkzbY;06@C6%BGq2Wxrr>}Y5ZOIdtOzJW71X1 zKK(t_KnTak^Y@e+vByDUW{gU!wd6agx#oGAF;$L5MO_W1?2+~HacyTs=j&{~y4qfx zhuqVBtE*i$in4X(Sr7Bp`}?uWsOzsqqooR+ICGjlfA5zmk|KNRdI|l=9j{bY*l!;>Os)vGzf1CuE8r;#924*O*vssC z9zlnb7B6P(;SAWt?DRi+Nn1S~fXGVpUq-YBfEODUuGo z`W(gQsTH3v^|~sWbmz)R^_mP+D&ZZU3*N3VK19V@oIs95I!K_-ccYP9TtD%=+v)1T zaeC`ZU)fev9a%a@Ykp$2Jgcf-rW*@-9(miFI`Wq5ZTCxo@z^tm>AQaD`>DKdCk+e_ zQT5mcpH)zo;zQ`wcJ2%DXx!gc$kSU*86o=%P7ftK}paPwU|chWEX{J*Cc zz2H{5dUA$lhDT#>26<1r=k&Am?8@YiL7Jof0n%D+8gON{^h?;-QFpCJlk5@O>{?9L8@;FIHUhLX|Y?DmUF&-;Tmv*PCWB$>dqZyo79jZdhqF!${T$+Dyk%Gq+Qp!1#Ep5X2XId+Kp?qP7YTn zSXT3~VQTD}qRO$eiKtgXS_%2065UEn8(}YBTe5e!w6LJW%PqIuqB{5M(kf>JxJeRM zEJSINI4#6Ec~k7=u6*j%_i9Cyh;e@3(pKqa6uXjh$wWBolTgy_jF#7k=9iop_S()# zs_vemq0{H7e0EWZ^7InY$Arjp&d`={?Rk*&mq}lb>)MjeR`ujUj*>HPGP0W)3V3;0 z9+&;n@_nLNRo^APCJIR#)G?9bygoHbB|W`c!J3M$U=6&iG#hl|*bGe$kI?MsIOU-R z2I{roZ^p5R`+s>N+T6!&!Dn85t9hPS^y|0Vq04Z#@e%pm{_5}lj?NuFNki9PLlZB4 z9<4nst>}$>D|&()h*1Ll+P%ufop`Xh8Xr@^N#K}fHd)xn$O!%DTi!yy`1AiO-G1}) z==#|`(h3>pjCgkWJpI*^57WQB_wVT23#X+}KSDdD82X(z|0~)zGfR&geMVZ<*Q{fu z|8eeJ_TKz+qOZcpyB|8|g)+zWm>eAVz(0OgzMiF<_gzin^0-BTW9rmrV_Df*sGd`U ztIchfNRF-PENtuYwks@octQU6nOf(I}uC{LZeQ?90HT$Lv0DTsa}suT1#-vOD|v z>YBD>y7NB!*w;t=UbL+{N;<3}w<04j{l2YT)c$jl#A)lNDXFaSA}i{tjb;~Z3z zE|t|^9*5^n(Idx?(21i*>C;l6+Z`=i6EU?tGgMq&RhiU>qGIAEo(Rq;jwe^IQJj=5 zT}Do)zZJzDR2JtgX~C@=op*lQ>i4Ow-=wEYi(|!$I$k{%@?6iVl!TmVny2w;NP)z2 z5Erzy*obr^%vJoQXlc&}QuUZ=)*>&PJ6VGTw);25o2ku9YjrBEtdm$-qgHu<8WSV5 zE`>R}G>;sai|0V6^t&73Y@aRjosH*&IeosXyh=7F>x)KpR&AAVijsCl1(Vn=J%;>O z;T!~|;aH?=F;=iLyT>BELA8ltDy_$%A!ZO$4Y+8F`?>X1dd{@8EXx4`o^~w{&i%o^ zOIw7wm#Qa{{rBdxeGx`&gnYYgqp;u)iAOWw^6asr^uV{jNdwapv~b_UZDY_g@HA;G z(ofIJ_wjjB16F4jg2i8O%kz~x`|G~vRdnmVYiP&Fm}<^~w5)zi3iF@%{9n;$jz3QS z;yJg{Vxvm;o;f1_j?#LoL9e~>1@yx&{wJ!Q2cLPu71{0uM(JHBur4I1lh59uh;Sv{ zp<~a`2fpwRYWYo9T|<@7y(Ukb+w0^jC-XcfuNz(x529_4XEWcMIGGy%ZtB+j>B;>t zKqAila1o6NEYmj!N}hgE`+b;-gtudM(`RAMbDEd7QFsCX1_*SprD4mbv1l{4TP1b# zU=Eb=JH19|2}=x_{l#;sBR>5~gspRNoO#n)Y76RAHyn<%w9){*h;PW-saSfcrC}Dm)p1=e&gAv=x!^_H!{gHoI)7Jnl^^s5?Z(C*Z$tTA|i!H9EhtLbGGz%EHSQDHio?p;9_rq{leYQ?Hr$ zbS2EANXMV+Bc8<2$L%VR3F(cqp{q>jQk<7L2AMn7QpDAEjH`I_p<`#&TE&y@4GQtPy0p{|Rd{q$8@Y5T>)4V$4+Xuf!kuciq&K9cQV)(K z$@=*F$!c3t_?M)`H@0BvZ7KFNP_gh4ThZ&%B0oyE>_3n@FFFs3uvd%7vzGgo);xi2 zUx4=ec4b9CP`10jTkSb72OwsLM`?L&l|J!5KTGw~XWPb~2jTL=Ql#^gfAY6N<0G+K z`)Vz=uxr?z{XhQluh38a_}l3D`>&zr%zA|@|D&;c0`Gqf|pM3fE(&FkWJ$md}{k^R3=YL=G8s*$n ztb1m1VFm{@MRL6Fd;Ac6>g!)grLu!B{QaM!I z4ULF#v265Q7f3Br2v(w36)X z?OfL558($-+ik2e?PGbAFv=_sIA3Q)nrborJU_JZX$%HGi}f1EoX;<(^K>WWq=1$k zbN-9Pd4Wo$0cAZe>bP{Sqa?-2^bR-noZk-(4$|)FX?5Dpt}H2ccwS%7?(qd{f67A4 zmU=6ryU^v-z(Y}gA3aE-4Nx>tmEwGLT^$>?-k*vZ)I2e#tm%w7n1BqY#7b$WGw?5S_kZ`}Vm`pS>~dlKt4dfxZ`Q`#^*cp?XG)I8^N z(sI6cCXPFQ@|bdGf8!7R5WVRK-#|C+y^5}xoJsNm99}p}|MnmLQeAKSo0%9KrZ1g& zmKK{ex@vq{o!=uX3-q#`SJT)4yTA+jhvQGu{^2qDxtm`~Z@lVe`si2hrr-bU$Cc~3 zJL(s?pYx8Ne@^`4YW%Q`I5+QG`jz60@4x((x6x~!|6T3RS?C;lZ7In?Ii8kne499Q;7=s}^yxa0Zsa^}xa{=Em>T@tF5w3Nv;!7& zu%+NzezPU0Wr^ap;@lr?w;t}kmE9Rpw%;3p7G(HjtC+~IQ?%KW z*?vZLZu8*m?L4*GU4Q>6I(_^EwWCle)8O(t6_To>^!I7GxhIFbU|Y@2I-S*BzF(7~ zg{M&yQR<4FY{Ta#(!bDxwxAaZddjv~IJ3~NR4P&n4XQS&IP(Gv`I4G~ZG|n`|9|$r z13-@ASo@pZ%Q;E9JEc?30SQC~0S0Wa3AV9uAUJ0m=id={&)Lt<*al~8fg?_2c;uWx zC`%~EOVZ`Mcf0*}^>oj4_sr}a7~6oRq`jHx={%jjs_JSki6hIxxpbTu?N67g@~-;3 z+t0;cC9zMKD(#*%cSlOvWx4ZGY2+YCut$(R^(%dQZ!G7Og~%!4zM z4h}_38EkSiN7W>pz`b~Ovi=jU>b>K5(tfocU2R36J?ZsalHQgcLP~;n zZ_l~oYBl5x4g1#YMOv}lr`^!gfb(Kbic zEuK32Stp;0B@-J^&)X)|c_#;A|NG=U_`{}Wu(k6b7LA{X4QYhI!28@oNrdt-09^*sZC-O=VP`3$CS1q@PQUOuK&j}zZ8t15g){|_5Ub|edc^| z)+Md88$zJSPvb8(7!Y92j2)bZMXfDdb*E9t!1sYSj@G_mmXE!d_;k?k-qRwlJj;+fXMU8=cPm6Zvt7czvGBdxxZe;0(iG%z$I7P{lh zDg_uCPP<2$zIFv>6DkjYr|Pb35)e2q;F9xFF5N8UlJk!KKJ=4Sb+V-~ zl#=a%3E=OCcfkhZ;5RWM?QK{KZ1a0s3vH{#dXjB2s>y*rqhsD2UkyGX?j7xDJ2$W@ zV;=W2F}E0f;EW8G_8BA;E;q*=`_fFdAx*nMd=Dlx3}%;9VSe3YL{W-^ihj;eGXSquV-hm&ic@PJByF6)dVNM>-nXwSN5A4U2&p(Hk zOr+*c;QYWLOyF(h>tFdQ-t(?^W9gLXm^ZH0J@$}i<>B2g;0I6Kg=?OD2nD>I&Z@4( zvn~6C#C%ar9ma9#`29^gv2UOUtJ@nfv#c5)n|rkQ*890M{i%cdaZ+sqjvhA&kF@N; zFSk910|VW7+oEG|?$V>V6m|eFH13B#u!PiE=)IGrj-kY`@En-Q@Fu4%`}W}a`|k9C zdU3J&9m4MQw6tg1n=K52mw(3y2L#pRm2<7l4! zHd4fad2M->Fa{YLfQu<6lVZyYF}>!^z0FE+-$YNhe1+wumI?8`rEsYAD(WmH?2YTmr@cy4SCaboE6# z+`h;(U`k-HwpP49a_JH1;ehthw*4q>>GCYUyH(&G3+6NIm?xzdr#x^8c&3l{OAdnb z3JXwLT7q1-XL%<`g*}J+!1Cgpa(5g`INH_<8JhA)KswC3y=5JNz`4A;#We%HV79G93sgYMLzHJC6wxNn=V*>Lo-<-oOg>C``n;zK<+@2zhY z>g)?9O~JIPapKei>PI9s|J?0=#P8NU$?LWRCr+7(&f!6HarZJ}s`2dJv1g00j=qos z^~bq1oyyR~^L)jc`-Q6e853u6klxKf`aX2?@$!=COHs%88*%<|C*$L9daM5; zY&(Jz!WhufBW5lA$W{QIPI0&Z_2Rq&amqmVfUEY-Y;bP*S7WNbHquAzkiH`QQ+*OM zQ)oTJ6jMy06a(~_UbW-`a|Ts)8pA4xyvm^CjP#~NQ z#7LS>hU{p;cDS9<`kx|_ymj*h2-Q20vHbERH+Cpy_%y(y)fC(&;~*QO#x%&IEnJzI zq~5gPgtsX#UAh{QA|0goM{qG4d06e&v7CeC7dSbJIw%43rtQ0+^DxY%H~*KH2Pfqk zEK-tNvFP&{9*zsfWZoUga3FsKi093ogJ;&Q#YJzq0KFWXZ`rj2$IP0IgKQ5P54Iwx zHt90~1ZJjmnIl0jgfxAye*o!%T%=Qds9*)C<^YG(ijyQ==f0Bg_jfDcO;-ZyB*iZ) zDHT#7k}hVLYoS~K=p;hgmpk6%^5$5RNg~Z>=FYxeSKV8dnfxS_0lN${+K(}=5hG!E zMC|pZ{5p=dN1Q2!gqv4Um;7JGV~V)(BpkI3z->=L(k*NZI3D;;sP6=SEX*${7H&z- zNJw%JS5RCeo*PPvuy}X?PLZo=K`O0z+^ss#1&Ngcm;)VsE{GrG43eIMhcMt|9}jbN zy9X?ywORmNW*CM#!q(J7uy3Li;p1Goe(&j* zq9iAe|8hl|WO4h~ANn+Ie(nkU{GQv;)X`?u4Kq17-jxOVLe_y(`SHeSpXGF*?I5nc z^>4WGu?Gc6C$R42A!;oU_D<&W&_JKCbbmPQPp35q?FBF6W$xnT7eJjw&pEc>LXjLj zBqP`zduTkY;7kwt#YS^uiYYtAM&e?6)e$vV+ApjdATbwV(xY@&5+d=+YVO$@KeH!r zCyODtFn0&uf>@lLtxZU}ln|E0o;cV!v5}O28TD}KP#>M2Y%Bx~xUDg7qT>l&mgxK( zN41OWaF@ONbaH^2?sBE+ z>F!>U-@&0FyycRM@Pki(QKt0;F;c-0O zA}yS&0mS}B^7-pWpNaP_Sb-0G@w2$+;fL^IsCL2xeEzaeW9i~0n9jj=Lq)ZWnoH-m zZ^Wl>{2iu_8;9MU?U-6M9=C2=D* z8#@nT0gv<8aZ~W?t~z!>mbhCHz})u3d`&j`e!sZ) zcKqql`_VHr5K+JO2&3vo`pGcUO?YR%p*W_ESsBUkGU^%P4EJxp_oG-bXMrD=)(#vX zsBxs{#mg&%A-VA!SeE4F<8UGZ=$(Uo;%G^Kx#iE59vMKL1^f^wC*U6g^O#~98^r)U zrdJiYmf0h~k)lyRx#3k_b-h@_e$9|NWx@{b=pLjY2I1^@#mvJKiXuu7h&@8M>mnYOiWB8!UdnZM5E# z&jEUE7JyEg3JZ>24t=|bw?3D79h@@>g)i*aRIOc5>I#lsgKfSQcL#Il%n|OD9Hf8Z zJ@3bgrALIOean4!W7OLJ@bezy%XIbf!6X z&na~!tPTg)37&3(U(=i^Lhmzu{rr9yX%4c7IfJ0TyW2IwHaLiW4!9{_9f#U5Ai(z^ zhSDhv_6`Vj=)v@``{O{IKyyYiftYp`GBw9#Ru(jICPQu#CF^!Z2`_~f!ildx6My@` zPY^{0K-VKM7`7}NRA=kp+5B7a9^g{tt2aK58+UFn{pny+eJAaEt(-Iye|*Ec@zdX5 zk8k|=hZrl-k!k0jdoJeIO%~u>oeI*~--j!2zX8uT?!nroeONMO8Xns9f{>tp{P;6) z>(+IcQdJ|`#gxh#tZ!??`i@q-W8QLp<14ma+^ep_RrSw`n%U@bv$*qw~(SLdW?YMpQQ{kZ8{!V(mF@)Epn;;B8&b0AO zuX73H&p&P@K7Q^69I%h`=Y@wEVtuaw6yfNSyWOM;GwCyK$gx4&o3!K zCX+^ES37ceJ(d@_YVidENONpe>`IOk5@;v$^Z5dK82vmagS;&#i^zanFA_ZMOrI;M ztVl78kQADO?IG@Ggah54&Q4L8gI(Riig^FPpiqBLWkxW>)9>N_s2vQ+=Zs_uxL1gK zIt!a{(Eyi-re(WP@5o&MGsgwwX*pIa<^r`Oeg8Yr!qRGN+qMn4jcv&7>^1ruHLr9! zgZaykz`d*fh$spL=r%=3%+))qJ%C!6ohak`+x@<; zW7qzDIOT%#F&Zshv;<%J>}N51@>ERa{f+iUE4u2Qn{nlBe-WzlC(m6Z+V<|wLs&9t z8ajsug!FQ4>j9iFZ8p{&;=sJE8OKkZEspnlta&$%n>Zcig+=J%W!k`{=|pEvoQ1#c z-XOF^zP99KysmDB0P6~zi}{#Ag7ScTXjed z8LNIXmVf1s!TcWbDuDWVZ^Oiz@!@UeMa;A1ydNKKK%KUP5hJK~aKLj@bC|fQIg3eP zUR)y2T#qTHm|iT!06nHxGvnwB~Cg6ej`Yp_cMcWQ|&qg-*0ysn9e9m0#X=%m>FMKB^ z*H6TEfBQ@9<fwYv3?bVU3lPQUcT*WtGx_>A!X?Nz_R|2_GrtG~?waDuFuOUv%UQn^&Q zPsnr4>A9q+Sp3R4@FrzCkBwV@32{E&(~?JuV*?T_cEO6*}5?);h*V-IIg&>r-neAgeBnhSpnu8KX~lQshF z2UnKx<$HMV-A|d}eps%%Z@ge~9ftNci7X{~Ig%Z{0iotm#~zD+{Os3}?S@VWXiE)5 z1HLGAlPDk-1=cNhvCn+d6GD1!NPJb8Fko&2Mqa;nzWGC#QC2Mg`hn)~M!?s!ZoufoK#N^kynaPvA`dGjCf;I@r8Z~1ZP9O{?bCLGZw%Q=9&W$PL&p2$Ic z)i~U`Z9PupAiZ-WCC)!*QY&RaO+9}3!ZX5X*9Ybvg}?0DfPMX4xP0*`IK6%bCUVA+ zl8UQ2+0&BHnCFHB9ZIV1f_Xar^sPU<63ty5;q`7`!|K$5!Qe1Jyx~{9*S@`M_asHX zDi8%d@T|Ibz%_q4h{;5`bgS6;UOm{$pLx3u+AO> z^O$1#=O|wB$MlLMcl{!{@|LXIUA+4s-6JRBUQMjp+=-InYo!n>k6DPoG=R{pypct- zU-H{74#pA&H@^Mb zU*W?){1&dc^{@Ezm%oLn9Ozth%9*(Ndtb+t+FFm*$K-di#~LO9hWF0<(K8q2v7`W@ zA{K!{Tubhx{!M$T3%S(2oK*j1JqsY3pNAxuiZ``4W2AW>wvBMN>SY0 zu6=WDs3yz%JbXoQcHpz0GhGTR%4K<2D3}fauQ1RR<~IbyKYef?uHN`0o<6h>1`rQ) z9>ChUyd)l_X9ZR z(;vl2AGiqbc>Npk?T=lCcb$9&-gxA3Vj+I@Eq}+n54;_3JoZF9v~jIlT=}8Zw-jk4 z%nDU1-UK)^B>9$L!4Zr@X~UfEGYL0bQjJZ#%?%HWvy^)|U>Ez_IZ)28D(AqJGurmI zBgN9VzrPi!oCK;9d6=JH#=&qJLyU*x=%$PV@l)n674fxpcjAV}A3$qYC&rhRW5J|p z=o=hTpCjZO$#UfI>AiQ*_h(kH;nf`@Wi^w;8?Kwnpm7z`>FjbcIv3VdyguPEbeODX z7GMZ@W4IQV9$xs#xc(Q;oP%uC-r0#s9H28DM2$slJg?ZM_b`{7Z}5Sk zUU-A^q@G!Hz02!O+t%R=N1leGmMk*>`tl`9@%7Jt5z{74#?*>x1NdEa+uyM2;k$*# z!g)s?i-vI%aMRkS(J;Q2OVN+Ts;3_kh6&GIupCpW$K!Wv9v6~wlCD$c)|0CH)LEEP zT8TS$ZN{Q;^#ag8M*GfroS$8IJl;S1NMWHpV=;X^#tl4P6?{7?{ySAf3g67z?_ZyM z2sb_bs4zGdB2H``nm;JCwm-4bpCuyF~th2mEyOH6ctDcY6=8aok#%bl*xtCGs;PdbOpxA@H z=1(`_k9XaUuD)KB%OktWi%am`4}20+xb*S!*ItcVpL!H0EII-YZ(8f++dYEJX59Em z&X_y}Cm(Snz`^N1?!Aixi~%f}ITtf0O+ia%J09D(4wt;{4VX|>gM<9Jm`f&S)J;Td zX9sS1<}n;IX8}%IvK#~H5g|eRS>*dKcHny0Yp@%w?| z0<5fUz$M3?iZy$8qN~3b4IHqo*}DT%t7@@y$}|!Fzy5kX_Hs#dC9kg^T>24_&f1;Z z@zY!WChG9x=e!k*r_DrpWd+u6+Jv9qcmwt}Hj1#{`ow1h7`*eL2ZdGw)%~yj{Co6t zce^zxrSpEJGOt0iyqsuEEuW9&?Is(wbMQA2Q^Q~C5r#?4Z2+(NWAc4ZUmvbp`xvf2 zxJ{e^jwr5}YB1hwl`abb@BZWNu6D7H{>Y^+Gp3%`;b%Ykah!I-NgU{vi_;O5_XpQM zk1KCjnsEC6(CACF%S6yK!9oG+cGun}nryrJ_!A3>h`_hBwJ5 z_UlR(oXOKl6iDYz{d`P#YR4A*;MTwJzS5y_a6;-$eS;{5hVIv9h=jAXv&XB2+p=dD zF8Jza-SgAme4%Gh(j#mBHGq2nsHcTZgz~%safbTgW=Jlkm|}YQC;{l`h|Z8N9b z5EXXld(kz(tBYYQGJt>$2y_*;Q2_$W`r3(*((9!%ROeNiA$f~fZg~Ebbc@bUrym)T zqob~3LvrCn(p`V;91T4YvR?Q71?$$65)x>2nCX=Tfvvx~gawk}$Kx@1{PtvMeCqxX znf(y*%i!Y_GcREcFTjwp-DPAwgM(QC`kAMkifjLHy&rF2;mvC^kGJ8}a3HK?X^8vD zwRQN%&#%Szul)tS$A6Eld5(kh0LDN$zYpOZZ+Ii#dFI(hINFc?;U9h*4{?Bd;^HIl z{r7)D7*sps>z_8#^~RYHa8OcIg!jJDHQ=>&%OdiMv# z^Sa&J@#EY7fs+?3!Nn`j6mAc1_>QRaCw_FT*czbdL=FVsc>GB?@q-tkySoQxop3S- z=Ji5zLD3Dj-Hh(8Zn)4yO5wpI157fC94SF}G`!^vhwivOY2w6D0s6MxyRm4|;^6ol zHWq3C%fQKDV>@>v3G2nb`T-8ir~!y!BGlG18t?-GoML9h8TH=F6KBBe-HPLCr{cfA z_+_j-{sc@OHy)G9Dm@J;GJN=>oBo1T5BwiG2l_FU_klg_2eF&~KK1(Zv6h2!vZv5c zJwfF0p=}${nHt0=PB{w?aZrBs6ZhcV%TL5&F1?;oRgG(&dkBkaCyR5=yV5C4<`Vkr zPQ4IkPMHJM(~9_fQX-}H1Ug%}ba;puor;O9&7W#MQhl1{BsyC0r+43iC$?@f>pqM^ zc^)AWa!)hCs$;{`m=iGL;8!|3{l;5w#kW8DDV+bNb4@>Cnsy>;i?bx}>0|@?9gc?N zVu~rImxtm}Bc}ft(G*9(Jk6LPbnwWrA56$@oTq6|;LHaBp};lUJ4Az>F!v}mW+#jK zr1mg+{5FW!fIa=Cqmal#Iaz0pDM-U~2EC_Ap|7fgbuY1mMNa64@xdzd!#V!dl(B0J z7-kzeWESX_tON-p#Lq)AK$HjwOO-IL;Z`IH$LU}-P_~UpIwitpA6MjBCo9nc2&BbT&C5i;C%1y@GVMVAWpe!Z)*GT@XIy*M2a*uRkJlgh@AjP=k)NB33*UGSmQI;0+v{QG{C$uwVe^@wQLkJ0t8b_(z$l;*n-oTYo=(!h!jRfAvE=@xn%AB@fiX=g@?VmquOd zbA0H$uKgkbyA`pXWN9J3^u2Fk%a$!>KJ9j;-1V#GSJ|xT9vr}4E>Z6o>_<#7#T3(v zp;&?*(|?3)b#_78gX4<}JwT)Ocl-09HlceHxDR|h5EKDuywD*Ne%RE`fZzgOfGACk znVA7>$-2%o2vemGOIKHMH}b3A1Kwha7EomEJukNS?optf`ELMAx}q3G`Z~~m5e%)O z2ovVWFCB=)J_^wDW@uE)yiNE19p(OGL+#6&IjD z86_h>n)>z{&J4Kbp*t{P_H>-S*p;5&yY@M(=U{j-2f-7TEJp<$jYY=K^4%k{=qRio zKJtH#INc0seg3EuN7$z14e7$nl%uP+c`j zS8w;8y{lH;pZV}dk&T{Ozt)r90#*bzNYA3q9_H`qb!%{7s23>kkMI-qrjyckVG*4q zWK7VR5DJviKJ-O3_4uz7d<~G@Tmt=`?_P-qdHcEa_2-D~5jQ{aFgiKlYpAUk?VA`b zOQy{dVBL+L37}5Y!6ofvV2|4VEt{SfR@~n-X9?DFV1C<<^*CY5Y<%#|$}6-s z_IMNqdToYcz#Zn09R`}c_+bHcriD>%`c;MFjFJ32E=j)yUz$7DPv34^+1iuFw4iZC zXdv+KMcL1`ij3kNGb2eN!DIR8hf$^nR2YDB;H0GCg+xT3v(J*h#VnGh3+ zCjX8C*P+eDSY^w9DRZqJsf5WLsep0 ze`MzVd>XZqojoXbeCAh%`JgRRUW9a65t92_{W!?rCI6)~f0Pbc(N^~u=3r=K*oc!1 z@1;2iqI08fUi~Eg=eED$x;t+5{2V`hu8~&)5W&SqorHIt{su&n#n=dF{a83Gzo{+Ij#??b>;z#pflcW)DWy+Lg{Fg{p?wF|GVLEaqvj%-GH>kDg)=Y|ROxa7l)Z&OP!tRF;$ouucq^X*J`8CdHclyZJXQ!rB9Sgk|@W zrq2=5@=h+rrIdgE`gh{?3qB%@!{u;5TAEiNegw`*3Qi}62+8?KieFRi^KeSI4T;}~ z__g+S^SNdiYxnKJ=YIEdeEzzhx!~Mjm;j;R+?dbhjXxxt^=#~eKrk+5h0_Nuojaub z+uwz05y&=iFR$+dgS~zjFD)w%qs_5eKOja28Is%6)h_BerkG-iDI3LNwwV4cL;xU@ zVLrfSrgduJhiUUIbHgYlUr&-I<1)yJxYwO6Nw()rU!>W2)+b*leBW=tpUk_Gs)rs= z<2cARt%ph79er`2@Bfqc%nDCHosP62Njh14Ped^%4g;cdr%fyF;?8@ouir&=%0-=i z@2|Kmrj27BQKrK9R>HHl8Ak`jPDqAi+BOcb$q9Y!Ab=E<#9Vf~z}|VyU)18$v*NC` zDOzy$4ha{GacH3NZ`zv}{QQr90LLyn z$}<%At&e{?kmm0nyC2`;()Ev>{boG%qicl}-7Ba!&^V|h$yQ03I7?bl7Y9Mz{l3(g z_Sl<8PLXwYM_70#N^l8t&c1_S)&80A^qsd2cB6H8zzd_-YeVHYEUlY@l{4r2b;;k? zlvm*RhS_*#>t+Fxhyk*#^PuM^m)7@uJnb5ARcF^U?Zpw_{0w~vZ)^~jx@K9(SgY`d zHmwyvkEHEiyWnEa8yV9REnN$`;L;1Ques4XGEE$#X7>f_>OMGvLE$r=9HjU+&TDFy zW4-I9<1C_2e<-{3eEZ(rXz%L8L=HraFLbStCn0b~z>&U~*L5k>+7 zh15xxtxhaZ!hHxC%RttmUrON|NRDTCSXoR z;1(T$mHc<*KYou2&hWb8try{nTmFnyyseYkyRg>I15T|T=Q9NO^Wq7U@Ta$ah)ZzY zc1)U9Me?j_`VIsj$r`zkn2*rC3&cqROhzOK2 zEVlS!imbioCvpx;pq`&h3Q3PiI(SuJ*?ePGxm|A|?M`2JJK5K4-U$PGqxGU@Ru&zv z{V_ZHfm~_r$nyx(Gc((yk#2b518D@Fxc@dTQOgF zTPXl~RH|KaPc{MdXa9aXzW1A7;*u|a4j+8mJGdnMO`h*Z)~?2Pe)k)cbEd$Z-}$kp z3a;|1%IJ_vE5H8zvsZX3-ipY&b3!{h?y|SN8z&#J949VWCZ1_Odgr1BWb$&ncm){M z)ZVubbz}QrH$p!a2%y*C`OCiPJ@4ym!!9lbt>b{;^XFYC+~{bkzQF;Z-mZwiY5Bxj ztZQoonX@90f8z%}&jEM8$j91!J8{)Lw}v!Z^}XC2UH8!4_|zND7i#WAw>V1NGlx3F%AQU;6?AY(@hs$?wYiR!A*4UFI4R^Z>c8g(SiJ z+s-{7lSh%BKe^tu#GkK>x7lTMqWY8*^k+**`Sq=*o`G-u>9;@||8~VZ&ovc5(gITY z5RzfxpV%8>bPgUf1JiSE61yi9Z-tcA)%J0QhcioJYa~BGUKdDiZ6n#lasW3`-^QPx<(yV!S@*`j6z`RHR^9=J( zFCZd~Hea1x0`Rm8%2OIJC%`UL-T8d7cGnL4g14_7%?GslkY!0EEZ{8bSGcQ6(cUR( zav0Z6uR|xKKK%hJ2=?{C*^W;qh3APO(?19drLgREr(iXIapR$#xUYRLF0Px8k55?= z&4jSaYLA~G;9eM#<4p9*!Xi`^6vse4rkG+fC>{}F`j?Tb&aNRAb!Vt%>k}E%=4QwH zDJi-SK&|7;)E@_?WRkG}P?s;g9Qkv)zc_HTe1@Gzjyb@kQGk zF*u-x zdN2NX`L_i)@8lqt)ZAbHl}`ynTipagMe=U?q~`6V!$I!%{`gxQIMj**2V3y$3$7*V zdM+71`0M8A6`Ih{4l66ToEp&5kpiktC)>zc+rcyPxi?Cv;-n>Rj- zi~e|x06-+!?iqBY?#1~91~}i`yq`-gAH_&pCw}_;quATy?)g1$$A-QyG}m~?>yryoPGSsm|9yO z(#W*F1Qu*J++lF?IOIzW346GtdYe3P(x`jS+q=5(^oDifp7!fUf+1V$NPo70m8uu% zHfKT|mt4OSXgWm40nnk@oKrI3GhYG#GOU-dVEl|(0}V61j;p|`jvbixU!TC0H~dzp zw-Zg}`T5}oK7~)c=>jZmm?_%pRd?Pf!Y!RT9lH-5!cA+Q77Ut`XD`4*)TY{6anA?8 zguCDSMU-;@$e>pX69Bw}14P;qMTX!8glfA~X&*^RHFsC7eK75+y|?pz^FKG;fG_;+ z=Nz0j`r}quoqOK{E;x^>6P?H(#gWj)YyI9HBfT``J!f=a`CMT7L~+cZ*+xV1MNXu3 zGRXUBPaprrgTfmASKs+w&bdg7wCQ-gtM{$JY0v%xH@5EZ(+pz*>FsDNOd{%M(vV#D z(CBtNVu~rI!-qJdoH4`{)5}O|?mWnd`p@XnGG0CUTafYMhu@Y1xKzsav}oTh#Y2D~!{a^Y z=;Ot?)8$SQ_pN>gz1$CNji6r{2SZ2ATY%Ey5*(~9#Qvc^)HU^Bptlb@8uww@?71Ag zk4H<#A*|(6=F+k1CN2hLF{TeAdXt2cK3996PgEmkjTr$X*~Z^ zODZ^M?+|gV*}F@eeNM*X=tmW}n|~u0U@~zP`})%iE9WnE)!Dfu^O5yy;N&F1N(zxY z)Fs>-YR2QlMMsEW6o#}I9_I{^ZqAS)i|%9?uDhoPJzcJUQiFFS6G9n{BT7}33M~mT z2i zR;3(i-TkWD|1JRdgD-5r+5@}!H(Y`WlCW>zB-->RPdyu-IQb1iYA)EvQgWWr@8!6X z$+$LQ#2#}F)QLfq(j@9iz5T`~AI9$=xrbv|1tcBgVvr8l6*xDT6I#7_^<@EDau?Ed z#{|C_ZOqMR?xe}`x)nfqi5re2;-rn!$eAoH{Myy4)ieL4$q#2(_YVkQj)5UK^)-0! zWuL^2nL}tV&2vHeNZL&&;pS^%ehI!k>v*gvn-Ja3bc%Xsjq!BNO(anh8@&gm_(FSd%6K77H;B}7r9^H1oVn;Ckj$uw$?riG!UYoRD@a=^p=8a$sDURsI3R3jjkaqdgllb@3_ZeNw>z#5VocJ-0zr7_*Yuw(=lxv#Xj)CuSkIY5Z?3G zUwgHkgdtY87W$D{zxjU4cYZ8PbP3k#6nq z{P`eA133Ij`^XhgPspnPqWOGGXzTXDb;bqS0?^`HVTJ z$S?HfH)8&L`VYSpOds9|g+=#Ip87_d#M@Rz=1190a6x&-1?3t@ca!Abh<_`RoOf{m z-p{YJ51l~z>RsFL%e!vD4wB~U64J)QupDSa-^%v z-0%d@(=sl##X(L8f&JzSMC#bftyBi^=<|jYwO73?8Lfxb<8|{%p_?M!NLd*5dG*4K(R^ z#Ftg=)RF(JkL-ue@>%TIqf+wxvk;5+Mg&eEBr|fO2aq+uAKQC-gk}aU=qHp_jA>zc z<|(J)*1PT$(pMwRv#Cr329^o}y`g!x3r#v9Q z_%)Av%j1vv99-mZ@S7vYm4y7Make`(gtpWG6zIutz>#k2gi}<2#Dr=vk_-}9(E*M8 z3Ggh&1>d4Q3i9A^|B2o~5oJPqr~E>JbSkd|S`L$YXAZ8t>L(bD{&D}^Xl!lq#^vQpk3caQinAq|x+KtueOOrL z8v7$dZ0DVHDpsyI77sn~I6C(=xxkKqAGLFmraKM-Ts{TZgPD+NLhDKvNY}mc_`6H; z@w-Rw!wcKCao#{B$D>6k$;(H1Q3;NjITsIYS&z$3KNsIR|02<@nB%bwpl=0ZA@!jE zyURyP@Z@rZYI{>xCw{~w^}pcYyrZwjAB?o|DH5FLB!fVnvB)vi;6N%hcX99dgR{|g zv~OI(zyDdsx(pLJc6jMQ1M(C}0cSw*{*TgP0p=afXvv#45xG<9kl>8Lk*T#VIHw>K zmztxMZ3&eJp2sb{?YO>qGY;^!GOxG_<+=H=)z^({n41pbHSkWx`snQR5k4LiBpvUx2k$%Q>G`6uOX*qE#A)Y=roBn;!9h6nt8L<*~g82;du6gh#O{R0X zBa+eM_4m}93lRN$X&cBzrNPEfG<#By$`jMemL(0fh%3s;6PA5ns}gkr=k0vallG3d zF@a{d;WYcfv0gJ7leF6JH*c64B;muX4&p^H)hWE74qogK9i5Wjf84Y%YCKM4e5TL6 z2U~f386oAZE3L$I?F{W~w2wg-DTPSW!cz6f_ng2orXOse5dN*0KPFo8&tjQ38P zfPsnCsCuChg`IukT|zRlNNP?7=3EK8>vo6@`;v1tusH|;HI4x2OXn}ZnoS$A>gfmd zs^kDEiSapwDB-ZTjI#`i83#>#k)Fe)YDo@!962WLALzmJoz3v1jhbZA;gWD?LKPUP ztJ5B8+G8s%uscdBuB7M`CjpP45hR93y!Q!#+%leYod7xUx5PQ;N?NW-)S)Eq>5Lba zO58uA4uyLU!WmK;EPfxNqD-o*$6u~pHL78_r#Gy{A8xb4tk?m-S($CbO%^r|u!_^FTReG?!k z`Gs2rhMK$fUeBZQquvh<1BW{Co~6g*vu}O}rd5vfEV1h(jxuGw^xRZycTZGN>hA8R z%7|xTV(#s1!|xxt7r%e#F4sCc(>|778Bq+7+#IdER(+_#+lJxv@{^=GC&%=w#>I?X zry1nE`z&$1VY;hFO3qy-m?=ruei&mTVnpP01RD~^0+5YyHfQSC5LpWKbCy;l|#9oDC@#1zxv zMzI7vrk6yFg#+dA1-%AR6kyHDXm&zlv*UK`BpmZa)V}aQSe3Y(B#m>>N0|^E8P)?< zT3+m>UBF;n2!}m1H63t|0c<-Vs&k`(EXwPM+TO8)QG2 z(#o@4rl?UxbEEv`MI3AAc$2I59dnn>L&4l>?iP>2LKB?3+y{74Z~@E6FM}Tg}j%)M^h6l804Z~O2sn;-2cR_lNC^0c46e+0b0apWMn_yJAu^RY1a6%z0|_9vGhA9i<6@-vfcZkl)GnDNtp>SSC^DwEL6rp`WdI3 z#zFf10d@CWF3B3KuSO0B)C#1lJcuG`zy*8e2yCTmYoTAcl~!K?gSQ zL7fKx;9W_}qd1Z40(AbIWUg^X>Wx&tB=x6$dVm8;4@LSnKCmE=)!`mZhgp-|JQHXy z;*vw&4@l+Tky3N-2MNe2n;8Yt-7CeZ)Y+j);?186)(2+PqToQA0OuYe*UC@dYAoQK z=*9=`!S=m-jG`@FxEQq)CIo;`q-0lJwek0NO&{OqHLn zjFkSWHbwiuU%%owTzcvo(AC(CHEY(mYVW)*Mcf3U66SbeNQ(ihpEC={YJ-c;;89^X z81BnY;E5euaMz=cU`kybDoRR)aWOT9`D2n{rvCUF51{z_h@3K(s|r z%GG~MQr`i&a3sk&S%RkH!AN6aEL1K)`l;a{y}KU+d@-G%{n1hq>w*w#(awKeeZ7KN z0Ap?M17*L?W!Ip37)l{oUW%@zTnfh}$rIM>5__?YnsT(ovkJ^9#bel#>BER>Lxf(L z^z5lA1EC+Rp^kF|di-Z~Wv+^HK98(`*C&DWJYp=sl|G9ztdnjX?2$*Tx#=(%AV-tf z3&U8=oM|z>60D&fPES7)Bw=SteO=~|NfmYlz9GT6j7NcVU&;>YChLdpx%6|0GgS)q zxAE^a>}fCPz=J^fUw%C{aQ^UfPvhRFo-o`fuH%kcfg-j4JWMrMUDdrNJ*R!jWL<%j2C%B-@70W9r+Ia`ou%x6oY&&9j@VdD$dLIy*gG)ZL$+8>_v?6w_;zVt^jgsFV$udlUsESp=X7tgZdYKWx5{ zZ?q+Iq$h-_wTA(3A9TQ2$b}lx(vpB4#wG`W{OE~!`_iqr9fT}Xw1uT@P%a9Jg@(^^ z@8yFrGb0krk(8NKmgzA!nZ#>}XkkP9HwjRVa6pt%+7tTvSeL4JSAC)A==>vS*i8fX z(WKVes=l>=L2|;Q^Rw&QjrUjO`T|+|?pmI$S4MI+mWT>Od$PL+`bAxez1jR-9|!D} zMI~cWbtlRS(n*p&d7KNf!!NBe9*2W^X-{_3@bz;6(4~Y|4%Q|(C-9V??@F@UyO2C^2;e|HGp<~m;Z95sSkiA*9Pc>; z(_ozLU;3r|rMYCD_V*X;X+@IupgX1(6Xj~boVmE=cYnnAnsFG74z#x5H#h%1;9go% zg5!=p#sJ;M&z7po=f=Tae1!w^t9Gvzd(N5kQpSPKryG{zo3oC?#KKZBE)-eJB__e9 z3)EdVRj#=cCgQTUz7q{)RapD{YIGiKbN7oA_$QE_Oc>+2iqZV$^1B(G&hGH~;G{ir zpuV%W1ONBbW7yl=gxQm)qBO5SoYbLvQ%UdZ>cEYwpTK|r!cgV$9~MmLwFZyD(H%CV)t6W6u8ASCP43(Eyi4<&2t*Hzw)s@K{zolh?e$?IZ z$27BEA%4T^A)5-l3BVl$m~IV#n#Jch^#TabFhq^qnQZN=yDnIgj?7?-giH-oW|?qmDWA+wP$*kFa3 zz8%@KIlh}>5IS_uS55IWNB?Y;r2h$!l3iN$4Ug!-}kPIL_AMwN3c=Wa@%_` z%q3wgH;DwDS*#Z9Is-zl3=bR!D~hbMk4&q@NL>|jcQm4^sl$^PC#B_cCil}07Z*3YcrH_h{t&lBXSbI<{ZleeWoI+CQ5 zvAmt#?eKt|2rjScNGJ}N$v~V?V+R?9bNwVbdXVTFM5dt@tdcWPBv4ntTY-GnlDj5J zmtZR)v+5a%<5x1U#W-LqXl(NUnEQDguL;soYS;eoC!Ajp0aP?<@9x4c|NdtT$bIK} z+dJ{t<1nteCfG3|7|Eo9eC_@XVxRd7eH|{2lo<45QlS_2XfTeS^{pn9U&g-6o^prG?r+|8ZnAt@1GU&`3$96QMAfTM$^}oKY2{%6W zAUb-xF}-f00B^)3qjGc&_2Y>>+wp^2{(>Lh_E)LSZUJCNYrBp)UU)AElyjy)a9}br z8U*HkM4A(mQfduFQynCdzJ+`vKPs986n7|Z#X>ROlsvHsGw)u56Vk<)TT_QjP67j* zyEH88*o{Vlc784CAI7Zxy*PG9H&&*Kuw?!M95Z`9wkA_($+)K(2u2I#C+(lf-ZMNX zm}6(x&+_KZvScm?>67s0n(17EK7!4IonHN|?Qg@q9s9-6i6nK;Mp?Jz*~}+?B~6H~ zL3bPpGZ+|COfkJWDYoJsQzQkzxrwGtztGpC%;WqrT>vCe%{lT)l zZ6gXJp73%6-fC*)nwTWozh5dhKUR8?=b-BoZ6Hio{2_T{3MfW14^;j~-}kODC*+M% z=k(9$<`RmWJaN3wYn4c~n2hnzF+*P6gsz9RQw2iXop+0E`cF7z?><_X=+36zE-iPyNpY!9ch`}wbJfd zig%ATOCVF6@4ZX&sP=P7?6Bg<2r;sDMl#HLMu3qZi43=qficE30HRefCsJ{rnk*6) z-HZ9pwd9_I1iy~tCGk+>4veV1`BcW-TsJVOS&z7k1|bO5&oo!ST0Ya!Q|X2}B-(qB z%cbkWLc0v3`%5rot=+ZKuKbaqz2WLoq2-X%GvKDF;(*%@OG``fnGb(V0Q9j?2M6a@ z-|z=<74wi}7^pevk_+D{;xxwS2)e0t2fn-O86m~gyV;8JT6}{f<^?5zFmwibbwQDj zkL5Sg$3&ke!URK#+Pj{M-HrQk<*$E+Rgc{5T6wRc^V7@RW5JTz`QVc2;cG#thyjD~ zl3^y($9~VW!qI(qAC9eUz^Bf7GnPzh;L_FsT=&5L;r9=ab@l`3=vfHM(CQp3pQ%&4-VGzz)2J-@I(w577LGI1>^D8$;~ zE`0RB)2_t4wbMUVFRjj2cP+brdG7JJWa=UqB**Wkdk^CO?R)|UhWq_@+Mm%=>GH(pwMF`|y^RJ}r z%$~fhbXZhgRBn|AB_`7ATqaqXTY$XRxPcl1>TLu4?z!m^pHV-J!1$B~Ln#{O8gUsF z55NG_TKr=G=Y|)Z8>Z#k@E8UTwMfP`K`LU3so@nuF|tg$-~9Qpn%QLH~$* z&N1b+$Vo!&3r)zY+7?6LD=Cj(OY`S_IcW^%@U&>lhpJn3gqQ))wVN=IUQz0*xqDu{ zhGWpgL;-4?TmjOl-zDhCH7MfrHRbuR`^1>oC!_$8KldKktxha0jbS<$Ez{%PK_T6=FY{hfB54u?KdYP zP=3u{{($z*P7@%@@onzxIXLa)m41M*aS`r$=0DxHRveqASLvkuVh+qtz)|HBf^o?E zDh_f8=y-FWh!-A%z{7~%oaWAR)RvOgnbkL~eHx#>;;ZhlY?b_*SCzYfos5%t+`JI* zsq&@Ydv4NiMhgRkgI^BR7uVFIufHEVnziE$jd{W`+iYN7m2H#SnKA|!1<+ZRvgm%K z7sH<&mg>tM^9a0Ce6J&ZihE3r3N-f$Z%=i(1Z#kNtIYGx>cK2w%UXm=!{wn&ZtMhjz;9|J17>`>hp2gIdWt@ zI2JP_A^QwnNn+lJ6c?bmi*(0Zr1NV*zAF{Ym4>b8o&Y= zBn{_Gfgv7ODXA$FfZ#ue&Mjx|acoV!UBY_4$VVn)#3Q~h(||RDcb++zugb2hvXk^X zhXZp~T7==L6NGU-aXgwDx4p33J7*50YO9dL{p7WE`_|qi*i>!nGavaFe)`@25#Mnv zMBx0VfBB<0Rt=7BQ$Z4Ud&B9cn=0}aIBy*473Y~>wfA`(80_=-%ZZ4jpe#@Z8#oxa`7<&`?&5wa-6~&O?V>3;qQ5 za^$gWVf{{T3(UIe47@OKuGb9*)h#2OdFJqTT2d^IX%mv^;zIXF+aUP&6hAu4o!T<( z|0Wf7l9bbrfO(#>OpjoD_A+P8)FJt|%Ov{-^lzvI7x~u=?NK`?VBXUwfO$hn1^)4k z|3z=ZIBXl~6Y*6%vlTVt$6?Bp$*8ETh%ldZu??vT{sbSXhZ0 zyY?a_8aDq%gM%hhf7!oonlfc|{etWWpQ~@0P(%3^7+#o+Rtn zwQh9{$rX|zxxB#qZnRIGqMqfIHfnVb4T^1P@&5FfVtRE_4A5hGS;)pGE-?dWI67EF zU|j_b+4!JQPMZqqn(*Bc*^;8yBI*0n$>KYaUw78RJ@j*o@_hr9uO%Qs zyQBGtWJioOgcIs&rb=-RrczKUzX_!-?zs14Y%rHVdh*3{nMn2wqG-b|BnML7J9W*KM(wyTpk~tSFV~z3t!w14yT|?B-N2B3yoRyV$QU>OyJQh&(VHcjQsH_Nt%YoHO5m zv62VQ`BgV6rwu)rJ8QOk_IU`?Qr~C3rLV(ix9_f-hab;9MX0rp25u-mVI)r5&ueN( zIo{uJ4KXG(ZwMqMHHoa)pML7;=<4Xinx~#|CA0L6DO|1uzY+M>Yix?kJ=(ZpgLSQJ zdR~2iti`FP6-goB+iFz@XOW=!dy$JvK z_El)8n}}zJJMnC)Q*5`Wd}KW;Dk?ZYpNgugY5~5D9Lo7r&5`!lsuQ92%HS56fc-Zu|)dGEFibl|#! zTZNyAc_l7T&qmtX!>Ci+jo%OPvBNhcmm|jEm|}|Q)j=^pk13QC=*()YMDwRl@!C>X zTNuDtSU6f7(EW!e26Mk|q&J@#V1p-^;3g^_26Ljeqi23(-ZkJ~th2&XFc=5+%Pin4 zAURj@MEO@?Y&WeeuDiAOT)lK;9Ri`XPRq`h0<>POL}e}Fk`}5hAwdnoVbjU%2%)5` zk9$2SY~ihdwBZ&Km%IicgwblvwVe}@Z;PTZLof$SsI?PdbS2LuLH7Z=I3JyZbb8Kz z2^ts#&X485?}BsA6CnLy+ad2=vTsO$^VIkXp~_Cjr1dYH#-+TKC|3#KQ%Ndt`Y#Hm9#zM|K?8vnq1XzR|3u?z-Q7R zm5hiM+8n5GsEl0ZA6G{8*qRzZzua4`K#_0g4IVhx-97LQ1=h{u(}bj)Gd^-!JCW=n zpgsZViV`=TB=7faJxC6wLgOqeDZ!UN`DuLTtCzb*+{Q=*&VTl|Kk;j~tTvxf$|Qr4 zzW?c`oaRc;bx^{8Hyzx8&u@JM_qFdsiX@cs^@{Qd`0c_oan`tL$WLfM&6ZT^?mz&b zo8z|DpF%oBLXPdEg*KcX5%)=HG_j(n1m_)pGA=#yP3UTB#+tQjeR~0p(!fxW>sMyE zTObVJOo$rLXd(jyWS+4Ft~l@9P&{ zia&h)+c@)>X0xXS-N^VDsl@jandA=;1I+QfwXU z#-8CmSCgP`0EQG@&IJd0y97`_aY6&UZ{v-VvfK zR?SpUS`m!+S>X>VKz`&GtK!#vH32kXI-xM#Y zxsw!~+?0B|2hbf??VT7L^!!ro7?8U3yDxaVr}nNN$(GYUh{1_9=s9XOa@%`Q`uq-Y zG@IWS{6(&6_cYw6}ek7VS&p(&$A`X(Ca{xIw zdlC`@DK1g(^)0(g34K{fDL!%O`|z{xe2+6xR^Y|Z#vR-6tDF9c-v0i;II2G{Tf79* z8yftVj(eW@SGGTf-#2a+5_DC)6AOy*6D~3T=;S4)L^1@#gn@Me=Y^IPcXcmXhy`#X z8M2`2%!oEmZAxh!x#y>!hKA|WvG&m?(0QoMHAF^?9AYHo$Ya^;NeEe{8>_tWQU~M~gMs=5)dy_>Az;oKSSQa~fct;&Gn{+kDJYOz z9J+@Fu&uonhx&SOSE5VAmA|tYl>@`5pIDFD+FBG86aZo4aTLInq}0NXEg6q{IB@Po z+SPO+W&wPQ?yjVtVQqVpxE@_I$t=6Spzh}8ee^9yk}6IpsEsmX`} zSvC;!+PK|@%=WDNcklg`oh|V;zOx0U{W~WRW#GG7`OHQvo2vxp?!B8?Iu@&MpkILX z9Jvo&7><+MB65-mu`iUQ=w$_kV`64xqyPQ+Pw}@~Z^8~P>FVK9^PH|e6m4sS!@yJ!Y zf{W_r2EliLGX(^slF~m}ajz;U^16p^#a;Cy+Z}#R%ra=<$Y>(jVaTymjaS|J4_vwG zms}Fu?HZ6H<7i|=P}nd~`BVc6(-VS~A+ubfO=#nj{QjOpM!RPr@deUi2mtG_^mXg) z2=Nn@XUgjk?KAHed(1i41}m2=$Jajdd7Qj-xmRB#5#H9;Dge6r#P3LKM=wjG>W-%| zW7|Pt#r@dhjzd*Vji=QhKL_k>ZwB+;aRO_D=eTtHBfB4W^ViL#>Fr&vHFpX(BDb=n zUF+`OS$+o2pE@7eNt;ml^&joKaCP%0)8acHzfLO{k1tObxCYM!-F*>H0VtVCKEJ2ScNH*9UYcTyG!09z56z;m-3E&d#52xi%1IdmKoQ@B+gS!xAGKK+dd%ECfl)s^D%-SA21q@ z!FrGwCCO!Q1&6t+oV!1rBsq?2s0%QeLf&7q9XMM1HpH*$r$8?=nkBRdA{&dDjvWZ_ z_g^9?k=ME@vz&u*I%Zz92y=6i921XteK6_xD(=aICQav8+N-PftG`qtwf8elITeLd zC*jaztC8BW+qE?AkVJh5iJ@Tt9YNy0u|J(aC+$&9kV>)RpZ}htaXyq54n6jBV6F^0 zc_nrn<5TG$$EZf8jbzMa$T%u~7md2cxyaa+2V&$^#cLf=MUq&OqL(7+rQbG^LSNe5 zgRb_IyVCEBXWUIl$H{-jJ(0kZtkW2xAEm~>Te_?WtRPqH*-vtSl%!*<;7i|gJNr>7X5drrycoaw>Njx8amSD8TyvdBmHqnLZ^E;iHW+O=OoC3ox1IN9OM5$ zZ%5IXV?<9k@4<<+Q!%cfSVaIMamBQAd`B`8cYf706maI-)BT5BcU~X6`Z~lhfuyar zq_oD5FM_cYco#MQb9a)ci~Z@bA-R}hdPPzU&|?ac<1glYr_l#*Cp0PpGJCg9NFc{C zy{W0vJ8v3eAju4HtVNgtCeU)D-1V?d*t?L)Jv?b=Ou#9UVGxmy0R$qL2LZAcoMG`Q zH~$MUIKkD45*qs72d*mf;ilpqMN|((96V6f3};+Jaxm6L;VsA80-?V)7$oL*%hm^- zkSmMu107=8T%M4&!Vp~oy`X-(c8^v|4G#-&?q~pAHyY<&6^4*!kH17Bwf7U}EWjI% zKS>yr+qm@wbocgn`Al+l8+j;qSf>##pFjINO(KTas14i8i-%j?kG zJV`pKQgcQD9ogn^i6B{W7xk3R`0-@?a*!(fCoj4bzy0KwaoQ0_y=Y)gv}xydTzm81 zaImA@uPB=|w-b&&22&@kqfq}*CjQC5cYj$esOUw;nv?AnDb8#i*0 zmU8VA&<9s11cc2q3LvWtG35xYO=)1*rYLZ&1@rIqxhO2YsB|3jUcD#gCaH}JVBUj@ zOafoL=zUoA-5=tZBbEs-84i9A^mJl72j)GgAuk=(Ux{||Ub~HYNMRnTp4y6v$_mui z*9(Jjx|Mb&^X`w4%#TI33+K2U9BQsYU_`5y2sh9PuCkx)m$<33X zYwq4rXA%HKsdu|;YAZAF!G@t~i7dGbjh{vPjVS!6Y1oxTdCX)YCtoGKaqReIJodK| zM^iCxhn<J6>Bm(H|1N~C%J>qMIEZCyXCTF^nlU=JN^gA0*j{l9NPrKjf z?aN3T+1sYkKV>r=;s8Cx0fu)jI;pq|b$3_Ao!oPi3FIYng?0Crdtvy*bI;<2+it;~ zk34{`-d<1j98z69!zGg=6~!*2V1yx$gHy<}&W8w<@_dq{lhoTGnFn4R^eqw+B1m*E z)aL;Y>HGIeyG93r{QZLIa5nAI`n6t{{+ILszWgo?P5~w)@{p68iv#IF>>2EFt*|r8 z*qd}S-5E0FCE`B8fgkP9cgXU+E_t`lPUpYlmM+H^-t$2mx9mu~R2m!_!o5#Dfk)Rq zC&wk`kJZfY1D<*~|1Da$5XT>Nv~a&>|2kZ=cde>El;-5&Cmfg`MV8rBSyi3u6wm?+ zd2fOD%7P+{D=N|WCR*OCXw}v#_a0^$;DbQ_6EgTb6DAl_g~{OQ2CMG916Tg|huGcU z2~!9bZ_!!mjiq3A8-NTxKP(MDj^AR-jt6(fNlkEWav;|kieZ*CL=g(zM4z7RQg_u`4 z!OOQlhC6EU5s@!o|3D8e-f$NVP+IaEe0<7M{#yc_kq{b#H>R+_+D5H!_(+I!HUE#N}q7$ZP8m<1NFj;aH%e(bn)0H(($fw|{5BL8Zw zMtd}4PJ1-<52u8r$J?JC-BLY(Vq1vz05!Dl9Pz>%zLzS7fWQTZEFj9%ODwxe(w z7;sgjlX4Hb+LKPt!ZJRW!2UZ<1Ngs(9>ARsKZrXNSWhUB?e94sCdoSGoG-EoyytZG zd4Svi=8n63clE~A9&{%F%)R~R8unIhjt<}zII~ujxbZL~9S8U849hdk`8N^3oSD|%RhsIa)QaD6<{RnzK;F)bU6 zV`|tKWdm~f-73R@6&W3@Evvwk>Tv;p?pCRsrxXbHl4jza8ms;N>23@e?=$*wrM0<` zzFje$&7Clh1-t4mf54Cad_4#1T`owcUs;KJ^cP@(Vflw?{W%O+>!2IfRmGw2PtREK z>gk0s0Q7Y2_zB3;Hj`dDGR+0%D;F)pRbT!(mds!1m7DgL@9k3(2re@gLP+!%kOe{{`&@9tm61a`D{p!QR%PTMSpB?3NbDxAI%(~9~gFzeiFSn zFBgo|Zy9r~*Hc`BL-JnsZ^nX#1pTN%j z`vU+RvWzTafi315g*)fyVh9E1x*I(ZFjxD`<$2~y zr_I6-zxY+GJo;F#thBd$SI0rL_4oNT%-sGZ3^`g-Z(Xb5L)gLl#4yI+vl=sYw&9p# zkHw0kk49B>wcM%_O(D~Cs5_;1TiYJ|_x{yxU6B^RfrFlrIQN`%Uz%Q);F9z+-i5i9 zn8{p8VMP|5~52H*z@PVccQkm^vy#p{- ziMt=%2h(KCDO80xkaadHNVntn!YR_qMej56I3GQY@S3#q+P}h zeC$MO@1&-g){oSZ2(+!db_kgtPf?N^Lz}4kC5-`=5CdkFR^) z1+EMRIA;<(hPe|ce-nXo4$`TAAaAh{m=_RW;%#a|s2&^}b`|UF&V$xU0=~LUNvAK~S%Oep_3skb|Sb91Ey7?#HKp z{C(W?#6$e!aiCryfqG#<@H}$){k6@R{xTtP5D8#T=a>%&!-y45 zE-wGbCvn+(-tU!-48S#accHPjljB_9mWSIex zjyw(L&s-e6UQq=W^{CAi+&@XuFXl|DExoSY2C}fR+#$~%&v;UH|4LG80n|ra>+y`#kJWun zQiKy~y#;yxw;NXh9O{w}uB$UREaGzb;$GJhQpTi2zvS-{gDFf+77CU810(%VK;Dyz zt45-$u1jEb;^HF&C_nqSldx#!ER+}jbL#6l5ioyZ{Te*BZVd(o2fVUnWWGZ8n`>?* z#T=ZU`Fa7;i5gS=IQ`jwK((!J%sLL|kDn1pR|if8g#ixrii4o8?rw|>4|`1Igaqgl zIXJJcs&VtK#y^z?)RCTMztttVs(cxykFI8Rgn!4MW7V9XZvmsc6y@flz_RzT>W-Un z<H^Zv}h4cg6J!)+fGXCpv)f5&B z`wTIIKBk!d^%Mj2m_{YFfEbIv=$FiwBuU@?nkh`8XTl9T9S7!oHrfmu2HN#l^~pL4 zpl6F)#NlXXu_LH{A5d9$9Rulvfo`>U(NDM)VJ zmpuc_mz?WvL|nFt+C)C~OMtGvw|FmA)~8IU6M%g2^jRE~&lJG?u&rTcK3DCQ3kF=oq&3-WD4oWfZ5jo5ZCI-^P~LBml-{b6_O`+6Gua)4AJ3f=c{rz8m6MlPWJ5DGbk57!B=gnVK zS9CPuyD~3`Ff*>ZAF~R|Z~~X0FIc<~B_+Xjujb^CVb7o_P*351abP2U)4av413EUb z@sPCco;D@wX(p~8Un)Vm6R~))zlGM0uim?c|JI5iE6OY>x{+ZguwDpU?KJIp0?Y%K zs13&Z(_@P1Ur#YWk7-Qg&($pS-Dne>?o*c#m@$OUHEKZyOFOC_1NH8N)giK$DN3urU;6l*dpk~2Yb(tw zh`Hu31JSv&v>4TiX@C9pqZU%|7pto2S|46_l29H&$POeqUSrF9G8^4W>w9z7|h%a8BRF#c~?BWJ)5cyDOW|Bh#$ zaVAPNV{|>kLpb{B8^k^N`}@*2Vs24oGzcHVIIrA0Iy(hO*MT`*_YU@A*I+l6*G|F7 zb<=#n2!sG_m&BGP#hhWLBwPpYI*#=KJwgCFv-Yy9_xxW$PM%%BTw!9SGQjuw`}g!|TF z)}BtRIC=$+TCoC^)z!vWlx383`K}ta$2AUWV^m{uWGBh7v{KQGmE!!K3+!F0i=7 zKs~0I{ z|Vl^)FI`e2N3X+aGxlg{37pGIJyfa&s}G zrd9yd!he(cJV~MH7e=HO-__SEegxn<`E^fglXP>9UhU;zo~n5G#6`$`WW9J#K)PH= zj@@{u=IFVcmy7m^d-wcDAe`=}jIZVQb>jZtP0ekGuy)%PJim2|3(Vn;cNylZJF^fD z5c9c|${VUHt8n_tQw_;E(KFqxZoJ(4oT4fpoGV8afub-s`yE_qKY?={nA5q_oBG<( z&H+#lzu!C1t+Sh;w4&TH_)#*Z%@L95%kh;3nQU~~+uMQlZ7n!!>Rdmb5O7ZE5AysC zaK;7oF;aaepx!WX5`O#N-^Jx0{si9hy({tHb5FaHYavZ9agSvSlbG7iVuBB3JLo?! z3CcghGP_h~@9q^$yx;!p8UvW8I7n^m?&RZ2uV)RPW=X>)noK}35nG0O@y#ty2!Jm9 zSC%4Y;y5s3Zg~t4IcafP;gsMu){dT8^%#7i8C{)S=65J3A{0_nxPzIpyU!*R%X<9hDE5UTbbLe7eeEIVB5^w!$JtY5!vk%71sWfSmp zSBvm_U&lUNTsP0~g9wcqqkTs8qp&gC2J!Nqwj(t4b&L6|rYPQ@9#c#&H^l%wrWZwG z2-c(pp6WC$VE6#7gFs_x=9r+t@aNd;EdQ)fe_>=USh51s(QrY?0!XDBc(0c+ByAI1 zvr*8p@&}JYuQLDwWm0K6zV%t%@2HY=P2v-o59Lp#@3{LO$v{y-G8ZwuyhKb7+9p8i z?rPCZ6xzBxzz*XBk8XEBTSn?}sCNof*sVr;?Bk2BKfkaVx9(VP=gP}wD_>;azGDNP zYTS*t&s~l)r_V=4K_Qp&z5MNF=l*@T^NC0B0+*bpdEHIo0G;Y!AeF-U1ABy@Dh^cZ zE2=P|tU{cz{-2=55x_l?F2$>g_B1u(!RMdB?xq8h;VEA*L^3jsu^q0|*j3vHX3dz6 zqmNvH{5*@f!rwpB-R!mX)2b%>k%fQ-J1lI#tUaeVK&P|E)qeB#ks&Q=d%j1-$$Xh`D8S~(dz6u`U>y!YYqk9}6`Ggl;RaP)Sgxu;tggHzIRSL49s3@FBS z4`)XFX7^hBW&ajGKebiB}G6*Q;OTrj-d$^7~~5U+RD(~)rE26 z#|wZS!He|nZ{Xuz2?xS&1Cl9pvUx*NC$|J_Xq`xAz(q5q5}w5yLRN?aJ22i8Pnpi z$91z1;;z2E|4Dhez zI}+H*_-L`3&lfNR%>NU|UmY$)0G0r{W5~Q!8`8g~zrNDfPoY~kptoaV#uI5tq6kjcWV9D`#IjMs$=x=-UA*|!l z+O&{H>43+qgSOrt9OC|oCX|(9d|4%G%PZmq<3CBAJv~^reJh^Yw2n*5hvb+f+pI%- z-9yGdgp8M}8A`Bu^pQtl;hedVfO|8SXshtT7?w63hDrm?#Dn*W>B=R4BzY!gS<*>j zbNfCqDkj zP5Al7C&by=lQ>}a>raN09gv;~^mYvNp^HmkT?^Z8JHPb2w~F5tzq$t3-Fg#t?{AfA z@5Qd#drnAy@FoxV1Bb?(R^QwJXUYIvSD$N{oi8Up^{)5fvWwqm7=R<=n2kM}Wp-%u zx&zo+0|d-(*tZFP-M<+X^d2a%wC=)B$e^R^oAjg=B2rQH1fwCc(D)~=^TfTz2fgJ9dH zD3Bf~$QazLZ>82!L|sFJ=;4eb(w?d=EJjQ*#q`fn4A5hG$s{Hw$2|H=pTxZNh{@pA zuFPh}LR-T?@z&K87$$Kv7KVl6hbvfK;lIUoQn88Kx1p><>sof!_!(?2+5Jk5<dBicWpEm2z5VB8c|Dhp31m%vXc!whTCnV|SK-2$OY!-oCkvpidYn34Lyhb7j|_#k zaOrfBu=!BHnGUY1@f9EVm@v#n*4|g$eLI(O7yD}Oq=ro@$@(|7>rq{qRYp3UfX<8W z>UWKzae#i_@h9Vl|L<#Pm^9h*+ROX!zOHsI=}-A}rOoYb^qzyLpW7R^;o4nm(L_fF z(mPVmCtzMyBsrCC;fFZbE-uN1pL<8vu^Z=6P6-}lo#>igi{9t=qU}%{di(lOQdY|A zJ{dAzy3ZwKQ-f}j37(g&s4FVL`)4i>Ru5n@+8xLH#^=<|z_QXBT)OdI^paykJ*pmm zK`03bEA*-!we|p&pA3V+k=3vAdx*XuGeCRNsm+!tZ8x2xtJ1N*>1fO?-ESE9xBIyE zj@l#{Lha^Yoj`iLKRu?HUOL49J*Iz}qQE7jy9W-%#Gw!P=EBZiqbWI{l2RxNRn2uY z01R_c5vICs=Rf-%!f1+;OzVc>0(;S+WiMo58b|{=uR#ZDL6u5BeJyO3g>x=nq|sTs z@&2)YA0;%2iCAHhX)ZY31t&dB}|M0?U+#=Q4Ro~Pl_4bGs*Af7Hi{w%yydS>%RJox;x=;P8slDhfr8$J`yCU1<;-@5nhecfp10EWs@ zT3952dTn_XYD&uxlTSk&NRgy`>%KigmE8cN7Esffk70~QaYR-osJ1+7`V1Vs{7B@H zy?_9B)*K5P2YbD+^#z(lE-YLKkTI@WAIs$oGBU(PSCXQiTQ?KKtvhkspZ$GM?Bx0B?n3prYF@=j16=D&@L3f#s4Fhx3<6%pyj=Ww??${Y1fT={$QlN6 zCl!~Yl!NA84xo`Ewb>m1s}=Kc$Y*v%v{`i@2GR>ka6tAC(m0^K>4smgq8$^oD{TlH za7O|4FfgA@sxIG=tpVDfeu$6ZwIyXJ%89qj#T3(vr5K>c^sE*3ZS=*;^}JcdYRR+X)!SW_ft~VxzRptPx2c&5_Ia4ZZB9!S1`{4aA3e{4aBOT zH}BdYz&S1Kz4e#fj=lcnGuxa%x_&aWZ=C?bXO21zFNXFVY{8>kV!pevkppwzy16!F zAd+{cYafKL4^)Y}2m1N14?9|#gcag4zEG~KtVTJ%)|8gxa3iujzpur$Ku?l#<^eN} zfhzv^8VqV2&L%;($HctcT%5qA=2JLe^e7BmYm0nu+>&6{3!{&HHgL!WBqZe*6o3t- z#MqaU%s~-v2OpfW2>M)wmzM!-J^EFA_#Ri3q4azv{18b)(#$P6o`#kX*U6_MVrc%|$TB7s?wMfa@9X z4ZxLE;fLS-7S226bgyJ-?yse{69@ZxG*+NvGI6Y^n>fh)uT76)H5q^-#~eO3j<0fm zxwa}kc}RHoBqfJr061ED#2{%%6rhMp%KP#dx@yZgK<`3(M+fQ$hmfyp(P(W%)#>}@ zAA@f^ey7++-piQ{O#?lsD-1skAut?i(sVy=N7(0a?_58{g@xK!=6*XTYH+gwv~iE1 zyWQ^XHAYzfvDZ7+iqe{7QMaE!6uM=^`{dde4+ZDYq-r`?(LlX)U*Fb* z-*0`6UzJl48Z^)EJ+-1U-!A&zXy*ixzVXDC^P&1M1!a4P_i$ zcFPkFJT+ch8mNO0la3&RZE~(~nbn(q|c)sX5C9fQhA9Er8*t7-T-1rFga7K=> z!cF~^OWfBUJb;;h{a;)>b1A;M^b|}fu7IZzChNjJ2VJoq>t!YJN(<1Y2}G z8cDcQ!vY-B{`5~hehY5cxeh-%?oC2Mo00PjgR}^)^1|b%F{W*x7afBMj4LWdo*eHl zJ^!uZcf~bV;YT;!fX>E)t^qkxB_|_taqHAMREM27{(dqs}K5fg1@Ly zK*WHkaxRBs=0TT)he|iBJO{#Q{-MHzOn&-aI$uZxi^{JE zmOufQ77;Ly4Z!_pDb2J+tYj!LDFO4^mm~*c`rHOFKVsOeiX`Q~;}Y{5cWvC?ih?3ff_cTqF2kkg zy#-hP^v76r=dD07mn!Gyx$5?$UQR_$`vAQnCm)}>@IAQfqW2mG;5r8T(c0UEp^*_| z?p5W~=Rfr+NR|E1dpGz7;7STPFt6bsyb#bCi;8g}pDPl%xV&Z|E~}g^;u+-Qhku4R z^S2f_ZdO(%D{+6i6Pa88Z+MK(F^>(v zy>cj(NqZ7WpIM@~I!s$FIzDKI-+!`Euyy#~{2x0v;CEYBd)C=K|B5lim?io1*U1hG zro+;9$#Gl@HsT~*__wK@TTQ_seF`&6R2y_{*-K&UIJ!G#4)#vp! zr%^Kfg;CEi7-)TbK}o;;Z3n$Eh29IWo|lixl2R^wEI=_|aFU@#dj5A(cYmK?D5SU~ zoUYxyy#kQax$BHYOSN6nAf4gQW9z|aaN1+Q$d~8GL2*%$0Om6qrdi1`gi7&r#;E3< z&ON7n=;ZcvcMHxdC^6%(!M&bF2;jH#GVG9~c?2blGcOZ)Txd-I-937(p|lcL9rXr$ zYThyU?2~ukrrjHjJa?uB@r^YP;CI`f7t-{LW**@IM4br&C#?9?H!@y5-njA|xN_Zt zShaJF=2^QTP+gcWEQ^ah;QU9D^v7FvEnv7q`0C)KS?$|;5(|dWID+*l0 zU80N=C*!x@{5~%K=qGXWefQ#~2k*g}7q+0YuSbM!sGEp}DRnsSl-J>s^Di)fIUU1D zz?`m(1;-|rabwQ&t2qw2wV{6EL;=t<2yR=WE2*>#OZU9q zoB2KC3`RB1>E*GXRUXtB(nH!n{F)%z<904ynD9Wq)GsklK4}NG_(B!YKymG5yPlhUlc6xWux= z0KD!sWSs}bOb$0-N~6^Rh-qNlj6>~zgEw&^L^AHLg;-#6(I?@s@P1~rG)F^GTKsyt z4iGNc*WK(1GBRpMSo5VLz2bdeuPhqm5(IxMf`0-)P!#Peh|0&%&mY}(+&6368U-+4 z+twr(?LLLIE#p`|sDz~#Yb~zw@5Q6FX}a6m-hw}EeirYLs_v~EJU+5%osjO1SW-8e zbZj1!plhXM(7?F4c+?pV(Cbbg7o>#LX5`uRIm`>6BON+85N>BZ0=SFhGp?F3{mn1gM0`Np7Txz7gD!t3oROrcdn70j*ek+%u6o2ND!ogGuDGr9nx;pKn zCoA{eeSI1@VkUFJs{$trQ1?q{Y8<%kRG%5!8P$4wLSLI(Q5a#?-P%7oYZ;aXCP9Vmj#H20(yC$;a8H<5AwD+fiqWfoE#2b z6JGU9E3Lwfr@tGkwmyd|pSu^kxing*J$+sHz+*Sz>djB$24jCZJW!@acgNA&maIR1 zKb1?te|y|{xMapsyyuCVvAeG`q+dv&UR)@Qm(l+8eLU^|-S8;>x@$c?z34<-Fm0hx zY%{d$&^g$Lp45PlrkCcqz=(kQr!Ibv_>D%xoViE_;0W+%jJ^hMY+<5L`QzGi9ZmSf zj@A4swFF3IeO#q$0M16d>b;Eg((>uJeB44b^A& zhsq=sF=z}ZDkwDKN+=~1yKlfM5U2av_IZV!S5y@Q%z=;3%skos(uzN-e;O3QJje?C zt@LP%Q4?qKkRiE=rRB&^#&9I2m~@JJL`?rSQb1juX6$&9hM-@6u?XmDat2WM=Wzt2 z)oStRL`%^96jT_;=Va^P-j6aAdIzi>XCcFIo8~`Etv--Pd&0KwC6SeI0_M5#KJ!-` z(caJ@xzF0R04+!$ZGW|^j#)#NzT$*DlQ)uXUB`j>KXz}>+O>+qG1{}gxOIG>t$|yn zMK01a^1=N5>v|aJ_Vcxm;^=Xcv8TBajqPnnkGSP`nG{ZN?Fai=A{niU>c&kFYRSVR zBYs*D%V))8V7437H*i81!#enVGCzgJoDrPbUx2bSOlFE6j=8G)wBK6?&1wuX@?rt! zjPm1!^Hr;rJq^Z8MuIA}iq9+4n8@^}jKHbUiB#9!x`hGGP5+!B!NK?(0_L-3yTDxK zWh{$z`<<5exy6;Tj1KN^-!D{q%W|cGFB?=y^2XHBfV=fB#ml;{yORS~SM`?E-Ss); z(%H*#Uc&-h#R2h^tMB(ylWDDOZ^qmke}s!T$o}%;lQE^(H8|+*Nw>cVGx$t{T#8@F znK>&bG+@_xpAwSvD>ppsG6NiKl2b&I=ln8Pnm#aueH`R`=Be9p*Z!@zeA#K3R9vPt zY3m!qwY@-w+~^GTaolg=7%Xg4Ia=v7g#JF*M)CYsg+;W*pbl=NY8izPX z&EdmMcYP)L`18SotvGb(kWh0kDk}DBN3z$jz3Y&BLk8-2A({sJj4@Kxq1~!{4L;Ym zI3`X~4(;Lh22QenR~0OJuybi+3Nzg9ZQtvC*4Ncckr8A8%u!4&-H*Dr)7IUi`czmt zFQWgGM140IlH-!}cz=3KG5M6df)iqj>EB2+u_xq$+GhTS_?CbDYd}J6&zSPrFv>ET zDwY*IbYdGT>GP76inq^Q&x?{=juKte-f=%phjXEGQN$O?$yjRsilnZg0U?F*jRpA& zOGv=#l6LPzv{kJ=Ff9b#|9#g6e4R`D*0wi!l49unK=ltTwBEY(Fz&6@AHuqaf>%uKayQy(g(OzY>c=FUS& zVWHSFOn}3U*UP!lR~(Ch7wAPA*wx*1@Jm{>1$-4=mrfwT;DK?b`BgQk!rSkZL{+7w zt8_=%ys$WkFbbt#0lC$gVN@Fv8dETo9Gi{dyZi4Ms>&;b0k~t2T*1Fftvlv~N#O$V zJElJ;yj--m_I?gV4h;`th=bUgydo?q9p{%Jg0T`vKWZ!w0WP$!nHXCnMJI`#UPolZ zV`bfRTrzu^*h{;ny*Uv7IxZ!?jRVq7&P+IQ!qlwQpup{jWZF?$r0uk{vKHq}nu7`s zSRQKG?dMY;)5(nhFYQq$_u*me=x)QcTc5>#4!-ACj7J%lkTYv;RBf7w`dK$0kNQVa zP!fGz+iHZ5Q?y^Wt-lw|z1{piWlXgJEgP8v9sBiL&SbcpGa1%*wz?rm^4~z`nHRhH z^~O7`{`h|~0C(fmlW_$H=Oi^B;p4-8F42DBP>WD&S0!+a@;Ki4;*yPpe>m;7B+gtsW!X_)EaaX{vtU24fiJ(_*t!EZQoP)WY%+Xl+EJ$D z*>3Yuz&Nbm*kejq9}Vkw_B%b^FpB8^)CW5FH>M|b`an!E9nKU3^qBr*6adb?T-lSZ zneGSy<7&ci-Sq{kZ8T~u#B~2GD19&%1~#Y;TLVVGZgWwCP_DckwZ1@LFc(s3Cx-Io=N{D#l<-5 z)YEXxvgIf%DRr5Ae4$^ImxmHA^~~o?sZ@GcoSW`fqP%0ii;p8B`iQph)YaolI4~d2 ze@Tr2;D07f>p;cfIA+}e22gkN!h9ev!)23xM}m0m*)tdx5s&_TP0b%YA@#>^Y|P_E za0y_|I2^P5NU_hnhD!qtU@prSDpd;u%8^V0*S(E{b5d_7TGx9BZ?Bz=d?jHUEpFS7 z%?-BeNctsdDuMLw)F4W@#NK9hR^}CQY5IILlvHAEN0Zo(t^~8^RiOK6t|M`mB5JxGL|oN1Y-` zXRwKb;=2!Q7v7iEOf<^ILi#ZE85)-e0Jn3gI$3`wi|ieABq3)+5Yr+fH8{K`4u1$z z*`fPC?B0M+KYtG%KeQhy#`|J%0XQP%ji%C58}|kV4VYIX^6pC|Lrv5Ij0}P+SryENu#g27}cBhqkxYURn=7}D=R}H zC&w*^EErk4fBlp>sLLtDwu4Pbap$*9nQzn~U=G82eJI*9(2Y9}?iOK+4tArYv0X3& zsQ;~)zYt4j&-G#;_4WjuaL{al-j8p7z_sAcozEIK4QJOh_=$S;sf|NX@!0@g_p4LX zb2fkfWdA2d5YLtycr6grVu(3VVAD;E8L-pLPS^rfP$HZS<+XACBg^$u z3QJL%$aTS~1nCN}XQag32I#ptxmY}JK3;$FDFT@5pjiO~+Dl3Ry`s33FTV4H8>yKa zd4SdbCE!3rzHMol53HnY*^{6GDNT5#AbOP%S9ooy80YmV>tzHKD zDU0u1QcRL@%G2LCV5V&W^CgqOE22!#X7uWe7?mft?fY%q2BSpJwl#5Jem8D8;4%h9 zK4_0Q9h*km9=y3-&$AoR<+V%k=cy;+^`-UVHRYZ5nK$)x3MsxAmsAsh6Oz7z^%%g< zofiBZ9q4z`o?o%Ppsg^p<%D08eS;`!?Lu)$2`VZpkzY^{92fZWjPe?sR67m5-Q764 zu0a(&beuDIt1|o}od@t#hkJx&;h}C6w{*InjQ>tpv=p-^PxZ>5lQGB2(0;?|>V0e7 z{pW7saLwFPvTykaYv0-WXEva&)lDG&e?k8zkWNN8b7OV)m=0r#0eVcY2=dk5VM?)e zKk%)#JE4)ttDG@M1Lk4zWCOWj@AN(7;c0}2u_g3+)@6jsbLiwt2H=Y5JaZ0UVqpHt zCQ=jakVcJk@}ZDsQ^8%S0qSC-x{yZuSH1}U%IbT$dXSmcHh*QV8L&teJ52e$9?0Q$%8lU=$`Dlw5?m)X?YmQdf~5QW%<<* zqP~Trj!QH5&wFLJBZ7atnqHgAEiX3*(;KD<2Fa02j==cp8qcs+DAyHt8Uldpf11n*93h_g#x4lB`-3h3@6RoV9hD&E1!2$VD z@VGSYcW-|;e)_^Q{Mv;@6%zzdS0C8uUylFX2$<5=5K5EO&Kr2Yxoq|dab3$Lz zeEV}~BgP;}^=m4C`WjcNZ`Y}(0l@36vxzuVnllG#}Vhh}d zUgg5czt_0}Vf|0LxBHi)+I_HXcBO}Kba|~NJ*Pe984sMNrTe(bYP|0F6VWhvvb%U@ z@WFxxM#IujZY1p_fKJDrl@}9O&lgOAVQHXGl}rca!cCLV2T9e1RU012jkY5JtDaaG zbIx#cv{adu3+A#4v_a6#f)B=>h=s5Dy_gXz5lPCYPi?>=4$7A;UW(fB6I^L|NL*2% zIua~Lg|ip*mcO~hRoK$kj$H%Y;+ZVFPs}fMjmX(xJ1j3@Y1#EN3UDCFcCS!-A94+F zT9UmAl5|gW`I>_5A{am1) z#8M9M&zm?$MEPXf0V95|Ez&cc<4qEEx*s0Fj-Gb>cH3$k;LL(~<+MLNA9gEHLypG2 z(cDa~5$!Youk{b%dz&5?N8t&oDg`ETNqupVsfw@01NpqNv=+aedOWVET_TQ9qkZNi zE#Jbyc}n)rkftebU8Yhm8^Iq38}Xg~9oUra6JtmbXUsXg-z!bDnK{Kct#CZ*atctD zTOf`nOXVez)7^)Hwq7npFGfXG6_=tDBOwuNCkS6B_@iRPoXzRxmL1qD+N*<_ZB5AQ zACl2J_{jNh_v(aF%~6dYj7h}Z=!n73ZhaU-a*QY24A;y%HIUcPK48DIe_A?$wr|Jr zZ|?up<|^W|(qlS|$zfwR`5IG9|5nO?d1|mk`r^TT))t#i_P9m^>3ZB*_ko7^H^MlY zN6k~_VkAxs4-E2**PC1O?fS31$Rqs4r$IEp9}?a%cLUBw(0*p*(4_SQVm ziyv)%0uQ(BR?A<{yNHEp3rDS4TCg(u>H!7+O_YBl?hw$aAInB=m(VHa96 z24n9w(~d1;n%a7XK|ceWvEV)IaLlyY&a`JQto9xG#GM?RBB7fzkN3d$|1_S`Q_so9;?0R`V`Ukx}^_GSOc=OcxIJ#z%k&d2+ zj0ELgJLCVJK6C&#@85!(4s3<@vWQFU>#AL&Y6%PYY58c#FToY{OL0l{OfN06%HG`D zCAJY5^QmM&IZ*2Ww9}U zaR3HE`Mz8Ce&qHHV#V$2Fnz{!EI;Z=BuF>ZOXbybVa#{c7WPqq*9{WBn9J57!rqz3 zWkCa0%x5AvK&SybYw}_tlUAMp<~cDie-#j&r%aaGM>76CNEVPyndHA^K`&~7D;KH1 z*}fV#?A{>uoR0>Y-N+q(;ixgX5E0hV`h*|G@%ocv0e0^`A`KtByU(XoPQ7C((Qj~Q2#u{R;w7-~KMtPpAzp^!>^<7u`msM=bPpzhiL^1IUsNr225_hP!=v+_2sbVAu2+ObX273-c_Pl6rxm;RH628j(!qBmqj!C<6?E_e~V~v>yuTKNV zkDrWdmY;>m#bw5Kb%Ckg-u-!rB={wH1s+hpiA&`_z5ZSf*t<1=u7P!x_5hdS_Vf!w za*#}~NkwH`qCO5saiBi0yhf_md2QUJkG|x9&G78i2GW0iKFg3D&nBFnlp3? z3$(O?=~m?A<5RT@aM^_ULSjxtT17kC4`PUq10KUm)-i(*(ABs{w`+#G@U5P0XwD45 z0RQq*!oR~Q6Q*KeZW+=tKE^qOGo2zVOO}d0cg}%F(Z{9l)1TOi*&S&tUb+Me7cN3Y zWrdKwYcakAa$8d$AJfi$_D1)-H|{j&=4ZrFaP%$TaLfs~=!~=dD&*kY$pqWz0p7;H zwCyo2J#Y2~F|yiz{lYVaMhRqH+2IU#BWqS$w*5u*?f+o^FU-xul+p^s6w~2KF+h*$ zRYNXN&x9}FBUa^x+GxaLJ{uW}T@??kMV(_x@)a<5!?5r;vrKSe@y@Tb&pcLTe{~S; z7o|lf?V}9;{@J0Y-hRyX>t0v4H{l!WA3_tCo-07C18GNZ=UTivIM$`u^(@q_BXs zE;SWEKjVI5cT0c?+C2bcrLW@Y=1m8|C2)SphP!ZJs1N?}k__jLo5`i=i(Q}|7T;KCG*GVRUvnoZ z`naM}lt%2ArQ_JnfACN8TyqL9dzl8<-!XL&zA*P#OfD)j+H^*53+|2JxslcHQr;%@ zGt5`*U5_hYcuWFy4VZgiRwgzu=mPbOrQJhGB(%PuhjFxWlj=O}GxzqGYkn+%{;9f! zTxxy<2k8o!4+*Jxw=!fY+ib>|XMO28xvoxi;ka&JzDc?7k>crrcM|4)C!75%=o%TQn+UCT3pw-$r!jM z7M9@p1!oBR3on|YfcaP@+y9~dUsqZ#jtYz^ro)wDfF9GUio|42R-Z?M*7|G5U$iQa zuGSw>u|!Gb*eL#NPf_xCB~4cgYCTxeanCc4&oY1I6MZ|f)=XAsT?cq=KrOAaNzqwg zdB+Sd1jzqu!$Wwkt!YfsZW|ms-d`E71=7{My<~ELP${|FncziaOVo|L0fBtYDy+b{ zRSl>tE#r&hsVESOVtsKQL0*~;Obrzns_s$r^v3o0(ocVg#)GY1SiKx74V{VSF#qcm z6%+^wI|2169K6$y+-k}zP+d`ptSaq*mS#}OJ*qmp4#J_U$Gds$9~eMKS10zjv~VHm zAevg5g@nAFyA2M?y|3`qg#Fg&XnFOUZvBJ__{hcY!FgxC!K}n=3+^aDoDC?40bxBI zJ1s5zcXvJ`)PFs1aQEiEyM7)ns+$*`eiW#%-)93A5rBojM0H`Y(0B?XlB8d`?m-Qx z`v9HprxcdqLo=7-Lo=4a(6&(RKyt2_;~%9*a|9VPQ%6z>wRaBESMA&A1LlOh>gmxR zF1;3N@57u~HZ-i2)uzCc8(fZ#Q$!%i6{^rG{-2cV>pO{|LD{2r^OoubY06nHxC1sPQEASiUu1^BY zf8k!`JCg={R4@6tw!a>lVclCgnGy%m-KVkPO4ITWui)$x39X)T_ z^X#%}d~Mn5P*GB9RfpD3^wlua7#mjK_41L0_H)Vcmw$E@p4qfsNS%#uV)t1)GR6WD zGJ~1Mr1g77TD}~tS5$eRy|AE=|MCT(FXH!AcS%aD+vX31kSHm{~)g0xz>!+@k=(jxEx=Yb1dFAX`WGDHBKm|kFIG^l$$U1 z#V4h)GZ&~oj#c|NXka>_r6+Do;5(9brRrXO;2N9DNt$suTE6x82$+}G;SiEF0X3U%^49B^n z<4dOxuD=)C`#MA+>(}iSAf0gu`iYBF_I);g+1&q){hzjuOew1rMiFC*>2RhPpvUy8rYNB9?f(v0!t0Cs zs6{>tS)3WTA=H2HMHoQ$q~{vICs3Xn8-RON5UI0w4)qJ^4D@fSeR~#imyPr`@mxnU zuH=jOrhy))7UxKWZmMJZ?GajAQou55Pq-gG%lptU%-n9lm=}*)=j%*_K;HOuQFR@@ zeeBs@CYX_CNPqP@X>ehht!BIX`*AhxGr#?2GcNtcW}+|{eN~sBD^RN-B4estW|@sN zm^Nl;ub4|0Nq`NI=H>R2|w!Ejvl1_{3w$7J*8?Aj^dy-pU;Ai3%bM@E>e>4$UJ2C~NJ;if8v>_MF*Rw0JRU>*~Z2Y%hfb0VxoXl+1AF9S>(554)VuyLRlSCBe;dy@~~05}GO^{-)I zSPy5r>+(kfP}*Q05%IzT-F{9WHpYhev+}jLx}F2{vk>foA<}c3EM^&OiN2((|Mn+b zf5*+jQoG&%RXz;ht==*J>}O`BXZtnlz|5j!JTnWt`{RTT_@TWD__Y&I^#t3$?az8? zF(^oaUNdI~4sF?CNYGWj?F7z!(>eI)yDrAWaTEMBqNHy`qe<|hmL5@ye?88Sbbh*b ztpGgC3Uc0fYk&G^d9p!9Hn0+wAC1a5KqtTvW$k^{&NaAl!@~lw2jZ7t`NB!_@wr(? zbD&-dB|#^$_id|gNZ_WNr(2xs@2Ot3f1?l7RT?@#7tcz%{9ihNo7=W9&NC0!X zC!*6U8oN8tKa$c6D*`MFKwmxBfnWFR!s=9)nJ2A2rWcpv&EsaEoR>>5=DAxge}7VL zB^D%0gn>@lYSiyA^yQcXiT$P54t3%)2iFKYZZp>H!{V+C<}X-?IdkR+gK-H_7hwaD zAFqSpaCIH8=j$8Ytrv8(+RTSG3uAFLC1v>jM?Wn>92_3R|8R-?^85y5y~!HK66565y`$v@96l(YtDZ7TPEXLHS zQ&C)89Lh8|H(A*48Q!(_PP7P34hJO%4q?*z{UU$wef^to1ZM!;+PM!mwC_T1PY=2~ zJH3$Sb0!i=%_rJSBvlttZ7d8_+Z5#C4#v%L@uoTU>^iVv5P97@)`Wnj>1g zq_us)k$cM3d*7q^972J2a^kT8dK56HX)8Y`HwNaf0#f_T`-i0@$V7@T;h6F94X_UT zz2}W_X?ZigzVRV6_V*a?bbzZsx(kGZetdtHq*#TE@NdK~^DT1AviP@{2WH+z2kD<# zauQxQX_n^V_qjkVg{^~`rXnuE`IbGq@a^k@go&oX$@BKdS z1YS$&xSn4NbSrgoFE0Ic2y-Covz-T~XH4eD7^2Md%y-t9srhvz06gc^*Wul7dk5yv znk^%6!WZZfdG*4m_t}6&Hh>gK}lJAOwA>`&*{ z5}qDO)N>>V7eIac6S%2$Cpt%lU`o_=fUe)W*NWT%Tv|JuOU)OfA>XwlL1QDGWqzoy z2fC3r7o__QOIL9x18~3U+KE4R?+v7(u0@F)oLoH#Csx!U&&d&S(X2j!OPWv3twu4D z=;Oe=e`GjNmpvR64<*fS)6delVYnH$4IjctcOQnD4`QUM6pU9(`N3Wsv-$w0O`m}U z3m2fSzFt^$w;B6RR6F2w_n|%b{?2DrVNvz$HWajVqlyF1)8GDP+}^cc7^x#V*wTvB z;2>61)Cyp3>@!!C4e*Bfv)%Oi%oc8}=|qK9>HHJ@e{y*hieBQ@f|z1@tx^ooV|vXJ zf%LR}l#C*M;tiQl!a7A%N6BU&P7z7WbL2krm|pqh?lT{789hO+3_NhIZuPg!hwgv` z&R#f|4h-}NVE$}-lk}rSEh!36>tNl{@}#8cjvZE8IEC*^cf5o>AZcKt5KwNr>x)=F zOJ3|mCAoQ6b;{dOoRG4~M8PARm#h9deqX7jK&h{JjPEi<$mws+KxPwd7*NNlVCQsYU?DQ?g@S3?H#}rX$K zii?mQS1$b2?>LAf5A+K)_u2ecT~mYH++3Bb$Ztxnqy_cijrU3EIgsoh!nlVwh;Pt) z`f}uzm7p-=c%P6px8vJ0kH?Dg5Cd@8KyNl?mY1Q3_hXTI+ig3Hq5$|m(f{XEPe4pD z#biF*Leg@wT{Wa-fc9)aKMZUK{Ak5zKsJMO zPFUPBFM6Esnso%;J!={K9BW3`U{y7lROJ_i0(Fw6uiCv%0QE2ctKxfG{TzH| z#!*5wT0cTmm!SJ&5zR$OR1_47quIiUK)aBtQ~o#*S72Nx0^S{i{X$|+$ES&O^!e5E z2k#v@-`0Bozv$S3rjY@Kc+Gm`_cc6U@2Q`MnS~WXT2AwJ4tJlEEW!LFF;g<=9U7A3 zb9lX~xDMsH68A$G#ubCR(9D@MeVcc1^gbfmbGo%1BaN+aj$Z;+MVu+H(}MY^>7tG4}sCRpSv;Ofi`h1N4{<3!+9o!Y6fI z0;Jsi#XK|o+u=jwWzqZFNu@Bh%>L>iZ=X3|%$wjY#4iBLoLIOgzyN^v9o&uYzVIX( zx%4~$i0LXDqZ0v|#sbtGEd!wvbrw7`-3;gZS9PH0kByQK70ydjrZc^?3fG)?z5xLVXZB%caiy^A-q0`&W#whap`+$>cA`ka?SD76?7%Pftq0^; z>m)s|esT-ac{xZOyU-m5I6%+v=g(Ko!+Aw@;)u0S8VeZD2Ig(Iu<&7Nj@4flhLcBO z??x%ptjck*<@T6jvM2`VF&$Q<_NN;bWv$6=Oh)A_zv`YQW7=O%z&xICULDjsOa|bF zp{qRW;Kd{bQFNfL2MW@G!5(~h!-IIH!%}CL6bf{0pgD?vzz6sSsFvXk&H*Fm`rvNfROXod6)wOPzUgnskzat5CfB&NeBM>{soVrVcJ*xo;K z4jS&)>XDiDHS%*fNI&@$RF;*CG)qcKgfxBOf&~~8fL0VGNw@uvMarFpw)C~*Q=1?3 z&z+V9C7}L==||zT%1L8m2H1IhDa-`L=*Y=IeMyCorrROO$8{u4-}Hz$-`pb|=<*94 z$M%`&%kkFQSsr7K!g}^IWY+X~P0e4FKAk2ate4At(z{_UA*by=gXv+hU5r_M##LkY z0lD5*4h#?A>n&^X+(3ufxkbKZruqMqD<|UJb#qXV$PppaJfZ?VhaQz%fpLjEvCq6$ zS7#4vGa;H4DLz=;l!V&34q2U z=;n*c>-TQJ)jL;v`@q8hEm6WT4ZP`6R4;EV3pn>Pg|PkU!jGrli%^Mrws->|(p&pE zFaW+5*OIC_e16GEm{3?^hBYcRymD1tk_!9P|M-*KXAU4_x^jhs%t%84NFPwD;4D0x zT|PS_+Pzmq#{4y?;U#$dnhCyHFz&Flv-*vgwzWE5eDMX?(6Sfl&AZ|3Y1Tlu8CEbg ztoXtt=)QlyjVq}vNz%`j>h3hI5_l=)pl11T$KuHO@@(uVDOwni(A|y9W(VI!SQ|K4 zY2I{jr%-!8APvc>n9m(A)!y@qvI8X*Uj#rjp7pe|P#KqKR~Hr|Pg8qWy@cor4$=vz z`@mcqfanjOdM>D)jmsLAV-jaLFs7Sr^XDzM`DzkyrZXDD-MypSq)Wz{E7f?|&B6R1 zI(FgbZQHbgLykRKz84f%;cXLUVlKbZ&_v*v?7B@$6k%RYiC_$nDt+4JDvXWkVSSfR zv0ZrX(0&Z=+%LfM@Rr>YoSUs$#4vX%*qq5Ciu{2Tj#=A;8C4T7W5x_jsI5hQL4g42 zG`4i72J!cf-MFK3KcMmlckg-e!|O5NWW13rZ*~K6rq%*n3O~Z7=${%HkBg@)G|Mp> zV<*gCSR0ILi&4y>us%3iTi5eq#bu1rSAYjPJzi?5}(p;oqE8HE97D2@Va!u5Qe4>$-$U(7Cr88@J+H zTb~g7U!g)uFr$N7yR{gASgoHJLZz&Z70wQ8_>&xjNJDa7!Grk-_!E9*##qpQZBZ#c zI`3#4KVhoR^}NcerhVqu-FYjz`+5zALV)x{ta7s~z%~HdnG5Vyu9U>t zXC*Kz3TAv81E|Bd(5@H+I-@|3$ZJ_FJfER%5#XNMOHN?{R(|25D4$%9r?zjwuGS{v zbRf~x24~woI8BEvhJu-PxO%%2kfQs3+}QHC1dR5iS85s?|MJS2KaUVkR<)GhGm$PUn0A!2CQWn^&{K-Ecs)aZ+YG9 zU+U;I<~g~2;b(_l5a7Irtd)C`au{_`%-hL3YUkjMH4XB}w2Y9BPfQkIRt{%Q@XU5` z06aK691a|_)ur~nJ2Q;e^{qzoz(EXmb|c-=j&yUI_ujEUx+uaCv%n^g7v(M-7)D*k z5GLmoU}8}@62%4R=6sV)nLg|v=@(Yv-80M`_sYYRJh}m?fdOr{;QfY!`ofcr;2kqZ z?Ac%3na1@;pBJ8gy{pKI=w`Gpu?T#A@tipixI zpvQFB6LkPm<@466Stdcdvj_uli3A)ti0PG0?y+h1dFB>kgtdMMzE-}B@akz(*CBj! z%cFSm&;cY84ibD(Mv)~Hp3V47AWtHTa1b17T^%0QHUW9Wy~Cp4g~nllNF~`11?&c( z1WI%B@s@@K;zyRMnU+p8F_^FxczV+Y{J&q&KJ!+|(9qJd7kA9cj0D*pm}Gj)vV1Z; zQ}KCabD$)IdVP4YGjmUR5U48ewkkY~I`jcKGpoU_7U|P#VMbakSg-s!uecDWe(__d z;NYC7sjCC`ZCWeaUPc_bHL0-+JO6PP)KPOmtgl;q*8rH7FH_6N@vQil6cvj-=|zQw zu5r!`hhJ%TOA)EOpK!tns32ANY`}Ib(!>`j*&7Yqk(GC`KfQH_ERkE(Nn(HcVj)!@ zRk~=W8AfAaLda*DS0lmhlpM-v7m@Xz!e!q=MCpmBJ>98@ex|M|Qg-^nHXg~=RupE-}9$Vp-j zmzY;NIpREXq0XKerG7?Z@^E@O)qzVpo=0h82hyn&hBxox((@5RTgh)VK0|Hf#3ev^ zv0F|i%gLBI<}g`X>Yo44-MUX`s#v{N+~v%kid-&5A8hYL%^<^lC%!Yf&06J%Ec=V4 zsC7-Y_88T!qxzn5i%K`Fzi57F-k4Ba<{b$bQ%r|5#Q;5~m?UMm#D~Rd0Iq(%)L!@JOVf4#?kg6-FbH>}IF?q`;r}f@ z3AKeK)>_YBsG49}kIj8g=b3-=ch}IK6sG5TIN4 z20&-fz`FUZ>HeAiL-=Xy3wU;*O-ozl-DemqEggr8>*r!oNexDryKkMCmQxZ%n362S z5FeX6XuFZrX3GY=1J4of2%ulw_ACl_G$X;CMqbzhX7zu&%3Y+L19s99qziJjVTbu< z>7}$>Md1vmk$hnuj4?*orejkAkKzCc?}d_t zHH;@+DiOSaW}U$yPM-aBt*&di*D=FGWwWlS*6{q5B~XU>#= z`v1*8^G`aX1wY|Rg>+qyk$@TjG8z;0Ym*SbUi-^`$F{ChGTuOGj#w%bQd#(lLJp9M z%gaJTOeB68D*@pIUaQ1n4wVXELkw z4h-Nc;+*-t_rTin&R_|`-iS>_QQ&LyOhLL?u%NnO73ZRBuD~GIa2?tB0<^~3T1L?C zK?KBELRz$3ji}D-HfB6nd2#b)G2nmZRMk+AYd`%_3C@|N;bb_;!TEDX4&dqq%TZX6 zlJ48@x>x@BH0U(HamL`SgMjR`5aVZch0>&#hMIJJE}g00>Z)p7a_J>Fd-)0^k`H7E zn#kDef*2Q==Ky!&%B-&)eUWR{2f`k##txxL|FFJ4eWJW3Ei6#}d~p#!LW^EoT8=vY z=gYm^_Rc~2w~lPHODid6pPNYw~h^RV7?1? zcO5ZtTlrBPrLwRHADzA&*VNB6mf6$%cWSB>vkJ;(d+6k4Jd#dlud`eWFZ!8;eW?2w zJ~F%;MO*fxgttcl(9KwB4Hx7ni)za#E5X?Li@?f?RS*WNFw{a-(9wrN4$g!AA-IeB z5k%(;`cvysvUo1~jvYt3vlqM8emtHJO1auLhjkwGyaCIM$1 zAsN2(;uHAd*2lwm1GT4u3l~fU4U{Hbdl%zRCGfdmIB4*eg<}4y;X8-wTKfs;$ z-XlRe3~%N$aszbAR{+<-csBo3DlaZ8!?l0*af}TOD5YkfS#v3<>^&JVmKQR=gh~)Jm0k3K!?0gGP8jB1V=<6$Q|qN z!3FE?2!SaS7?moMl#%%u)v7DOqqww`|CNO9EE4?nVuI$3{!)&u^Xbgv&*K)*cNU$w z42W>5g-zeqdCk{Kt}Xw}vP-4*+?H-5uoeU5P8#cXZpSCT5#DDmt;-`4j4A(`SMRg& z=f4L%h_VCgJT;43br<0ad3}rzjVMlFFY0JlW&?IzevH(i2~Qf7{O%FlwN^6l<~hK; zD=*@U217Xf%qIJrO1o-uJ>KxAe-wgqM1ZQbs~uD8>eV$^Fu!_dfbjOjKYbHNpWC8Y zbl9Q`9pu_LMahfgf8S=-&Enl__S6p^E>a-y_ zU9hGs5C>^VK+dIXAjTKQNlyTEVeS2Hbv_*`?=7WO2B=H#u-ipWEo3gfXg-}d1X!sr zwXMBV`p5mx;hzp{!lB__kQHVNviP9-ZXix1=2$nnE4e?!ZQ0^od@x?Q#&O{ z2kf{EpZx!GtEb>E=B&n)l5(kZ&ZvGpt)L7IsbYEDsAqT>q-_C*9(LLNr&^poIVCDNp|ssNgR- zAy0YAQ=Wd$X=r>D-CQd(rUgGuECNh6;17UgrO*?R+Wfl+d)W~9hml0&YQ(o?P2*YQ%W{jr8 z;z_~cB}*A=f!{2C7@3sh(P*aOEvuRt%~k%O34KUwni-6tQ$oXJyBQBorFmr{#Fsjwl+G|OtMap3Xwek>BWFU(%_31BUX z=xwZlm4AlOu~8g8aU3(7TTohD92TQqAPJ%^!6#3i#8b~cjn2MaOr6q%va&LVA0pW6 zKTSRW9|VNgz>bGL`+$$r|Eda$ab?2{oL4gi8+%Wpk1zg-y!VZd;Ni}r61Xp{s6%r} zMRFRHuWTSFiJbh3E#YE^kcd@WQRUK_NzHiU)U(jd!SIW{r%C6D@+?62gI&k)v-Shn zHQ0@6u9dGUC`L&typ5!FbO1Ytx^ZXQKK$9y4S1;c1V)hYmXRQFkNiI^xD5ZRWhtr@ zPlCV61tpkUSczi((#HFIAJ>{^65@#EIcY&AsqR$y2x=c4!n!>#0u43D)K#HKv{#|= z4igXifz?iQ+_2l3`zLBYrNe4>J*JX))nC!<7BHUv(CH4~L_1ngg4@o!)Napi`|y>6 zTOQQE3x<7dK3o5B{rlXKcyjr6-KksBB7&Qa$6Xwg5y#cU{9el2YW|pEo?h*g*P!Pq zPkH*iCNW`)YkAU{j18=mFc2v{sPR9`r$x-gomoc6_dWU}3DBh%C75ZAIa6sPh&1S$ zABAvGQdWwh;z+=@a4B={D5uY^e`pyz0#h#V(>b=}P?h1pi7hZp1RS80;LS4@Gf=6vR8m9a$uNQ%lU$h+JTz@o$f( z-;>g%X<5f5;Km>M)ekazZV{^AqVf{?J3csUr9XMbG~E2>|C@g=lQA6aXvfbtJ&pHV z@miD=7UHMRJ&pRxYFspbv58JDLa5@@_BHPN+&|&;_C2vC?z?J@@@$tA;>4nih(}>K z?s{c7PG7&ql=eJ5t~Bf7nFG(Ds1O%ibP-;6#Z`8ERM?k{1<0!H^ML~$&W$&lR?h)O zcem}wzsUR32dwsUW8*w^KE1-v_e9)$04$q70RTCmt}m-ZO~Mx77kb*TcH_EGo36$k z6*w9M0x~M-RV5e537Z-W#64`Z0@&Z$ycq9lUWm$qB8cTtaeZl7ssJsiG6)^NFaXy# zJcPtWiEN7$K5*x23l8nR)UI?GpP!_DlH`9P=_G;i zY?KRZtGMVKSX)+(x{`8f@jXDE^7QJXyaxUEh~(Kl#>{rb{0d^8{wI^@2R&R%BCbvI zt}_xLLu;?Yd7cQPgbbjJp+}bt1jBdtJda}oJrM{uLT@OqFxo`IN)(fZgQMLeq}HQ`IEUW-eo%tVQ`oi^eulGg(3Pq>M* z<=^}xvf+?BZT6St$Zq8!_g9m15FI@FYx<52r)433OodVM3R zIS_t&|1PYUHXB6+p;5wA5DKy>rTr8CRdVh7zQ>-mQ}uvz-Hvs>Xqg6C@}cW_bZiVq zjvvSDmgy)Gw;3QXi)A{zf%Lpj96opu&u!X-($Z2iH%&zhkh!GVuoti;J)a4HAZaYD zn1m}Q&5%x8`r6F`(C&dweE0ZH=`O6kS8=eK2t2Z-tMlRu>&c542m9sDp$yk77D_{= zl$!X0!N=#EgIQ(Oc(Jby-CXl;;&H(`1@M%InE&v3O-&R2W9e&fb^UY+fW@L@Ay2A^mXjE_6mI=9KQ8B1yXsBtrJ+{9nu^d?|VGX_qc=HR>^*n5;$Rt76tX5sq9 zE2Fqv?RhTHOv}P82X|q~S3YhVa>2DrzVL28OXy>Pw+(s9(ao!ymS>C^9v{QN=!k6Z zVo}mZS*P(l<>?hpd4T?VK*FN97>LAVH(NLcvti;tssY{UeTX{FRxOE__a6$0dCA1*Gk z_z1aUYHM;3LL@5qWZ{m%E)bez6hRzRG7@<+*U0_d%B%4Qb63f`X`|rrC4;~!FlLHh zoHKvl-~SaJ+`NIy$YUf;=WcWBIe?2}&xl-EGNzD>;B-_qrodg#_PYcAk+7CP)27+7 zEY!yYQ8zGcr!an(U3L*}{P+iDdl6B5@99S{t*!yJ6_ps~^TUP%yRc^NLJrs~aJYj5 z_R&#HpVR=oK&4x*Zlc1#+>U!6L3(V=DkJ&}M8^R9(@b(}q*wU~kbd}hD`rk@K?&LJ zE`LGJ?6UCsyP@G>Y~Q{OPd&d04NZ+`teZq4b^wv_euu3hDYP6iu#^*A244 zVKaW2b}tu8^|%`-4CS-Z7=ecllUWf`WO|Fj`T9;kHh&6Yap^lU$Gsk6wLX z%~e>`*lg#`ty?#LzINc24gE&Lx#g8qz7*&u#p_GQjmPyz`AFi+m+v6(E*Q|o5Mke- zpO1UO;Gno&N1RX3Hf6lT7Wj%Ky0)FCS0Cj8`YV_;Fc)WQGoFDe)Tn1Ls_qUgOr?;g zJiVG}hyy(V%#C$+AO^RoFhIe%tvUBk<38EJHv{U8^Z&7PBNupwqd<-ZhMqyE{J;S8 zh?P`f0f>Ixq~h}6;J7HgM79GfNy(R^CDp&eI{U4&m*fAfyarR1h3X&{Yv2h9laUR( zw&RoE{0jbg-4Eo}cAdrOS83gmOi?ifXC#5xpw{{5D$1Fgi?}X4n|3%#Q~RZB4uMt0 z+MkuSC73&rGDq~{{?32hHMs8mx5>E00&NioL&DJ3R<1=~K7BSCDr@lEp}iqs=Kx)t zQ{H%ZA6CygOU9H^AR}d(YM5IA&QI*v17E%vVb0WBV3$>pwRBk2?l_I`1%p!^?U*}# zh7_oEI>u|@wkcqY1N9d+ZbDaAH=3K9QC41#SXudmc^}~QeRs8#*#M6duh5c<^XWpX zzNNn%ed$qITK#;^eng#5Kd-h44Mk=CI9)zzetbYJi3tBg3b2T>6o7?}4Yj;~zp-f^ zZkx6gcc0p)wCR*DGf0>*3+h$;Q_aKw;qnj2j(5a zgVCMl0N`6($Oax=kT?PDsxPjV@@lL*vMc0YXz@>Vam{%eW7XvtpInQgrpZ!nV#A{d zq(+?pa1GMMW6B2UAY*w-kNGtfz>Fr0HB@6{WCWSM0bqYC(B5sf)VI`3!WXXh+=1?^ z%cL@)?%B%KZTqg@B+62r-uSh9QkW#ZXneWa0{IO*{Y8{+KbMsU`M4--GUVD_YR_1a z8}mG8i{p03B&j_R+jqu{Nftk;K}f73ipfw2WI-ON=jl~QW9mM0 z0Zc4Sq@rw-Y23H(UAazp#tWzCdyno3x3~+E#+5<=f~m+t8CQWdE!gSsS^yTB0I}7* zurSo#kQ};rUPWXHh%Q(3H_BHKQ&LmmLVUk`@=W~wimP$ilv$R*#S-Flzw{0a;_rX_ z9enmbzmC?9Hj-DB9%Fv~nDgRFJyf2|wR&PecVLRj^=)ry_h@L~2(}4HM{*+tt2(@U zzTJsCmjoEt9xlJ-jkx+9Z+`8Og)(6UXt`Tr`);mH@8tkp`0V3z_>WKQk>}MHRn~_#9DMP5z`qyN`10Y&zmOZm zj=M4D-}YX|f%eB|uTtCI+tDqqOfxH_0y9Yx@lzsICu2Q#nZ9E6vi8BkeWUP zh4uWmq!HI>NJRbsrk0i%^=7)y^MBhH#1=UVfFC&ac9 zm51=}t2ey|E!B0%39Nl(;pW*b6Y5UmcHP}@ITkrAKR&))zdnDy_+4JJeW!5)W}(^j zBgVgOu{b2!eZj;H$+^SvL_!6`UlLIFOl2l)p7NB7@&Nr6L_+Wx<^%iypWMgN=?RC^ zgNv9jML4n5t=L&P&r_a$A4#7xA5`beSwfr6*3tuKD`>V5maLMs`-Cv>*^`H{z3UVK zUi2)wa?=ZDL12M*?WX@k{^VA751#>fm2weqMl1NnpGh__?^DYv@edrB-!|uLsX>R? zcASes5k;Ic|JPrx!^gk&MX5axv2i54$#j_Knlmk%A-eJ{o*PHgpGAx4)8vL*+Z;==#E+c#s@q{*0ESuJ;A?LK`R%cjmSn!Zqz9;P4!SkdMNdwOts-yxc8 zB1f8!=%_#ddUrbQXGub@*uLH>K>D;ulTLlnI?)SUI=<7#Phi`YZ5ZP)bk59KevqJl zeE`UP)_#*fO13y0cVW%_s`}`Bx(2*r3%od=E?yT@PKvIIa>oHva9IG057fAzPVpp; z!|84B0;s!yNS$LQVz6BQ-uClUxpI9Rqr4xCR+M72t_lIyglC#2A`Q{yWD1#v5xf#x>^SWhF=p zK+iSiXzK!6+d^FYn)78rc(Lyt3-&6YS{qTP3o49Qm^3;(w5H8~@@_q*>sX?$dkpQQQ4M-%mz zr&m7Zt+>BJNC;Bmeo%2QtVK%r2^=>nL>pn3*_~Mk0TbEolczkr(#Y&086WiuGIS-H zIc_nJ0g!8$(>sa-@@(Q~PGsg8__v)K@t4otA6jk~4k=r(GS>+yprQTA9-UI$2a&K* zTwaC(VT_GMS5XCoAtmp}G94gdG%#Tz(p+mRxPptS!XmtV`ciyg&Prr0n=D_}i|y>+ z_-7nzKOLpSY*0MusjoGV?<}`LN^(Y6K6&~;4@de0k@uWNsLEt-RZn#E2|{%>?@a~ zg#cfAuAii|UAH9O-R~0t{z+v+S+oBkew|q*5beGIlS(U4U0i}!BGKtQ*Eb(%cl~-w zAy2=jl-HpDj!8^>`kc8G*8&Q&Ou@T*BA32vh z$QW$+3`~5cO)*Dsa{YR;aLrq8!fW1oi+mPGJHGwQ!*WZz_!FA*dv`vMHM19RO?eIW zww=HL*AUK~+=NoTC@a2QFpF#03yaagLDbRi4lHhJQHwXx`Ez|Hy}Y&>tvhz3udTzb zJHur_WNBv4C7wkO``v%zh=_BLYj=evebMYW<~Wzz#`SWCEs=&34s|}gW(pdL z%d)58=EWMva`+=|(%L&8SH*55(Jl&d9)%0T$#Dz9Iy(nD9GLUbiEHHtacZ#77=N_V zr+iRe-SW%^uzlrV#>ya`KML~r--e!(c>m6a@DMNOk<56Q7Y?$;8TyhmlN#1AEt6>Y zBetZE@OtXwV_GNI0KPy$zp!`ogny^FcKmcFU&-nT-Gx@jZ*JUfIr_f&+ z_|D$V7|x_k{E`>5t)1{6z%P7hPpMFADXv`;;h70fjNQTR>!8sGDt|0;a4m8#sH(@` zExQ8mn6Xq|L!`l@#S@bA+c!9XzyI-f@!4;G9j#hx&Y}?~1Wqg-)M%AqmxFOM9%E_R zuSw#R$2&u`s9|{vqVEB+f@JILeuAFE2t|SIFCftdx*lmDHw(n=?H~OhE`9xVGF;a{ zA8N|VW&e2L=s~QWc@`S0YVhdZ?U+_vF^zz&>0V;+jtjUV>8`9K2b>c+{~@#s?_wy?JnSozRxEb;m}D~onUY1;bwHgP7s zpD*&vYHoq8xeucBgMeu5nW;O?|LDLV9)9!@w70io`m||rpq>lZMGPXQ(MxipVO-Ec zXkleN-qpNVo=@L3(22hEn9Q00>_0lW2gio{v8ZxVbS+yFU`f(Sy5#cWgb};-#5rAg zKAnS=B%mJl=0Ix93sjwT^97X?1HIv>O z936vRz;okO@r1j1LS>j(oDmCDXL3H>u$bWfN~2dYZ+V(HDgL^fO zkG#%Qfb&o>2f2?PKOhao84#_6LYfETrhu9)2*DCE#8$lV6lDMr-}28Eel58~vKtrH z#M$zX%{>o)wfIsA6ij+FZqGuY{u97lo-_ZKFUf7~G=I^~1bUX-i6;o`ddrIKl0+hS zgf$1uc_k=jfn;b6umkd3O?}{rD)MiKr82tqyXd;BFm?H2oZh+DfJ+C5!#A40^AA6S z^RK*I^7!lRo3Y{0UYs{`9;VgRhpA=)!zOzP`U90 z`h5DJw0y01Ws9*`{|fnJKv{(E92&rB4$Ot@!m@!Z%8QTpT;z^JP8oX3mQ(9UKR&bf zX?*d>^JwJ@9=e}b7Zv0E&CBq`1(#xV^%Ut}Xwk23o`mT4Tjt=lRTtq4*Ss0;KmQ785Ke&eK3>kEyf+-#hlloTlbZBJP1F4K=9Wj& zv#(9M8fh2sx^tr&w+|%y;!5Hnsm(fZ2X4M6(x!YAAU=Ysxk!FV1+XSE{?8ao%Y`Pr zK$Qx9JF+n0!kwig#pi;TH|;$A{!<>H|2L9YxfYY1u=MF2fD(k4T$3BKo-B#R7OsL& zYaz7hV)8A@8-%)_TyxYQ;0eLJk?q`IS zAV?5U2s5$rcM2qdd^-#xVp-jMJCUMq9@dmFXxe8GwUTq*brY(ZCgaGnn~{-sATv9j zz)q#Sq8y+6f1k(VRVzS^unEoERr8lgdm_CfL)d)m0M6&YyQOv#e!1g$3A*b!2p41e z?$al5-t;+KvtEVA_U}L$4>Pk)0QPd+%Qfz+TV_ei=~`f=?el_iuQKsz(H(% z{sokAP3H9JGYG>$9C(m8CcNc#B7R)IK_Kr&ziQHSym87rso~wN?oSuxv$5wC?mE3+ z-YdSaybeiV)EmE#FJGQLVI&Y2$BH;;=^h!DpuLL&a`7z2=3x$Eg;ngV{J22c$+sa_ zyX*2ikvPfN)0-Z{H;-+@XZAgXodcc9Pxy~8YInzi%jK^aQ{8e9;Qi{BdAM!0;`$}4u&3<=zWn&F@een= z10{Ui{uvkY#rg7&Tzsw6kbmx>yK&pOmtuDPWIVWc8}@gcmYO`#&%XG~{a75ddUvk)B}YR2`hD1!7O!I$RN7w478{!Ryp-NG?d2 z`-7Jm3Y)QGg8tlJLHWJ0uO7;4(0_|0&zYyyIdkPkJ5(ak)kgj9dU5=l4jKS)4U!s2 zm>r*Z65a+^BXwc-WClko-(EH^n5UlHnJR# z*vJ%r^?b`(!4x1n;9NRJY5RjR6V?q+llF&K2Jzf>MB=x`dlvCYJvKB>#-IM(=g~ZU zn*1h=7=8cwr%}P{ac+ay&R&flz3>b!pSu`MRW;bteu8T|hp}LClN_^uwR;P$o=)v zF<(JhZ7}Kw0o(;;JG@?)G650a4sWL>w!4pxjbmC<6X|7Pc)h+BS@&m3APplAK&QEo z!=a%eJiloZUfi=AOBOB+&!-1&|MS$a@0$;BWCNQPmLmg@)v~2Dn$v z#ln!>w>bb6mf(e-YO3K&0$fRaP=85$(fFu)wsLWwlXzqPK1s{L8<)$Iq7A*LaO?K_ z@vF8&(nzN!QHFne)9x79>Tg8*JCGtKf7fnkGsq+e_x#0 z_@{ZzmVcjrUtTEXO2^k8vb7Xwe7N zaR*WNMX58Bh2wKa#9xy8Z-WB$m#0?;cW7DIFFM*pPr|en}n9DPoFbalX7foX7QT{l2*NaM?g(K1^rbikEXIuKE;Lor$@NYoWuxGy<;mV?rw}+PJ(5EyLY1(k%>=me2Bh zyNE|@}HC+H*ZOG>sxei^PNyG5~!cGR08!~r&^_EUC=?^kMG>@ z7+SkKv8bs<_TLyUZkx-NH@8gy{ye(vD5)e zVi`=0^`Wglwa6-lpa60V5yv`&JY4!R-k%;{#Es@0AAo1hyhZuWomQS+EtCi7FEiuQq*Q6AZ>kD4U{>4fq3_E=3>hqi)lI&`V%uoe%gD@Y+O@T3By3JXntr5SRE5b<>Wq-Ol>qD%3XmPII)tN(h}3>APC zJho)0F#Ywrx8uEE{u~~7e!bLQ#tISsg%alG#VV>ukcv`|li6KhGH887A{hXleSD0T zA>jzNQR~w+IsfWQu=180Bsg#1cgSuAL>^=gs{v?}VQ^D1?GSdo33|g#H=($w2o(Ga zLvKIiAp9&2x&=TN*Klq)b^uq*S0d)H>HXqqm!aE3)4o;^y(3JarmrzR+FI^E*Du7xqTSFg`NI z>xTc1^6`YfXXNuJe;$|a8D4L~osDt8&cn(-0q*%XHlF73rE%!cL9Dy?S18~~O`9`I z9u@F{o~(^3h(E`GLl6g`zBrVB0W^gXxi>b>p4(ACN=4z!grlKhCA0k!hw1R z7EPUI1NB_>NddU)=7zK6(Fv59FT5`u_ghjKxr+w0jC^tXz_}HFKn7tlnY(V*Y!uA_ zBtj{_caMmh4w#(VDNv`O-Pz}#Pi{vOD?)9FbDiFANYSp?_%1Qmi01@*d!Bv|D1Qe1 zWg>lr!hH)}K?M~JiDoXMFveaY7qqSmciE{lwk*-^Vb<78pJtRtg6Me9lP+F$(# zc66T(yKxYmH>c;&4G$^xt2?1MbFOAsvBkZxyv(Gf!8Vmo#HznI5Bbqqt8m-2B@zE# zKAmzD6dTW#IL@OKfLfY zSTMO6ckkGQCy(sK-(K@(Ss8!z$j`B2>U3Pc;B4&fIDvoN_&EOR${XdK=wDg?pwyn< zxM-D%8|XZJ3g7t5|3%lSHd$W{3+Br7+jd2o^hnDCz$|d6@{@p_g<5u{5jP;)^jSc6 z{lFs!)842*XUrl-l z>kSPJxapQ#aK+`9`$2&FoDC@D(q{Vr2Yq(p6@dJ!M_<74;Q@2AmnfD69H4)7!R2TX z=hJ=hWZ}UY*0RYcE>Au_Df!AGN$yFKC!9FNb#&i8z7uzz+=*U3k5F1&GX-CmcTu>N z-3_0OKVQ93_iW|o`{u?ufp+1o15WE++<-eC`<2w7%Z|f82}6);&t8MuFMnP3`f&No zR-d_OqHX(3;!lcmqGjouKPW#b_3N9rUEX72fa+L*FUWj&)7Xq))e?QDa%#yH4TSd;bG5Z9))jb2pKuRKyyX8^8Sj!UCkq zO6?VKrofCUmUnYd{xJ^Dt2`EHZF60SGQh$JhWTRR&!2f5+YW`6g$IU)@W6}D5s+?0 zhIj$^-lKxj3ktgY1Hc|&G=LM|w25!#@CmXIpo5Zh^*1Nu(Km;8;Z|8*fo2Y{n>py7 z+BgMM`ENxUC0X(ufO`=_x=#Kuellv zxTgGp{oC>2!JYX0)o+sVeEFFNFmF;LuAjdgd$>kjg#D9?u9Lf1x=x?Q*FN(X5}fNy zto_d~N?zB!=M$iH9@T18FM+N%kZqbQD&tHh(x#j3=#2CiX3tQ8RbOFBDYa;#(-k0n z2?yyJ1<2FHD4baQ5r*T^DxGn;yP>LlG4!Ml6dvGyKyGb1oLS5A#~@nkK)@( zgAR4%Ll}~~{fg^3SU-Pa_3ZnWD{VKgX9}pPn=gDe$`yC6HkS%wGv0*x1({2-@GOpg z)^U(tt$;VB!m2<1rEkKSk~3RJypeC&s2~r|UsaR`=)VDJ z4WSmcV%xYv2_xr#ZaQB!e_6k&q?FJb3Qktcv1bdjFgW6OB$h!5RN`u$684zC z_wTzm;xC_nSiWW?`+-5$p$0vG1>W_q@yGu-KrbkZR^X{N^Zc5r_}J`~@;5tgZi#{e z^HDsrV=JEBxm8}D6pkB`OAHJvkUlhILQ&x$4u;)#zrP?RBr?j9%c2vvJvf;*5epN= zS%{mA0mvc?}Ti)_k zG)|d9Go%_{7U95rHO)X*T0)5`rViJYMx9z1ZmFr`K>hVtyK+tTa-V43B;_Ls5GSSM3**M;rkzCD>N9CkK~O4E0GQi2 z)HjyEpEbV$){ZcnCzw7bfO=C!wKV3b>8ze7IY{U8W_u6t%r=?E7gt}0k1kq+-zL#f zQfSZfhf-f%ln3Z9fyR{~iA*Nr{q71KhJqk2yhKuv<Ntu2bHTOp+ov90hYy{5nFQ=2 zO<}OKe_$X4wW7}s4B_~ZBRFyJFnZcLaPsgGoH~4zyALAKlDns#t-6lY8FOBvuySEY zWLO*v6M5EJcZ73RBz#7^Xia)rfoc)2xEX>GkeyKr0hvtN+U6btd*UvxJ!M+s&dfG= z@psv4uEyJLz9j_eNnk~1EE_1InRf@3mjG788SKA3xDgL_9VMk3Udtu{Y~$Q`_z7we zV|hpdZa&_##hJyQZZpDy{pWeU3@}%7{^qgh2g{M}8&2dT7Zk97N>RY;U>^tLMy*w<$NiT#FS+ZN!bk*S?e5%S7tL^`DFX zEafCAm3-5Szu&pO)!3e;t|*YNRNLM^IlLEN895Sq=;U+jv)heq{oqx%;?gNIt?)W! z*UiV#nNR`p+uL7lln3Z%K-M|)mqM|Dg*iPnVQ^R9;^02+-|hI3T*RVeH6USdgrLBQ zA@r*OypCTy3!pBnkroE|HM{@GBzKXFkHKgeS=6-Q&aZ&A@I)sg=J_tkBm6Ms9s>nQ zLF+@pKTBj1->-Vu=P?Li1Wq!6GBO7|i4U1E0*&QVlo z%ICe)mg3{HR-&q)7&)`+yg$2pJO1hJA7M}H5nQw4TojANHO5TdH2;ADjQzvIc<9AV za=W`-AEdNN!Nls!1F{1~7eJUJl&Ed)#(`O1_3nWsv*t=Le*Ti>@=S6I*Nj(HR9L`T zw<8UF^>f%}BH$bSndF|U!R#c3?dGc47!G-mAEInU>@(mEU!z_u;F@ zw&2)MKU7snS}4z_UxAj=3Y-xL^SS%J`Mzh%TT(j8uP^{7b_R$m2oZ~wHYRYQCSXWCh#^ZGrksxrK*c}?w9{KdRA(sFZbM@|;*Wcq^x`0F2j3qO78QIn~< ziYmNj#VV9=p*M;v+FGnhfb)l5+-w<+3%L#?fT6-c6qJ-DvlsQ~dxXYd7bh8ms>>^- zru5p?7ho|5=3;BJwLKlMz%6q?bp*tt)@NzP895uS%2k6jb4FZ!m-tV$X$sH{zzq|-{_j0|x8esoHsa1Vyhrl-7mweAMOZ4<+{38nfc))C&NFd{>oX-dXUrIE3}t-914zD1nd$|H@cqb6 z4%QDH#j#zxag09?4-A<0U}{a5wFA%*wThUViNn-W)HY<7P{3NAWluBnVd?C77DzY8 z2M9<{lWW)nK##tKKpaei_q0q^pGgnn6h`O5E%Wu0C*$pJe><+e>Z)Ym9A+%;Hc@vU z7;(dSfXW?m+xtFZ-8`hVljll}x@Aai0w5>r;=x4t&Xp%c0+@$q>v!_!F>4?Z*U5cm z_IX%XUT3H41B%&XsfpChM80R8Lx7w>HX**)VnI^7BDCS)9(?}SKf$gON20Coyq&H+ z=K|b*6$k2d4al1AOIbW}^LVC|EnHIC6K$`W)M9t0Ae9rQN;w&H%BHg!!ep?G+~&ton1Yq(5+R>qUKi^+c4XDrgF zConhvjt}wlmoGemf8V<)T-?)(bQ+ZFaAvzZ7Yd3CQCn4w&(1v`H#f~E{IdyxbOFr& zvZ`+ues_xr!Bvv+!Xpq%e;~cSq2z z=HPo72VK{$z7Q8IUBLnQ9K)BjU0ea>jEXnficXEW!2nPiM*B-=v_?GwsrHt5=b4lZ z)i`&I597(+Q~1|ku0s*ePbJrAFPkw3H!WL@ma;0cD4_UP#bg#47(f>d1V8m<6=o4f zrz4J2z3*55h0k1gEtWJiK%Zln4krB!U4%fyn*r~pTu2`HUmMq~_q2DQb=O|B9XW=MgGVtkFksqG zDz^B>)DB%@HJ{WMX-Lt0M%t%tN78RP+!BBGSqpI1v>DOD3ekp(ZS3Ke_wbsx&`oV` z7x_y^U_I2Fk4rxW)U%A-IMi@&;-srdNEK896`mgEL3^jj`}vhF7UloH}=9qCs< zeZLh%Y3L-ZoBX*+6UqNLZmoXIZK$h{dZW?}W1QRlRsUhEd#>*~@k3_U!_EB^21U zKbG#hrY*t0o&8$;Cgd&>Q0YZz&+~g(UwxDZ=vhb@yXxawFmHcP7|3PE$u$Y&ip%|$ z6b^Pr)$g%4?TPvPBqcxb^mNn}TtLkmk$ZJgbk027&hA<-w*Weew(&)aI4X>Tcxfx6 zppH4hM7DPBH19-#Dv}8pP0{w_d~w({J~WJo-%1*fO!)Zmp>6omj;C>Ss8_`rAkv_x zqC~j{;E$JFg!fHbDz8wp3%TAqxQU zxoesTh@)}&Kq?`jMgR1^r?H{;WK>pxKA*mvgY|iKxFmp^^nD_3F3;qB17FO#}%7scaDDyxYA=4?E>ak}9GJ-&P96(6`|p3ymQYza-IoD%qmy{l&l z)E~SjwD=x&P!7)Pxkmj$+rgI#b+ zNI;g6TAnk%=g0ha2g!g(z$7$h%2Zq^K)U13)cHmYz641B_4Dg7Ha_l+CvvmP$vt;o zTTv;s#bc?;2RTSD zoHu2LDcI~0^Mn~Rb90YWe_d| z+8}qKrzJaS<3Bp1&h+_p$I^RRY1l8n_FBB>?eD-8ZSg%D0GX(pOSwQAC6)HNHl?BF zn)QgE@QN~XclenAH{Y|5PZ}R_6Bg({z4z(R037*TT~LHun-}BOriI}-^CZxl1EgB% z`zRMMPWqNj4g-(~YJFvyEscqkX;PV}_d6c`CDv_v8m*me${q4t`t7f|0c+2_D0i7o z#LeX`TN{~3`$|efC9e`?E1_4=tyAP1dNaB-CuI;SM ziJoF6x{cmw#sY6LiX|%-Cid`++;{5?WwGIoM^;~2fgs2a7{Bino--el!mg$ycr4qA zZaSWC@so3jfD3(Q@AEwYsMcdcW^+yboN|%wp?m!1>gzD*>8k( z9c@Qv@vF-;sAe`eSDY%432WOs)u_vFG5-|2lWp=Uewxf%~+a@yV0Gc(V^ zaOP{Pn@zZTyN{xKYy?-=PD5>BiM8&90APqOIJjkzRuxI}uC32YBYPQU`8P)m zNJ}^8?p1n+H@%3lu`#5_#^u#+C6yI8^z?H$zEv+Kfi!CtbF*IDW~_cMK6@2rPoE({ z_UP~kMhAx^2-iTI>Fw+SgvYL3Q`X<92A;bW)mCBB%0)Q6aVrMfJ7u26bU^83#4*^ zGxgk?9>Z7pLczC>?{xSN?{L3;+KP~eq;wMUmGv9>ov<;sY@k}Z<>JSae%8f~B=V&h zo1FYSw{~}OZTinSP=5wyFyy~Nqkj8q-hj2|U6ecR38XV&H%&<(J?Wb-?QHQlWtu5~ z-x51RRtl0FS@ir+rU!q>4kSm?&jI>@3BZfUianH9*5r!U+}3X9NO9-U#=?k{qU`Tu$1=lIF%-z|?4ed+l} zaf;Xd=P!D_@!QUUc?Z{=XFTAWmV>)6V1so=w)@j#1*Dv^#A6xg3*dnDEoj}m1w%c3 zn7!s4Syz3h+HhjaPISmE@lGFb`wsIws`|KyYtrY4GwDj>Jtl5k85xyYbr299o;jyy z(3PP$1_R#f&tHKlSFS-pX(^7}^AJuvvB7MQ54G*;4)%1Ytry$fZ@ul^xRz_xvx1Fm zz$?i;Y0ggqH-uv6(-j|C&!;ExOTYWlQ~rCoa^udEJ8|c6VE}FfbP`xyGexE;&zVmI z1p9b$^PkJVuPk2*nDzltU!0U)N*>)fCz40vx3#MScRav>`g2diOhVkx>bVQ>nH%4V zg;S@)3#@(Vok`o9XnW3<-=uVs!uY_MJPYG|1QZZ7$aTfFR%A`C%_mBW4LPL>66SCwkC?}u};#xwPz?oGE z?cgk{4<&F1kp*}1#k^?%4hNK5tY=BDj1N5G9A%zI*pqE=N3rM0-(*9LulFb*;qDnP6(tj?lowU|E__4JhFAO)QB3x zb;{qROU}j(=U;?(Uin&S^i5D&rEN60EDjXuS#W03i?+SHK-|`}>v&-qcC*}F5UKq! zW}mm#HKP0rZStwHVLZ>l`54QXux3nDX}z$Y3vKPEaplVMq`Ls=i%V+IR9F_Jh4_Mj z#S^o&(Jd7<@~DvZr(4P=I6z;<`;9$~z&HR>9?j}8vv=;p0B^yCH%clBWE zvPGzD3@uO#pe_Kr+zzk!2xw!3)89#3zV@O^F}ZG1Se}_sgD(1n(5R1dpsw#omqy^y z(RTLms$p0Gs8B#w%I2!DKHfc2+1a~X!<2wdtS}8=z046p)oFS4suPO z)MzH`_ORaw0u4yxB@MwC1`7f$r;#Y!RvHC0?@2>b5CLsBtd9&hSLtR2=ZO*78L$94 z{p1!6NJ_fDTd_L6+_*^i7Z2{kw;ujAUO2edQ)o;cNjSwp`k1;xDr?4c-5@HFY@I__17F501~9{$xIS>_a1V}+4}y_z5m*I!1_$uy zw&yXeelk|hnU8%Zk4swsZ@=tnOerYE{G#fJM~7GYOuYkqIh7U^VR}W4DRW(JFSNJf zv+M80cdvN|$_tB4c^n()LFdR|1ddf&bZa{p^DJUS?^Gi$HOm3XFR4p&Rhuqv)z>w6 z1!(#@pLRVNcm6Zb-hmx=-;1`L`{i@<^2M0Nwd=(dB`!SmSgC`N~djm36jFiwnl#f7N5XeDaTT_(qhj`c6%<@Jw!MfA?CHU_h8zJ*YfNH+Tbo-2zxOM8nFbX*1VXpkqytv^e z64n<_0$B9kutUS;iWf+(hlz&qPh_3 zUzab}f3EKn0qAagNpf{kdFgSLYtS<(DqYwVwL_|mN~XVM$MBs@8*&mKV{o!Fj4q?| z;IN^1|38x#*q8(3Eg?|8HdW#?+fJtx`(m?bTSCocRB+WN@c)=Rwlh^ zsDEfWUWq>>w_~%|7dt>Ce23rVvnO6(_*fRnufAKdsGv}uqs-GQp9H8Amf0n!^R!Lo zoi=DgQ7uW&naVo_#nyt96N|6exdNQ=L;9N=&YO;!2$>^nB!PdLQwI35X})@SXC7YM zk8YZ}GbS)2J`aox8()n zsPDi33G^Ikm2I+WN&{wIa1N>(C!^!QVGh=J8nEt;DY}?6NUy1?GS{RDkUp*~y^jqk zV{+;6JbJ~7g%X?xydFo|I&u1er_g_>Re|kvcyGEgCKqbn$3xj$fd26`LTm8qnm1(` zUObPdQh3ih--&DAc%wWAjEQJKeQq?LK0uQsW6)PczSbdvbW zwxE#|ZX$6dwF@V_7?;Kia_~;%YxxR4SpRP3GzaLdm=KMO0qxz0=L7JGQUBa{Hg2q$ z&gJED1JrX7l`RA~FUSM*S1siMx`*@@ciE@RAOY3Mg5=3Yk*_Zr$~J%}O}L410j?y? zTxW}SSj?H|J@HTn<|Jvc=%gzszs3EPPomoo@pc~GXU?LE^tJ_GYoNz7XuR%x>T0hM zdpAv!1lC$x=1it;TV!^oV%M|9`&2j)ClOQs(%H4{?E<=(#+F5zZ3@<6T3i<(Dt8L7N`Q z#?Fymm2GC~Nl-V}n&186dNftn;p)X_<50(GJh^8(F6O{|I@hFc+_wv*eEeH7b2e5L z*U8O?8C73q5g}j}aHZvCc;wW;wU~`clNrUq`5WxRl=4csxVFE)6MbW&@p3eI)A=N2 z$e+X_L>FI}n_O6e5?&zbjIKwuz@pk##wzD{Ir@OTE-%LHviE^{%9Bn<{B`W!kE2g+ z#IdKJGmW#7gY{W!&PAmJ>xXb+%P#bt@?R%d%)$0G7hV!-(v7A&Z8YDbBfQKiOHqCC zDik+2f{l)&>zU`#^W1i&B@eB)o3rbTS(@@RvFfhRp-cYLp_V<(^jY^b8XKqJeeZi8 zUiZ4!#eqlCubZcA3xEQ7Kl0kp-HXLlTXF$C&i#lgGU?-OV|eyfW@V1641=2eBo zS<^S)T%LXM)kI_?-(rW)4~=Ic{3ONYrt5Qed44H?-}mgM;UizX+)V=Yd+!b{zN_@5 z#diVJS6>{@Q_><*60h0P&&KOS+JCP8Lefl2ExLAxzg;JO3Vh8pEaD1=7LfQ(p|+?$ORJdL}~u~`Ky}p0Nq1EZ0H^y0?RfD&}RbPH6XDE zCuY898|Y0(%;qo4x40HO8fXBA5Amk2%}Sil;;iTO1b?k(pF|k_=n=kH7G8DZ35hWYRP zTQ^~5eIw4ByFiASU09B)pwP)XGa#61nZxqSSb(*FYg86iHy|aqY6hT-6@z?!+H&fc zG+-xOdU|@~ypvHaQur;esx+k)l}(t(qLN}16c?E?r*)zg?PB9UGfgk~S~YjE!&)q0 zh+mCog`@S%2zU?Og!S~jci)fJr=GJqxB%duM_QFg&{l&xHdF)mCG?mU35M>k@$yEkgX=I-=RgC1VT#ti6=?nRHRz%!OH z8hfE-mw6}Gwly_P!EGPQ^x5@40TMv{p^l?i-+KzX20ER1 z;71DTy`{JUSJ%&wTjbBk_?w$o>fe_~?e6nOzv=H2&3{t)C)I~9&0KYsl;@;+%vPUC z=_kGW(&Kc;eRpBqh9{*_Io;`{#rNyhV(s~t_|r^kpGkS3eti5Tkvm^e%C^`+NGvjr z7X+>Z95vzigXQx$8^wFld!=!>3DX!~sOuZZ-u9etEd^!xN$pBhr;1RRN^$v_&l^Q0 z7B@Z;IV;P<^H(+H0eS+-Yu5CYLb_**$AE5Y5Aad}H&by%1D5*7nPg1T3F)}5bUh$~ z9+59IHZT%4a3fNjD=^@jm;KBzE=(4nzU&N)&E)C7jl?$gA+9x(;Dve4+_`Nm)-C{s z9K!`^ER06Os0Z77^kVbE9D%@0ouuDLP@2rHm%UtlRzLW=ca$qDe=!hEeJAx@7W4eV zp?&!FBllrnUnkDF>GjgE)q!W8!|@%veG!mnl@eiEg7luQP?PRvguGh|3?v^HEn0?; zz5XV=cGdY7FxO>+k0Q!h#Swj5cOQ(YD$&F%4LXy>zc*yZb%~*u(b1q-Fi_oru+vAJ;; z_A?m({6GW0j%<(Dpq{Do>dV)x#$Wu&r=s)eX96N!$_9|!@VVU7yWGCcL3((me3V?% z7LHY7%epwD-cnqN)iq6O3w)Nev*kxGz=8~)s@`(~Z#Dst=u2-RAkjAl(Rg#^)5n_| zmoH2b@b}f7%U?E-oDi4T?taI;Ay5Yyd&PEl3Dku~Jt^-#9uwLWb+_i}Z0$0s406>a z2|repGILKThcM#Rj3VSYdVgPLFE%p!io%>U&c}ekAy~3}$}KZzseu1nSyYdIu2^VX z1yG;F`&hZ&h2kM`_j&&O`Kz1q06l?(;LtNN6ag6DfcH|#H~3hev)q3kIELe-8$gA1 zTawv3m*S)cu?W>Vg%KS2gH9oe3+=%lK!oS z@5Q~(uQ&Ofy6`N_Ie!((s;kg>>;!f^@Cb(V>b&7GOa6p_!Zmrq+Pk&gU43&HG7odl zIt%~%=6B#7t1d!$%D)pP0#)i)rA@Wx?wN1_X~ec{(GJZS^8`BD+ZElE!HZ*B%_JgV zdB#UIWC^YBr~pem{?SYZwkAKaHqKPtmhc5dQ8q93cVJcZWYiayO82J++R!>SD8v7J z$7U>Wv?*$Z%hgY<`YZo!ocmt*GSDX8W^ zKR9#}>vwFG8g2o;1t1?888HBqEaEg~3fbkRBS6V0uj4ozCk-u%m$`&puT@t>vqb&sL>z`0z zAqUFUXDz}P-uZ#>e0nxO?7C5RNn z1?Xa)9Ue;-M5s_j-rrf+gin^va=!DitGHBddoOg(rwcAikw1U_YNtFv&q88y?i(47 zZw2Nwp{v$>gAN?Q$n)Ly9F3RJ0FGGacltX4LCj8u%3;(UHjvO>eR*Jn7Ya}+ic3&l zl)U1Xr~h8k=gfso1p@#mc@~FESn7?qMQtQ$2GVIeF*!nrd(|J@G(3}buI|PXd5QIo zto&PYm^We?AA_4!+o$PC#)HwebLRmL3V!m;WAd+mXwaKVvj*y1t9g9q9voP|2}8a8 zR(i2rJ)~X-`54yL(+yuHHP|gFEydD{*Wjbq--NX-3lf1jO!*x8Q+I>#^m)UT=Ld(oFOMY#9gVm8IqQvk!b2b+xtfwBg3* zH-|zQ|G%oN92I3{w){%ktRBBlb#=(PImN;K!0;en4C+EZ=V||mR%w9HgbxBc4z2MD z0t3M1vQq$JbT0((0S!Yyd%(*%5XT6(Mo7jZo`wH{g794!a^l}aI058Sg#{9@rwR(y zZz=@&9PkJHS^n}khB#0^c=v-i$$`4BUDwZ<5uR6{(jbk>EqUW>QBqlfbn9shKd=Gg zJ-renhnj7sth|r#zhSkdJ-RwA)S`>C<`F>GXVcRJq#NK)ExVI*@A`T+ot{X4oNM?# zaO->U_IJG#RpM$w9~kxl1B8&*Aw?e3rd>38wamQ89Nf$Ya(%lqxGdlOptBIOyCrH{{C zd6)?JcfV_L;t=E1XrX$btLQnXkIzAGzVjF?FCc~Uq!my!6P=^Tv=@?V|xxA=E-rJj} z-#y85<{5F$JZ*rI`rsD?$g}>}(zZo01746DfgOOPw4e_!P;9O|bXLFz~;ykc^GmgHn z6=Nf#u(sDHWMY7W|MtG#*!QV(W^rJC0hZP_;*VBdhUThTF0ghCQ+ z?_xnfEDk)?dH@I8Pe}uN$9g)s<|T#qUVDQCwvQg%g?*i;uy)ZZl=210|JwW%>dPx} z&4T4P#upjx+w~%@U9bXk8m8d!z1y+>^hw;fay4qpt8h)#RMZv}n{|T&Cyrs=laJy- z4%S7v2mmhb?Vi`tf;k-gS8&ihmp|83*I?&ZKbBTbLS;b_`gnh?;lBlPVN2ms|aS77zBFI4;5ZX`b)>{CVr4{RU{8IJFi^j{BjOJOcC@lq2f5`dccH zAYB%IDg^7&k9rPL(YLUIQf4v0jW>#0Y3R8ozI*#VwBPq62i*N+DhjFmBO~%Ew(uIZjM?r!o{r9-M_b;-d&XFY z&ycg|I=oo~0MW_|gK`X!HF*Bs)HD_E=i2le-tY#T5pYcclBOtv++k=qy(j}`NO6IZ z4>;ul`}j%%+PVCb{A7(U zX>v|TZ3&@8P*!+qvec3|M&_z z0CZRdEQGecqraDH%S$kG@d8wGt!&kJfX`m}23)q}Y)phSu+zn28ER9&e1MOWBk3^{ zM(jfR(U$eNf8RC<&?O}R?eJRw>ke7>JAj0BMENW<>uYAt!~4#?%m(W`-d?^?A@~s5 z`e%;p!;Vwx`WyiebwlOx(I`#BUtysdd!>LgiGfw#$GNY&*egL(ZDdyDyhQa`l%M{gIq%+c1$$Z z)?3*~seZX_uoK@tz7tC;>hZ?je zEw)tF$WJX5HT^g|_IMINsGv^YFkB-W#*vuX3zAMzkcJ*{< z{!ZVGo>sOVWSq(pe6&?Q1nB z`K4(jf#M{8ZhYC|Ovfj+$D{_mo%hc9gN z1wfRS0ch- zSNuM(x&qVhu(>AKxTjJg1Ioe^e?mB!8&$veK zh{+cbSSMRCjCUGL{xat6KG}LoR{yH6nSPfPhwhR|5n6tknbfU*#Ay1_i#%t}HReKd z4$D6?i8GfO-#bsUw6X%{ti4_u^BV5!$My#wL)WnrS;L7f#8Z|nKt)46wmtPM-p>K- zM_+#v*Ys9mqLgG9P5_WWF*XX#q-cW*4FCJ4$MGK<9^=4rC^^y)nZl@swVTWH7Mhn-Jd-FtUT(ocI6tZ-@hCG_4LD-R$YfR&2w^oL;Kh6HL-+BMpdEAa zyxCZN{k16SAHvwtlTxTI2-MC176NFYNf%q*GqEk~X+&Cd1>6nDPs{7-^cnQD_M6Vw zTj4Y0Ts!}7T->Ej4AF^nZn)tFeCR_TLUT)tYAEc?0E-W-B+;3G@?4Yz{@l2IZb?8n z2~^X#k`^f@0?6GsX;@!ezB$te+I^H%);>Skcvru7Y5;-qk+X0Kf$gNrS(KG|-BjZC5LUp*yq3FXr$YM0rcW zvn^M>6{QNfcBwc#znQ1sImzwp>e@7x!=I)kBZL-qHh!a*37tI`K1f6sc1lPyi4Wbb zY5Py|4<>o_&n}Jsvw}u93Zl`PsDR z?z`~Y%r)tA&RZpc`iWh8abUyq7~-0B=~n}Du0@@3_7eGfe8(Qt2Zi|Zdp?2{v*zJU zh^ag=*Z^Ie^X{R?fH*yKWIz7*ufB(4d{M$GOSkBKiNwZ6=R2HlEWuyWH9gy;6ULbe8%+q-XLAsK636 z&-FI;AkvsCt-3gi9*8-mAlkNRfO(+jD3Zs(&TqH~5LS9U7kr1-Z^N!%JOGk;(8g0% zT8i_pxe8U&reeH*5JQKKsV>8$kS*rG3BuNtIy$c|K>E14 zdM!hZ!Z}9eEF*Kuzxb2e-t`r49Khdr^G*2h2mY4?>aj623Cx%Rw7*$nsvCYHVB80` zvH>sZ?)#Q2tqJDKa?1t?U4Iivmn9rWoJiU; z^er&F7msC3zH3U$&`?$>x4`G=)kS%L{u_|6kSw%DX(9|8Qk}ObeUc5l0iCa-Az zpdb=J;OCEq(7YH|7K_PA7jZ4Rjw9D%l^c)6lZng<;r7Lej*86fUn&CK$Aq^0KOVgo zr-apQx9tY=y;Gz^OpoU?kRG$v<&`ua^69~qVB)pqb1y}Gc?Is?zF9`~h9#>oFgl8F zJ@YWGTDS}=T4v)jKf6{j#9+o9{EkEl80Sd^CwmbynEQJWX7ew3Ob(PVx@3LR% zl`)kTbIMC=@5j6Q(R<%h7}&KR1j(yv4`UY~{hU?jpscP2r8Ao)P#-vOBurOYA})RmUwvqzr8{e!2leDe`74&cA=miI|* zZ6aU3v=iK=#rI$Q1nZuDobVTF)Ial<+i=y{=T59nkt4s9eEei9Q>VR*^I>Hqw$0sm zR%429^59DI@lo9HV5CU_tT~l12R*>)b zuLu(776o7!6ZiTi_;JCx>qf~I+HH5~oPdG?EtK2y8A(g8R0oz|Ow-BQyn`SSlezD1 zKXBvM3kHGJAM6|&S8rtTludVanc(!o;eGhqb>9oM=EFm#-vv(oqO!zmqG@dv*-#eu|c;ME_wD3=JMga6mttPSR2kK=t_~dSYPZuc zfRi3QK=qjr#|9+HzmSvd1G2r&cro=Y56y%&X}iF+Ye#_J}9@=!k@mTE&>=qm=2Fi-(N5D!@`|9y?kNq zxo4im-FMxM>gp;iT)5Db5u9?Ki00M$M&Fah9!ECu{#uJ&PW&#pAlnDLec)@Nyt>a5 z0C0(MxctyCNoDN%qi#v%;fD34L)`vu?|N+QIgLu*53i`5Cc)Wz_+p^|=Yn?iwqs*| z8%jshDCrr*YnHA;IR}o3`FG1X3jh{p(63%`uH4$*H#CS{M-NN)zQIBKYU9(`$=}bO zGY{4JRsvt1TuREXugsEoOG=A{a>!Ug;GKe)egL@`Rm^j0OcYmfE~{y#4yM zgbSCWzoN?1XpTdOS1 zo9egBevQe%;kzxZnzQTrs~*Du2h6}&X3raddu5PlV}pDZSD!Q2mFL!bHe$XBO@GsX zn$DvPU=vYIy5NU2J6CJ(+hU)AGaV@)sCgCIbOQsOe&Q}2#L7a?xhh_7pCn%4^a-C= zrv43_e_J~`@U18A!+kGoj76#z!DA6w`vly?bpOv}Y`$57G`6_I0;lnlR||5=o>a00}eCOrDt@BFB7$(Y3dJ@2gQDw><=&NVPxuUFS1f z7#hxfhR2WVSN{g&C9nk{L0^5>hoz>qf-h2TZ##jX?tUIOEnLYp=~MBYEzjUkcRT** zg4b}Z`3S!I{L@%8xf$1R%{yh~fjwLC(`_3i5KU=Kvf?RK5bj8l3m+NXsvyCdxl>zd zUxns14vq-30TD6P!b70_IzYiRr68VnddE35cz4T2y}x<(GQ9h&Ra|>ljJDn${Phq1 zQ~t_U9jFBaV;oMMF&AJ|#3}MFqESZTjHUM=u)sI7gm%@_6c0wYR=mphRAlQylt%>z zCe>Vnb?n!V;l#6>;Zjv)rPQPsaDGHt3vGJo>=yLzJbNIFe?x6*D zX>Wn4`pW3BErS#b)b(uI09`s<#3$C?GvwTPsAZ3=zC+c$wjM9W{FG`NEpuk$uRr%U zSiPJWcJu+QYyjAGr~c`8mvVt!UwAi7UtHPZrSC59Zk*ZTO#Ng6z7VtOqVfG}`#0lXPwl{*(}Soz*@M4-#|O9uy~d8u zQOpWK5OiL>bkjsqoJS+{g93R>GSdiZ#3JG&;aQ1aQg+=lw< zYAb#aR-8gHuHxUaF`qzUw{5Z0wV|v+YV-5-`%8I%{xVb2`E*GXplG+G0Z6(5Mp822 zSWFOgD=>(;+2_x#@yAL!{31GcWl7!u+$)7doy&9P!cd9jo-L3)VCU@SfyaQF4z75B z7l9Mcbis$k&+fVLyK^K>!GLMSn+EIwS*&7NxS*qPJ4V!bdR0Gf`TkcUEA1&~JWFam0peqy+Yod$QA2XR_ZB$ZlD%3a7a?`CA;EpEq?TuA8?EzudD0&$S-J zyUxA{GixVV?YVPk0ROdfqqMMGD(rU@6r!vkJfkSB@G91XalIjB`F@y}t$6PmAA#1i zik(b?EqO+ngE(=#YIJN&{s|BlW+Tdv&bH~-L11Bd5Rj|i_57mau?$KEWS0p!7cbnK z%Bt|WbFRjm%1LPJ>Bhg_cQ@Cf9g-nKATB_6xP6_aG-wweQqeu=ca-Uw;fbs5L9Qx(MIQh(GtBq7uage@p6$-fKNo&kYW;LU* zq8$Bu4`ZaWTLSfrS~y|m%sG=0Wb}1yh^}HwsfG~Vf3CK<3x5F%&$83A>6z#XwoF<8 z`4E_E3-O|kb^n*y;(s^Xcq2aYk&na;$z=mI)L$-g<`5(;0%QZ{Zd|UL&##XUH$P{> zzb_6qA4y@erKkK@vq%z9q;WWL#=_^|={c?$fB%k$P&zz@*QIN)tg#ty6xXSda9`c| zEdJ%l7R>1$MeXT6eCf6ip`p6g8(&VEVBM{I`Z3&b_m3rDH+}?AznBB{H@+Dy^^Gqr zouow-A7#l;ntvK7)NxABQ847Dem>^DeaHPcF*fKX8pe6}ByjLJDucqCu5u$Rg?XDpNb&LzCH0q*)0=b<%f6n}sL&gA{rwz~{9L_WK;|}Cn(2Il%6_W$b zvo8eMlZyeNCQWzlVBU7`=7FXm&!AI~jrg?1JaO1;hUqd_m`dnV%~+t+w}zbJ_Y#9xqpMclbqL`>S}l(Pv+SO-eL419vwKNKqf8PCE9@DjnL=QJ;_$xqa66;`>YZaF=o%eH=lC%84R)hDJ&M+m0p&N4pl)b* z1jAg5KPaq^i^XB}&1Bo;6A|#%;8*(5-&y1y=%R26-7%`CP?cZ$Wg`nmL0`jmC+;0`o0OJWOAoE!Uu5Z$D)uKOs<$!ZE_PcoqxJ(>O>MSA!05 zzAf-CD)~cztGFpGOblAzh2V-1ycv<0J2TzdWlR zqlp4A?biifJ}~Zvq4DGbzDfSFfwQD{UtGRlN4S;GtzVv+YW-A{z4xhiIvLoq! zqkRv)-ntdb1_I0(PT`{$UL!5IQ@?j?c@kecuo-juGN|bqz(2m{c1#ukeWGRVn;$1w zXTp%&9e4l80_p;suf6Cp4%FX*2``K!m4lC;BmnRD=`B{{4V7QcfHwEH z{Qt`KKFr^KA~F1hiId}3)4B;xW!r+zJ@E@@`mJo|0sV3 z{kKHsd^-P4GiC&c7*PR6ktI+eOBfZU?#}c#{H8lG`CtZDDB%jS(^eLjprWWmo?pq+ z@1DYQ<|Fd_XOgf$$8~FrCq#jO{MVhxBn)$bC2S-Woz)-xl1J&79k|q=HP0mvlHooA$b*!Son0oxzvbj z5FJF5~bDz89& zO%4Aom*KUAaNV!0#;x*9cXR1;;&ulP?4nVOMQ7h(;(np?g#2ATX$qFjnk(ZK=TVJ+ z;Y|Z_0`93XIWzZ>_?h;DaE#HSt2xW9(x=UWIdv;ug?77VXb^?siqNVmG@QK{O&6>} zdT1E^$4+3BFFLiicc8hcNghRzX^f6x_~=Plmy<49C9ks@>*+&eV?#GFrotdL4oKb< zxQ=c~AzdRy#s?Pmuaw?Jpu%ZEU4GMPvhcXt&TZTA$ioi{3sqRLd^ut=MG|-={Ke-4 z7I)X*d|`Zm)J@-g_W5PpZHh?d0egyxp{sDYt<0E+b$X--)+4F_d$51oMLEZ2uu3LFNDoV>p zG(mFOwEehk$@TB4_eC>i;kMUYFG2aPqleHpz!xe92e4t+cC35y5mc8~Nl>I z-B(?QvXT;ib`Xt=5kC>D;-8WW0pbf*9F|7V}GLK=fR@Z1a1Qo9Jz zR5uBqy5(J1Id1`)_)2-v%dpVKB%lbN#6BPm(8Y7>a36|E1@AcBitGO49{{J$%~p4b zkXU@1x{rJn?7GqLI!@~D@O_YfhB??9=p%y#4Rm7L zU?*Qd2@Q@7@r9%A{yuc{^}+Ny%$|=pJ#fzdxpdjHSa+6&6buHeE1_D z!HsLzA}6Sw$ee$s8cjD`uDD`zP~i9P`tfN@U67U(w+B>a1;;-BE@dlEH~d7(K1;cO z==3H$*Lxc0A0I+_|0uR+`Z37+`QXqH@9)DHd8Fzl_uhBBOF}XfyoNoNt{J3RE-o<#) zBTpE)lZ2B!^$%lu%(EwvceXx*y`#N$Anv8Ivmf)H+3$_>gRgs|G@b`{lJfBG_>d3c z$V|m3gP-AqGV%1mA`Z~==hJ^bD6c{PZ=i71ssJUtV~FmP@pZ_FOqLGO4y-M~$A%Ij zR8c`oX!om&OXX@>9-RN~NuM(p3pvy(wpGW(Em1%r1GTSSUrJpEgh$&&C*?^?w>6&t zrvV21Q>2-kJOY5*zTI#xB~Qaa?Ada$3Ol6U!9jfYsfY0Y?)f2hwjQy-c^Jl?U}JsK zUwnjFX7TO1)lF@}Sr?p(Q~M90b>|+rHMWw2j1Ro-Mtt_|@5Qu+#+MPCE1+YZBX_~m z&-;#HUU8MmPk_ml)%cH%kI6RdjZFFjtj!p-0R-faIqz;&c{_yjon`Uww8L1t;_UVH+tpS=Xv&soA3Hw%@9i^{cveDu^`g;nw(+(Wo|+RA5DAJzg_uI_itcFv`b~L5>3J zc->5{sg;0lXmrFb3%2?oYd`U-4~A_bF*qZ+=ZZ7EGUkt%nYvcX-f_ zTV^`UGfPErn^g$?5=8z@8%%{|z})(T0_cHm0`6Vuk^jDt5fpPxdI?{k7JdrL%FuYh zN|e;sq3gh53~?=aqXZ8yizmY0cvp`+$6hge8VW1RF~*-w+2~XQyN*qHlXfk^ykRHm zXA!BfMK4S;7L8IYvd0k>S=dhcdVBFO*QP)8?2w=@wL13p#-F<#@0dX2OAsbA|W#X%|BygTB ztO_qIxvyTZ7;7)OLTb@>9XTWmU1-(sdg2khZq3E0uBi04KVMn+>e5#S3A_rKLRfRp zNdG|69K>|Agq72o5j-&3fgcYa$C1<+`ne`uE-Fe5dO@f`A09{jkq)@jbygF$%&x(P zk#1fe8C0f3(a>@S(*iQhW@b;^o|VWExy0PDX&5Q9nh!& zV$3Uu$0(fl$$NazwBAhTWLlasKBNIS4$SjgTwXaek{*lQXYQRC;b0vH7&FJ8{G!?d z^RD8nn6k7z3K=7t1{7tv3EF`f5+?T`5;WoAJJH&FmT+*nB6+ujVCLbU1o7qLp68#% zSMIwD!=t0buV-hA=Q%7l=1a~-?!NJW{|f8uvsbQ=u^iaE1>=GT{`sbhFUPyDzMg~h zGPxzTv9tokFVhi~5R@}BM!9FuKX&{%ysvr|T8hd|{=fX>z4*r`??VhA(`OxdkR{<< zLz5eK;BohnCCn+00SUl=O6zJ-USBIUyJe|DoL@T?Yv(OPOI+feb4= zz9A0wIy*X~rc;6>4$Pm(^hj`iSwRf~1FZrJOa*k}w99Y8$lTG6Hk|6|h6}RIRN(;>ng`e2(qNPC6@|t4=QY=3a!EPb z`g-s;-~Ug!Gbl0yrvyx`RhRBCccEu=e7fHT5sw<&%N80$+w|?T=-NLcaYSXN+w;_F zz7WIv4%BiN%W+hy8$@DIXse8p<@v&zgx;MLGI*?&sR{ei=?Ku4b5h zP96G8dd7+;jF(0JRXUk;##niWG9*V0%291P9lxzHU$sr4N&m?0x7+8_lLUB5uCSji z90jGWKVLlh-3_0M5Bl!o$2VuVJS4^I%ZK~j4Vx{_Y-N%VpP5UN%9qpe;eLFu<9VD; zjbr8R4wUr{VLKb(+Vf$hK_5b9c$60fR?W~oUT3+o< z9}|@ceTxtiB%x9N$Upuq2kHmq`=aTyvF>wU!WpIKOP^(tGn}|a3IhxjxH3TCoi+wo z8b2RBjR!|MaEdRA$T^VDznP&?DGQ9&mXq@18uZh>nE&t&D~_>}LL9te2^g;jR#Xs< zUA*s}#TU45s+fiqCADZOEb+{Jbb@239AhX52S$InWGlZcWmCx)Q>Ro^=g+6V0x7RS z{~eG%pDy$VQs8A7Edj+l8WkX8?k+3H3Q z)l?uhfYTF>`m^Fs6q;b#m4=EMBt#P!b5e*uYV7X{?Fihnuue19Ksrh2N(zUB?%)8B z82b(}8btYfX73LCE!UXe_1sg&T6Pp=KtMR`XXvuaHF4VCX*}Y$Zt4^)xbS=&-?fKp z&G&NQeH>FbSp1vYK7bp}zl5)ThZ;bkIe%HfISaSpM$Z7%=H1tS0zczg^fy#Sg8v0G z=Hb~R`=!CSC^N9LNZOcH7b$1#bBl&~;&H*bn;s)^o3x`bQK8Y$i?{>3rAcbc1^8WA z(}-J|=3{Z?Bxwn^wz3LmH#AGFxu{d2c@Utfiv!)RuC6e81r|&Bw>d#6W~NG|KMkn0 zRy<|?QtCHqEA(u701&_SdtkC-1ARbi-@|Y9MWt9?*?_raH8?cVj|@*tT(?xl=YoNe z;qWL;K>KPy8?Y%d4R}F*sdnA;Yvnf1$F>9gUAS`U9F&%nNUQ&QT8~9loFemGDu@;a zbp7b@B8ar&hBw-V9G+r8p617J8_^&ie=B$y6&T?(Z0ptYXQ6EAe6+5Ao|jiO$TEcF zNr2_?POeSoz`eclhiDoU-qRSn4 zg5Qj7I5Qw)rLNE2J9ptb-~TTJ+;8*T*`c=5Dh|6mbUJXx?HEl&hM+RdL_n>N2j3X( z9oIc`Ae~EGV3)+No33xZr1^5gC-F-CyJeA$KN7BsYd!Bcycq|%_I8B#nJMjqDB!^S zl+cn3If5^MiQ^AdEmKiFt%-y4^6&_Wj5dXBU8nHqvHiHbc@C<$92m=+k9-RvNoD4n zx1D+s8g-#He`4$NXy;?6_!HaQ7tNe)hf7Msm!?ZTf8?9M<|8RGrkGx2Jjq=N|2A?A ze?N2xTho2$?eD{ATNlP#Ph8!^?cLX>eB>$HQ9cKB z4G+q>K;n{#^zA(Tw@@CS{|-s4Sc$FgqC2HENXykXJe?;3ZWIoLom2>j7197)aozyj zD}=&x=A+@YX@M5XJmd_(mM{jywn##`Q=l82C3T~YzS|R;5koD((n%3kLyhB1r4w@zJ%3u~L@m1xpRItQ1N>n6cKVeU4} zwWudfpOU`@PzTZ0b1}9Tsx90aq=i_NY+YA*4uLw%RchK#K`H{;DUj{%0Q6!&3OalU z>KZu)i96hrz`1@;(R57_`nBh}jN3*A@cQWs ze0UIU+qdddKgjQ412VTesI^v$G z#yG_sDL}gHhU$Akc_}JZFXeSSgp#fStBjahtV?rYd*HxPluhT0J5w4kdb&gU361|* z8Hn=36G;WBSldnO8((DIr=q4W{l^7xYxho|uf$`fsLYjlJl>5PHsFza??)A1Y*@Hx zkqKkP=?eTVpww;@7`gSH&CQL|=g*D91=zm0d~rd;TO%Fb5Fl;~2%pE% z%iHpEClBN1v(EPC*_SV0xzIYvR*r5x^52r8BFvdOO&W|7B(}S+z4S`kzsrlSE&3L5 zd}%uAi#ilr+{aS(xS&lw{&4gp{(0mGc8~UB{8T%JdH){k=mv&HWOz9Tidz=C`5bq= z4Q$#Jxxg#zYLsxz`lP)lO{}L1Gw4f&J79t$KA(tVu41Q^IEKLE3&zrtt$mq6+&6Fn zBfMT$l-0qW7dckoKP4F?=ZlsNEdyU&xPDYvX-G~SMdGhQk(a06J>>!V@0iT_biQ&W zR>spT<9hH3?6}3G5v0 z!HdJ)IG!HDRTWbtKf=z&8y2mU!0&lce!8$|k-N<(-r($K`sWRw5H}n*7c}wr^2U0U z*VoB5GOMf_?`>I%#**>~JS&t}&$Z6N`nLc|ZJizRPqN2>lx`Cg#A+>eq$v;kk9KUmBDL*5L3+A2M8^Y_6RozjY4u zt9Vjy7yR6N&rJzBLVE<#qwiun?aazL6msFTcW?lQPMt6VL;%}@d!Xw_wF%o2J8?I4 zNzx0P#t~30I^{JQM|86hTgAnKsW2L+x5QgVf~MAEY}OPMwss(;1Zqi{2q=%73eTN3 zT(pWmx65s?VgCXa5oG!Xpm!i-xYj()`+4Tjaiou)LZ-6^jQ^$h zI8Zpdg)gcVpmbmawR?`sIQ#jc%8}t-q`P{NIn{~rBPWp&pnhZ&C@VpVFH}f{g3ze* zztO?~FOK!#g`qB-U0R1qagVxUyOdADg$#*p(_4$avheMQaK4KLpZxjsR|e$)`YVJ& zpkBy3MSk6FNpE6<951u(DTf_qWk0Ng8v#5STKKABQ2iMMV%gRNb^NCbP9 zh^hrUzzFCjO;-?d$Oqn(yhnsgC445xO`4M+#(X?YuP8+(4!w0#r|io=%k?dQrFrtv zkJmqecYgiv@z~C-7*=pm!NV&DQ%MM){F#S)WKc*`;@sqmkiJy8IfxwA2C@j~o* zW&_unhg#^HFTMhwyycx}n$%!nQDIL)obi1rSpNii;;y%5v604T0s6O+An1tv&fj7`uFrBnc~Hr@O=0v`XV zoZ5)8+G_cEWpyLo(Yz4FDFT!=$mM_Ks@{)sfH^P}8a#^tIbz$^!=9pmdn$!!`+C$L z4d@P15ui_zt?bZ19C9JR-R>UH?emClou_@DeiPdCrR8f5ZUIxON~K(v`r4iKK-w_Cbf_oKV@W5pf*!>l@WuT%rk zlrziXJ7}{oV`8Lnm*M%lSQr;$b9u3Uu`9zD7)H-(k?|F_^@bMX)8V;sP3eI_408c~ z(s|3}IrA~qfAzNY6v>kr<@HdEXkmq^u|jN{50oF-4^!d0I1)#T4AywWO#CvhX2mTU zzx(?7@G#e=Z`|?%RYy2lEV1>B!OMmUl3#u>+_Rs zfx?#$_t^(3C&H`8|EbX-{Ic^1$8e+kZ(MF=9}!#Hg<&`jrp62SLSLOaS1!Q00_GZ& z3!4VadQbE4WAb`P?*a$SgD-z>`S}({d}T<>mj6DqX+2uo+GJjCU;CDL-d%9-DVs#V zNy*b@1vVcN{{-P*8y^Y(V&njh@rBFby+<(CCXOw!&_2Y;4y5-TMVh;128Lw6kBWr< z!~1-}lzJ32)S$S31U0)`cL(LybQioC}biK6Vni>sln4Ap8 z@YG-%E-Igl3IWnNALZ%r3eQ*{^pzD7%F%Uq;~@S8-=QJ7VJT+_Y?7z{e#-B8eua_H zrq`B~$t@;gLeFGd3HF4oE3M#ROOOZVuM84`pBU(d`9#VzI45-#=zHHl@6bRjSNdI?qdnf;X`My4(xrSlbsD%4vWQRpIPbib zTzg)QZ4W<=aV~rb<8NR7gOB1P*WVkVS{^TO9}KZ*Q23axMTC^7lneEnJ&khY+rk;!8!EW-|g9 z+1M8RGytUKExcspe~c@l8%SGXu<9srC<$U?gzVa&W0`#vL$^QXj8NfNYx|+!x<>}N zR&vzDZP{K>VTTsY#J~lM!FWGS@%dJ0nF=_NmUSewY=c8+du%<*rZ!;GIZHy2Ey`XQ z$16zb_pn~g;te^6FUL)l8D0M=y+9*&Z18kL|6zUV#TZlmDr+e+&g!P0%Yz#~|6A8{I5Y~F4i}Nq#L%Fei8#QMph}!`7Jp;o5{wIbB~pXV-Gt^` zYSqU>?fQ;Wtu}vd*(QPOqxRD08R}LU~uOFWcr7~oi*M47~8rR=@ad;yb;hv)o8bq=-cTBH=tl}1cim6 zT3#5GO9#<7ta+q63K?I(8{c&h!%sfX)%hb~{~aDhYG?$f#)t98r#7Q6GbX`%fwQp| zZkc2ww@lo!^p&R~VIQfrzXvD#d%aix=IOUfd4T>3rBbB@7r&Mes)_WeB0gbPaScyZ z-a7j$hQz=>$XC3@*62*q>M!(bw55`H1Pr>}(rnj;EokOkkf93!?w}k*U|Bg>Pl0)M z5l~WEp*M|*Lmr*HtI5F|o8r%Gr&YXb|1_Dz0ZdPXIy_w5Gs>E3Q-+|gO0(C@k~Gipn?jS`^MVmeP^OZ(ExFGl;3V>tNYcIjdGGbP<{+I53xbE0v_<4UQ019CK@r$pM zK>dr?zbypnPRe0mCtCf?JkLR5Cg9c7(|7(?R9=R%hC1ma&Ya)WIL`vcMgtv0Abeci zD;@+c0GvQ$zxdV96fj4?S#N3A0NP3ik=8si6bA#eDfluP_>?|l4)Z-+B%oH`!}tpV z#~7XZrnT+O#TB@!ra1(e{BQM)c_=C@GL^|_`2~Ts0FbSi={QoZj2O!H6WE@0buooT zY2Sg>-qgQAM}kMv06aH1~7Q4D<(fUsF47XQb28x;LQ zYs}MXYkLUF)mC@@ERB@O?d~CXKi1u0^X01#x85eQsNmLRg8!X|4@!&ff)>u04U1g) z2c*p0IDP4n@`Ia4;#QFk$BBO#JB~K7ILE;`8x60h%Lrp|2abin+(co6Y)0N0Ur-;r z|5@1|+cTr+VqrXbVNQ?JEC|fPnaLN|st-T)0@6oLgngOUo4h`cgY>^|--$xzDMz<0 zC6$9)uD&wKMyf1(MuxDTx0#azy>c$h(|-@;x46H8$(&Cwh=d$Uh5VZ^{#Kqh0QZWa z@ILeCoVjm8VFXlDa2_KSHd>!y5kz)`8B6x1E8N)xlPu{({+ZF*MffO!>@kTuAdhDJ zkF-OvNx~r=1@rKxC$yx+2o~okS_sfW5dRfg@~=Mh3;gl-zKQL}4jZo;fJE;!{}Bld zhPYxyZ+8!LIwbAD%ZtM+4_i@Ji}~lDgFWjvp}#Y<27c2;m*a2V`F^QE_vKFr?+qm{ zul8I66w@A+0HFa&=r029!iu}z;_gykW*iR>p2poAq>C-@)%>reyb6mOTkyW~F2nTN zda1!NTi*k~s}*;j=`VBq__~m8b0%E)Ts@;%0vtiFYn+X_<+aL-4Z1@h9Bn6$G~BSX z=6dTPwVs{|Y`|PV&UWi`LU$K9yFgpV7lsYPr7R#G+cJ;9XjqNEAcz5Y;eR>@>05@n z6V2u zVT{G;DMOW$&bQFe1zbD3VZ#R8d(SUWRb3Ssk^?E{Bv9s?zp~9cF1Yadcf)z+QSV%0 z+Jq+!m$J=|EMYO5hVy-M)Aa#fA3wgu3I2RvUn{ythvnIGao716mn%j_!)LKYTNpaC z&Ybh#p}QWz$OF&HHX77dD`1|9wCZZB`|amn1#jMc_2MflUpO}(35z>#{i{!z#h}~Y z^w!XdyRV#*+L@b%+df_L)Q9-@!Av)Pnd!hV2gjNI&=BM}e@?ge$b~U8zN%*yP`f%& z8<1VJZx8{mm!6I;u#`Pc8pD@#EDr3kJ1xc$F(*u_mbWg5=ObJbzowuXErn%vUfjI7 z&u+Q9W#=njT9yfACyk1#1*5znx!*qJQQhwWiOws4cwK2ZYSo{3uI5^_{MnFK4vBN- z{aj-%?z#6;5P={Kgbe6*2E|Y|vj^FL+*4_QA@T2mT97nsHnL27GM#|P$bk}yjW`JA zRcpdNCo0ub(Yv(^zzI$BVWJS_LP4)CgM|Vt=`HhQ zp3sKstT`ZnUX~!^dzi1*)>i!KCqIR2uecHqJ^6SX1X*>&CI;NJTM+pD`0C#!=Cl*~ z>jN5YSYKKcsJL+l&g~~|IF|=Euaq~JS2sUy*{b~XrpIt!&cjWiM~U1M)n2W}Il&$0KlFT~Hh^0~hqVdGeLLo9CqbDiT`rTfgvG zqdgbK;?`brrJa_qtA1HulWIXjL7?^SwT6=vqy+9bG>=bI`Men!&>;6=5}5q$OJ2C@L5CIq@&J#hKdzd?K}e?ao$mW3B6 zkV)*B{kw3}9iPX?|LYqeFxMe+RG8b(s1|gDFT|YSAU%`uN5WV?SFAu zt-KblK;HaOjCye<6hYRm!HgUnnBonrstNF>SxX{~GyiY%K!@~U7O^tq85UGVhE$sw z2N*Lq3WQ^#dQ_=uu&-|$h_=2*;bq)#-0goP7!qTIzMou=ho&zuo1dX&b1-z?f^cDz zoEO)NJEPq_XnSfC8ZSLhf^<1n7KG)kx3b%Y1W6pJ&`pltbgR4gD=bh8J<;)p0%GKl z4%~WShAw7kO}eSEAZi=Oj<@1t@A(7#83*gJ^XUNrLDbzdPgw;`-+jP=f(9QL4+4J} zyOQAp+c7?TiwnMdQ3?_OVp6*F*T=t`t`pb(vHlQ@r^Bu7X_gMppo?1v5N>m43VbsK z)W`Q64(qX|62XF*2B5R?=sY?&@LO2lWY<|zUVY$?^6knF)E~*CxWVC$yMBnPKJh_p z*tJbIx$5$A-1(WmftP&kfbx}8rY`tQsuSh!@xGH76*n-5Fyid~sZK-#Kdk-+)gL2` zIMJ@_eysX$DE~M>J_!BE9%n7KrOZ}%;cs-uev=1bMgGB)2k=6>V>5u}J*f>Pl@oPS z?v_kNXRhE;#=7aA`N=z z%;0K(G@uRKu!^WKY(z1K2Mvt%BEyI~&4n?zvuB@0eOvj^R9X1)+;<-F743)+|AuEB!?s3i$dm`{qc%#8 ziC}PGySw!BzGDigZ#5eAY1MW3;`MLGQy=_u+`_f%)x~9qtr`S~S2p$IOALe=^_o)U zC(5*?w9;&0kA71YP?5g`Y}+6haJACbS#lzrg@6mz_IVvPD!YIz5@ieXM%BcRV{z5C+)31%60F%oqaB<^Ib#9!YoommZ z={aE*d1xJ3M<_IaWcuB1YX)G-@sYMm(oQWooN2GVmO;8~G!_ezT6D4HoJD;kqi$CT zpQAJL;=iE<)6jRxGO;(_)VBuTLdz|zKX*L68BLd;kK&rJ-3d*)Yy}obCksRx?9;!3 zz_RMEfAyJk8YWOXF7!Ftj9K1@Mt$*=(enU{b_x`*u)yzLw+`1_c@_Tb)5MUR4=}l4 z7qH!301)H=byj6%0c8Yi#Q=*7$Y}T^9%#JuH_4sG6(sZtAAr)|vH+_6qkTquo;GLC zLj!Tc&|B8^_Vo~e7c`1Hv_lXWyJk&+a{)vbH8jDyc#)JpIA!Mp?k>RN@4W*dFu(dw zKFoiAWcqDOL!$)eLW7PZFiFdf@l`i-}S(*(lQE@;n32%96JYx(VHH{&kk+J*S9}| zXHOnRFINV|>OegQ+1j;RE^fK`+(~&Qxr?2nM|(Q+=hJ@+l;0nnr#zh@=_`ZiR(2^n zs)0_)kt6`KCmJgBs=>|xb`OZQ;vuBd39|9Z{F-`KNRCaCBn`yUal!^H%9auABd_KR zq~gJpkIp-ZQ5tj;ei*ZXx+6IFuo}r6`i2MbjmPf8Hy^)WuHL(xhxrdkE(9RW*xWED zjabEo@r?0y&@s@5$yL=+JAhBNW0<>YIYzj!er)$%>DE*?NrLkxVL@B9fj|kzdI3ep zCyuj}yi7-0-15@ENxMfz;b4OwH=cw!och;ok7^`;-hUi-_qF1@vU+@|b}r5>og@v& z384P*i#ze`;l23sQ}^LmPiGXFub~HqN!Tj|uD{Tspr}Cc9ksnkQ>~w2KQk9pt6voZ z1==PYbyHnG(yasL9Jv!F{6E?hp9(x_kp7$^?-mb~k60L;PQ+h95NY{K3yQ2VlSM1a zE0qGZgCWN(PZfyY&#bsS6na%Vx~Joi1QJoS8sKI^0alKEYAk_0vV|^m!6*qo`&s#?97znqv>qUx`=0jfR#$??vscD0}BT9lU zCItQr3b-a+UZo`)S_G%`q!H3NG=oR z$+K8U;C1~R*Q9UhI*G+~jhJ6E8I5I?R+%$jzI_1Pt!wS(hV}K^gudLv$IG6PVbqkA zqJi^On9A#ae^bf>^gQM13`ndN3Se$&qZv6@4qH%+4Q9?HVhKp}yGMWs$MC=w4gn;H zCee3BjDM*#^!r4%Dl_L-MFNoA1|%ITQ}Tm-29l9H1Wvq`v8a%F!!vz%!_oI2ZF~}+ zz3acwJ2V)DH9{6(mv~?%-)Q2rd50GAeW|JZ_VbBJxYk!zS-z;(1?QiGp406(dGN4& zZ>pUnx3vq9ZdSk%P54xB7F1RE@*8E5$$|(InNZM|ZG+@n{3`@R1o5rUt_&MXgM5hm zg%2BtI^3a#xY?^Kjc?Jl%ByfdB_g z0dQuRvVoY0N`ZAo>Q!p2SY%Yq0Bsdd3@qs}(it}x8J~jaA=akFyVx03B@8vPzyN1` zu{a&O%)%D`XS}Wg-B+zd-9yhKH7c&w4CI~5qS__M8XA@$z47wZIQAj+?LpUt9 zF^g^QnLsQR@^Auxi%+Srj9DgJ%t)nlegtR>0lCn|i$#T0N@)30@>c{D;9M{GNd{$^ zspXEq9+&AD$`CCQ=zegRYtaAYAHIaUzxM-t_@f`ijceZ+wGDR=Z~>lM7rtj&{_cF? z0}Y;VCQjF%N+;G%sDB^tzH~feU}BtE;z;COrQbC^Dz~?5>+Q1r@OQDDI66Gp+V=*rF_w70BNj0euyvliW`8!Ykh?=;!^%zsB9k; z22RIt2Y0uel-onkZ^Y-n|1Gq3oHowlDz#f*bG`f%TiX-K7boC=UF2>{x#XHa7>gZ} zziIK;KZsOep*&h+fH!E}@xEow^+Zw+9S`-m6htFSlpJ{qhc3Mkpk|}M8LxtHL3ey~ zR16#_;%SSz3hyK@z~TOG93SjMO>rrf)J?&(@@l)UC6%RbaYg%e{kd&4TfZjn0;ms; zjiHW@opq%zzrCFQNt6fZdCJoSNNjf$7A(^&zJ-TslBkel$Tx{-fEw7Eb8mQ^jt=j* znaLn3w@&2i`d4 zECddS*1%U#Du`>R5!jk(8(aq_pTb>OM;8F8Bq+3jw*IriDB#Rsfp5)67*<(mr#u<( zANkQwsUUi$anf*NE3MFW=(Fq{L;WU?vZg7Bj)5heVps)loVggUX*o-^E#Sy-AAa6> zSOSUyvE5b%Qnu5hj*d`pB_K*ysBlXK1RT@vN?WA>Xw(Fl$*&$~O{FM5QMYA;53&Hj z|Kv>^TsRI=L@Soji=7~#-5#UHYAevUY94CV@8$x32I1D%KwhCH7KqY=0;F$5<7EP* zug53{RYH57R^^zIc@}ax3%ALNcFX@0H6*q1qRka3V^!*lSXdKvm|=ldxbb2W{}h0w zLgQkA1=8U>N7WgYY*c142TTEW{&)QNaeU^JpTyU`_Er4#m;M2(m!BO);1l9~0Lp#O z1rFS}DS76&Seh=NNJ`5C>XYX0n13Hn*?>+`e7YZKt$8{;cdoa%k1C^Z&|BEm8FT^U zxVqY$lVJ7>fkQ_O*CvHs=lr%2B` zZn-(-vtjpk-0|}tW5cc;h-3zawHIBE+i!fc+}5604sO}eddo&h?Ic$n2ohz+%L|Q@ zFt+m$pBG9je~d;i1_s>$z>sn#n`lf4%ot>p6$r-)1Ka~GQq zI5ogE2&{Bz{64?dR~gZoOurNV0f~qD75ABc z_U`|}y<0aSRwF^IaG0i49MOo5D7AIqg^mOG82+vQAxEOU(#d`I~f+qZLxPj z$Hxa1P~V3Sa*g_VrS)hoDw9C{?aME~soqX}?ulRH#~iSSk;3jAI-vl>F)%+PTw_^M z9WJ1BjSb`AP!HyEp(X;^fQH|GKpXX2OUtfC106aCU2IY>{1+Tp;7 zUxP`-X&m$;0^kl9r=TAWkCboWBW0eWZ-Tdjqy1)!dj}V)`bI|xkhY6CiNt;9@0hm| z7fqQd{fUK~-Gg2D`q8ain=*nxf{AEx(;Cl2k(e<{ePQ8;sQ(8h1hf3wkwvbmp9i`x zN7_@S+6GGiPmHt$Vf?dF2$ziXhv*;aCqu?FeMWt7))WL|r15JU1Vu2B_$EW`yY+(j@R$nEeUwifLsv~9pr;euO` z4tbIqb@5I>et0jsfdp0_LYKgtM^}pba7+s9_Nm)J>ye{)&&_Ya8@VR^BY*gZXl`kN z7vO4g2h?2PAqxPoev{G(g1Ebz9vS;$?=BEw{xQ>=SL)vdEmea?nLiMxYMhe zYqDEvCShUIH2Gd6mS4!d=hCR6y5duU_wjLZeOd_AF)(P(HmAD!k!cz?n)48#XK=%% zS8{D}0a7(pa*?xOXpE=D0iV#&3JrRyKrZ$aDkFx8c~|+~uxAJExaTJvpzk1)dVtjn z7UA|cycw$(F0sRBn=7&{=w$Qfy8FuBN8)O;nN1ku3mGMq72#E9isZfJRFF}>0Rl^g zGqMut97aQ)Qhn9*+3-jW-TiHkLp)?@F@oAkjP&%%<1K>6qWKh@7laEZsg%*Ei#F25 z6$JfbqY|js@wQY^RDzr!+gCoG_H442t-sKB6$fA9#EF4kbPW%prK&c+?fncX576_J zClBd!=Hu$zmvm$FY!b{MM1iLM^-V#H0?S;0hD87{=4O)j44|4XhabwT@lPf)O9RSu zxWGw=o&|N-C43nfTbGw-67X)-C&ICg284I&CVurf^KXcA=5jlG;1TrfR}g_e@P4Mo z`;05`8I?>Vu-p0Q9|?`02~GOUB?~ajg{D)7ju`g-`W=5D4Z~S=oUkyXBm$JZOvd3D z{Zo8fSF5RC(0q~n=|!ZqH8b#)&OPYMjA^f4c}Zu9x(5NWF5U}j9UH=DIZzi{-_PYB z{fkr1Myc3#SyPWMUH>+G{Gw~{k5Anvwdz*qGl_vT$mn^D1EVwtP6b8b)OXj(z4$-# z&a<`8QmzlrMTel+JwG1$HQUt#s!fOGwyL-m{cPNTbGpsk4NFq67XehZeA@2-#NGK8 z#UCJkMn3#~eyB_GSD*~ZZ8>$!6qg=10#cEh?+l{FfrhdQe0=Fem{w7vz&XQ1oyYK_ z6MMoV6UtDP0O=s$+KhvNJ*I@`-Kn8QsBWM&)@pn+AS`+g4IF~hKePSPPZaP&gV2DC zUjomlh1mecE=D*X1a{o87q$fT&1;tTq*rY_1aZf?3>$=J&Bf-Av94b9ys!&XFJFTb z_dgi|@gO`itjktt-N$7+ROi#VHasP^wHKJmmw%#YWpiX{6tEE3Q}7;En%dXEV17-R z1M=#w6fK|VA5#%5i0)9=U+!9W7w-DmI;?%iTk+vP_+K2TH(O!60Kom81a4hQipvKO zT*?Mkd_3ji(Tz805LD@iE2-|{IozYqr$D8IS)(EI|r>oQ7P9vXkksP_=07mSR<>~axC2#w}E{JU`ZN;K5g82~Bn>mH~m#`y3UDy3mV z1LP!jrGojy8PZM|a}E=Qo1R%&X@w;lTXn5SY_4@RhCWmJ5(4)dl_L#^(d1 z{I{x*1K5sUU{W>M;#o)^I)%c?br=^r4*+{1FgjbKq7A@Q8Cv}$5|zwc!mRPg$9iBz zQ8cGTj<3Ul53yj-HzYOYAwZ8nc@Y8W#yu#&aC!{wLut9tT31{q!=~uXFS**WN9JU% zj6#2Dxzl*aw_)CH_jkAF&!?Xu75pV9iG9<2#5Lc#+Yk@{k5cuSM3kO6Zo1gT= z84y9&`sR5y?;e50hK$-VnvBBvBpXL2fiMP|ZeWRK(2&*_8!ZE7_+eI=`SR@J!w7yJ zVM-g16W_o2#Qk{pfBq95*|k;PNp4aPeB9Aj2mE@z2WDY{`lF#7KoJ<7rOAU3q!uh+ zD$nyB+OgZr?YF;fEv{U?3QmNHWuwcphOdUd)Jcujk9HC78Bnpx{k6e4H6SPQSj7=5o zuNn$UsyPQL}Ah2fneJ$QWJba>qt zciY{50_%?*P<9|xn~plLD#E}jPVvuYR$h)76}4uu=i4WC;@-AH3X}^VF)jf`FgAwb zQ{B?oT%h_&R9ALd`i}vm9+|niNFN7fuCPe!TLrd_KEZAe(EXkC0K$i8_d>G?wJ>1! z6Z)@nyeQ-rBa>@T)IErz{y`N*pqgQr$XNdX3d%}RHD@{okDavU67i3;nGgh4s?h?f zW>d@$89Up?e>iuT?d=R^;WXkU^qF#t2X;|fJ2EW%8P><{t=sX?0}pV7QHT{QR=_TA zr+#!f`2dvb&yB|i^4xT28a^KAvm2kw3-#yYBS(A!n16Hq!}!F{zazo9(3+1J1GE0T z2tR{-gl?G9h%w%8#NX)X7}uVUaJ)7UUah9ipi7{x&Y>4<+6C57i}4lnke*r}0`swP zsW~q%D#H73c^k^h%B-|Hh6nJ)txw`j%?m;;dZE(PYEb9!Cl^@+eRK~t`;R{R4*R2mXejk`8&!b$vyYt`+d_|ovluW9@ zSVaj^LnA0qM*-yGL=cX#0UZZ}K-oorHKyxt@f^Yxojn^#;l(FtQ1S^0P=XF% z3BGpZakmHn16~n@grS0%HIewjxX3&313TEFAtF&TaxoJZ==|#udXOLpQvc5Pr}yu| z$A0il-1*ES1enKTjKcfgy(Jg<^A!Or>+pZuw&J_p2j%r_ErsQ1C@PiUX=U?Fyzku0Fui&bcD5al z19e&m{9SAf6=hjm9tx_527BfEg33u2xQ>8s1X#^I=;m6r2)v6VQi*^$y2D(9`hp;^ z(lGa{2N7_maRIQdQj4V(*nDX>dJnq(J2p0m2l`LQ`W1W(j1J@e13S0?l~%cfU&fIEU%ob-u2L3lZr3U54 zI*=MyWy{Ez#Qz^T-GQ=cQ&3n@hS830@-6bGqttE9Fl+|RKxz1+9YBEz8!aS6R_v`T z8UK1L(B-G^UpL7hUXJogDWxXt@9)P`k3EicKm8eMYO1kt@nXxb1;{2kAn0(UJS@tk z1$V!>{P@zN;T%4LZ2q0+J2yRn+kf^gJaJ%`v|*!5Z((CIu3x$e*DqR$*DYR&s~0ZE z`CMy0r=bZYg+&~occ8bY2Q%l)k@CpscnHu(#zy7UWg48Pg~3|A#hrsoa1cJebPm_N z7b#$_wCCLKn_hDrmd>AVSQN(Q?mxN*-`V{f+BoQc-|VwPfG#xX{8wKOrt>PSv48V{ zd+>>W|BBq!uKS%>Ed9jVx8qCi|0r4}H=Y^5)8#u60PlOI{*xAW+;*Yds`;Y#od;jU z2w$+3R@3YG!bU|Y3dG+iFEe4FP4!U&yaPxZAYmNp59C)orbxO0>~`e)QxO!+ZU*Dv zy#K^$FtKQ`_b4A6#!y>Pi7S?z9csw+pV{Uf{%UYu94OEpgaBUbz!6G<@Tfu%Uh(F# z?3OG2O{zyXoJ($ar#?m7>*wYB|JnNw0KJauJ{14Wx4mN*i{1!;1z?v%fugD@iLy+~ zwn(|hksbT_IhG|lFL^0S?AS?OB9|mCQQ{Wc|HzJGHLFV&MUg6`NP@iqAko_bSYUUt zy?x)E|D363=Fa^Vv}LPtfN$@;)8|Y(zd18!XmPls^`(v6RYr~JX-j{}ykx$!46!u} zRso!BHVIUQPZ*bGeq{{s;(LArb17F?z2{6A&qt1Nh_tsSY4UUpvnnAf0Bm6M@})J= zGUm?y5dtgHRkD~}86=DNSnb=;Ks@;Gv#+- zY~2kG4dJF+ZpPi8`iyoS?C!x;8@J&9{Ig&2SrF-Dm}FTnS`kyOwY2^%O~GogVec!Y z_vlJB3pTiY!6bri3vt7s6K(fv7oNihE*`^+HB~&Au;g{^`9`%6@c$0&#!sAnAk?4w zPgk?OYxR}5NlDka`N*@6;KR>7iib`zg>pPW`|MpejE=ruYbEf#>vrPybvrBoHvwZQ zH?tG){u~erJ1<0JXZ(c4F~ip-z&d@hFg2TJ0VMGz;dmM>-{9$~$ex@V$CtI_+{81h zq;em8?h#BYkRD?`k)>N09s-@{7CMRfeQNBC1_vbFt1T>||Ai5A zE|W4s%%Zs*-o$OcrM(y2**l=FYu+-;{&ak;%s@&@JKxJi0QA1+3IqJZm}}#QODDv; zRW1BDFzt@vaL{Y|QqUhFZ9DdhU+TMkauyrD@RYVQXYr*uKMbS5O(7$!x%E{>vpZ?u zE=+t*gLh$n`W&(h@9`OBqfpG(%+jhi>%LD5JL}rJ@zGn}p%ue9=_{w_`#gDH)X{e09E#C#v-`&dL77hgLVxjktBbjDJc?3 zlbrk4{?q%hVK4@gkx3@CuxKLCp(fpRojv%Q8Uwy9ptc~xOUw1U^rbHl6vY*iD~*#8 zQ1|XIXL5gU;t>AF_`#?+Ci}@%PPXlo7l~5dKzcHh^!%T%Rsj9f1KyL@=b%0}wsmWu z{saA+qc{nme?$TG4?p)vh&Tb8-9xMNj|@phw|4d8XSZIDVY_3Y5)cb8rE7VKxsH=D zMYT+PKn$>J!o}YNydy~~1>w2$)Q=RxeR<}b-r7!X#K2vco53dzJb@{?fk23AaL(T? z0QL!48jzy6e|r6EvB{oApRF(9cb@$kPEAc{Kv7fDpn=0@O*$5gYYpiZhzkqO;lFF9 zz1sZyu4>bkzD`|!*rHyPZ)D3*fR%%fTZsLWa-$Z#-Iwhk5yGEE_lsK6fz+_z1@9kN zwf__b_nkDLyJ7t~Fii__N^(aaedIIup-{AvyAJ{S?}twDX(`)6fUkZP6{T=0{lsE2}deT(B|9Er?W+o6g(Qm2B^N&wH?G^JYhs zGUJ{D4OKw93Xr9y8JJtiE)B=isPJ=I0qghv^#>I&KN99CMSwf5y$SDq)7v#j&&JEl zv?z36qPnMr(1*{T#3znCi&GP0m{a}q?1c+hP>Wq2oHy(?_T`4XQjNWu6mSPW}x*Aq%^Ad<5_O+Q*C?{pj5OdbM~mJgC3h zW){$P>b$BH#d|E(lW1Z-B+3rLkfN_Y6tOZ`a;tT=Lk{$!KGdgmqv*wyb8120v^_Sa z??4BB>Fqy?wQ39^Alt4%cdG&B!srVy+uH3}bb4Clep;7+zA~=`B3D0`QeUZADQq*^ zSGn1DI)Xtvv0C|g`R{P^5;bFI}7R zw*lxbzQQW1av;Ol1+eZVgw~Iil3QS&hB4nPKJ||jyZy|WhcU7|Uzv}Ucy32C2f!)j z>y@PZSJzyJJ5GEl(1!td7m&2QziahX_BcJq#SWa?@U<+^MgYq~FdWMy>h>DK8~{%>xTjKZlddU4F8%aM zHZo7nPvGIX^9rO(#k`qtPn2kRvkYFBR%Jma=Vq*Nz;>W-4beXS`k2pCvdX3;QxfY3#Zsg zuy)9SeLU1i3(#Bjw)W80$#{iyWLJcpC%m5+fExPy)-OGW*70d0?Y5wR!jg2GdQAK2e|DJzAeg#ZHhvSH$(Fw&&!GUq`B z8|rkwEM$pJ8hAk369<{|_qvV4*N0oIeb;;5jUV{2AJONPOLNvrz$!I-E~x@*X@~(mi0$HXG?VjURgt9ZMPx&(Hg^2bx1Js8Gbl+>MOVw|u&@wfPVq(fD z=>ww0s5HNkP{xfzbU%Gx4~qUS)Z1G0E*ITU=;+Vs3r1>QNS~M2@46O0c++dV^t_cx z(c1(A=OWm(*dq!pR*Igji)M65>A6xJt`sgu_ar{v|6fuh7tBil{U!4qWr$HB?dpX4S}7te?x%oVsc=@NyQ6h&!yxJV1)DN_9>jb^x1Rw zsB3-#A)Zf8pT_TBJcKh0s5j$HuF=zE1h4 zZv(s~+s@ul_^dFXzX+0cCp~unzmUP6kI>P(>$hNII?UP$<;l^XkBjL7v(s}x;{~;4 zx{mE#?^Zzl9d{;`k_-$vb18uEQUG5SSZNlv0<3&YE&RRjzE2upZvAfR8^phUoBA#Sisuu|VDMEd@p~UbJBAQ z&Q%@X{@OR-t+%}ee|-FDT+!Z(9~-_L?;LpuS1q;RN7egH18dRN(&iP9(S7HSAASMv z|C|D>hxWThtFyM}^4)m1T4>&L^C=J%?8S0qjxjTF z@GI6_4rJ3rJu}Ud^zJgwAIyE`cYWxO@ik%&(&!_)$Px&TUP|0}1;CcsOh=!>0#lzI z$)xYjk9ussd?(hgUyldxe<1SzmH+TN*rKFGoMQax36sgIGk;rvo^HE!z{#9(O~APY z91e)b_VL#ED4dH+2=Dh(3aJ0lxmM)$nSx zwYNhnTvK+dJGya0-!R@dv>B`0I+5R=&Hy^Mk3fAMXglyVyKja?eUQlcOL zZ#oP`$HyyIW;<4I=pE2Zrfc&iO$*Egd_Qqu)Pa5tuGxs``pM=;4x?ve+<<3k!%68i zF4wv`vHI4Vaqe?pLZJWvEeL2yxOCgo&DM3wvj%P&Y#Sh6Z#Yoi&|BMmymq^LVWsL` z67Qt(em;ywCSTex_H*kPTuKYTU0cvwd+j2Q(?;*_S661^#*O&-fBH}G_V4@tOK0HI zv9j6Nne&_TlLPs=@JX1xr;pu*=%)XQIscfR6v;Y@8VjSBOa0_g8j z0G-T+mQMWCz;?W~cY_A!x*dS(Oycz91n&Ajf1tO!N9WB|U0%CwCvLy>O?bXX;_k8I zsC9PYXIJgO1LsfR`&VtnjlIKaPH8g|a|JrbrY_>Mk3WpZ4nJ=s<_v7@xqO!a=IU88 ztdB%APTzBGE_}`}H&tLe$0z0DPwOp>)7qb&o5DLE_^1Wxrcc_HXXqOe=&3+9db-TB zv%`#0(#B^OMb7jOH1tm_-_8ug9DJe)Ic z)u3I~p~`=oy&}&^&uhF7zlipiqaTeYOa3N-MJ(0rRex^I67hVfqu}vKTH9#Ri{R~T zs@=7}WA!wF2tjspr#Q0_ZQ9Z);=DnHN}=4F=4rCxl7>+cb?8;c7O` zXbV)u(;GfXNtsPI0+E7AF&#DLhRNo|@L-9hM_Z7u>~C^@y~0SF1{tpK%>jji0hIv6?Y4Kuu$Mr* z3}??>7@vk@pv3z7^vsCd@xVZAA3|J{^bCc|gSR zlK=I!4z$y@R9pPQwK0T#Os0^9k48(pEie^A1lD|RA_8Ddv(12U|t?Y)8H`~_@6WvgZGlj2P={N`J8iZ>w9wh7h=gz@9Y8W6b z;CsDmi^5AF^xk99Q#)CYIqHnlj}u;Z~_m{HHij~&L7M_$l%*rPzqHCJARM;g=k>kCIQv%IJl zy;>oaWVh|;S=`pY!JJ!G>71XQR9JBVPaZvp>FH@xsf_?KHw_NseLwND2AHSiNZ->k zre$OAB_Tl?K1Y8cR<6%W$xBt7_NhVo7ybgLiv>i$Te5k`{q#2YR;@K(OWEl?0_L#0 z8ub}e0BSn}uijh`Zk*Suf9aU0(G>RZvL>eEZhnew+bD>WyL`*hu@J8#}%rRQc* z>=OD$j;3z>Cs4k$gz1@SEG{mZxys(BuHWr=?dH82pC16vzxh5d881yLGC# z&A$WeJ-6MR1APU^Re??}&1x`yK`m08s?VbZ2zRHee*`2RK5-Z)r^ZboMaYL_>00y} zHAKns=-y-ZAT;kcKu~WOSz&uX^Z~#KmQYSC~(RFxlsfsywqG+dwo9L zf_0a^)8JlsnuL71^rbD2^!YMnl)RbnuoZ5;oQla_*Vcu${+@Ukv7y}}m9~?~qL&Ok zF!T@eHUEh(us2eunQ zQ+o`+8<7;AZ+9;kaF54#;Th}}A$*w}o&5r%U^^7p_y<-ujy?{^lOceqx4zprNs-(y zyyrc73w%?s#@};bqnY1aJ?DU2W^Sb|>>qgYeth7G2QalT@BQuByaVrk)os|bWk)t# zsSIfzGG#c^Sio;xJcuvP*8i*v{`kRYj-gOdceYrZxfW5wLdt+51Xi_%{c95X~0} zs?v(S(=q{N;9i>K98eNbS|UnUVr4{`n-j{59}g&r+HZd06YQKhPz}munXoBvTghMa z&8Cx*yLtG{{4W1Kczj-S=Z>9NyM7%WeBc2a&cyi+D?LwAi<0r*DFcJJ?LBh-G~T>% zCk9^R>ape@WH>+Wz5ymBw{HBZHS-?6J@A%XY971C>9{oVSG185xj&sehR@B8@J0(Z zlf%m=C9fj(K~J>;Uu%sgYRC5w11gu6Ju{R7{a5x5E1<5x`p{)j9h{n;z;Exp7avw& z9UOqF_f-n0_w@Flf7L3jNjJWeTwrrY4^}G)|K`pfY-;a8S4$g)T08X50qxwq=I)u> zIzL$~E9rOx=j)65{k#I>^qWzVHUq?k)yOWeq{=)tH;p6X=dthHX*~u?7|<@khCd6W z{dfZgZs9n2XFA)w13wTg`+vY3q=1rGJ?d&)IFwU=ABL+~m z80If*afg+niwInU<(--HNzem*nBE11zGdajR7irL@$2wr*P%h1u$;ics6nly@|byzJ7U3Ue}eg1wAuHCkDr`cTt zbSEWe3gg_lb+57AhH=~3$*yiC?)u$rf!842l!I@Rbwd-Ee&)tA-yUo~bi1w`&^Oww zgI~WZOB%OEM;qS#fBzi*;m`au`rOrBP3G=qU?U62nt>H0Ad$I!Y~SN}|K2aEMXzx; z=xIs$yI=KY+;R0Qn&&B#PIdty^ZuE+Gx(#4!x&MEankOfcuW6A{KT3oFsv3SNwwfp za})SK7Y^aMnQ^nDVrCYv=o!Y$fuopEfb+!o1uGqgIj7D5`o>j*xOv-^*u7~h-oEFx zT0?s&pDzDZbmjhX_5hebXTQ0!r)H)4t)gSR@doJ(aDT?G?0_ZQ9ZyQ6C{gORreyNSx11`t-rJ%XO-a1Br&E%nJ zVsSb^V2=NW-7sRR83=cG*t#^M{PRg_g}6nM4*zm28kaDad89x`dONAW!+YqQc?!%0 zON4W9+!J>%XD1FI`IFl_I)pgWMUtp4WRk%o?Kj_c8y@`H*VM!|3ovGrr01lmSR_UC zCP~xTmGYqM9yxysYdgE~x^ThE z19rG=?1qUVkWSj+yK4s6leE3NrPW4T({@vhWgTWp(ZYP!HB&_YT54e{A;nFAoUVu* zQ<9GHxfvW)jQIK4DLwv04UI8SmRcV4I(t6;Wy6RR$OdV)i^#pY@$AhTFUOlKKzHrw zw-i99>!IkZxgI@Qgx$hk2J3p8y9M&nN^7J@@|WaMMtRTpg^~0MukbC@cgh61rm}eO zB(_|&6K%ackiJg|#0yc<-+d0}2ceI-_@rd->fhM!eHbkx7jV-JFH;hvb}v;jpxFTH z)o;E97oXgZg)^g?uXmDk4g4*bcObnH0dc#PUB)1t?5N$-m2G^hyOXMWNj)O6*`Y5; z&%L$pT8If^6oQx(Tb@mKk8_DoHtk4JG;*qv5m>fZAw@v)=NV(+;V2ml@N_V*k>Zz@3t zwhyhrH5<051(GW?;G`|<9*BF1xwV6cKzp>Uy_Q^~b}95@bXinwV|;8(XPI;o&{^@; zRwFsDX(ew#a(-rh8hb}i;rXfam|9-Kh@uZIX1KUa{7`+Q1AT2B=vTkptJdJE!L``b z(~n&PYit$_4g9Vldt*hz9<%6BAYGqRZ?dRBv>>|C8TD^h^AbRR$$ZNjn$YX2b1$>) z>}IM*^@Q!d$s|DAF_}PMN}PQ<_`7txyEUb>eUh2;<}42-lbLXQ*-&9J%zzFfWvOgc z>6FVQQ9_yNcV%{@-V*o+U%gvjoi?>F7vf6`7L@@B7-Md^FxfbhlB6`Q^dIioMgsiZ z!ozjfU619ZMI1PA06>r^-F*3O{Gb2lmrBuJ5(|=}_UD))TQ{!CM4JM?>X-7xWV%&GZ)$YozWLD*C#&0yZ9Vvf4cFj}t2Uv(r7apW zKJxq%`0dB;!%3}B&Z3S)QroSheIyAXeFS~o-HOp^)6b5!b_3es?l`acGws1XNF?0- zD%L;$ZfT}Pq+bHJSg^?ZA~v<+Wa zztYtb1MfV|pSb+x*sYkduscKIeSi2rP$TW*Y>T_w%8vB@bEd)FYjLlMG6)k+dd|SL z1&d&L^X1?m)4EGv>@q-~{nDe@e(S4^1YL$jRkuK;nM?-4Arl=8GV})kQGZ)JavI}* z@^N&jcJqpBuGb)4$e<98`e4hNA@p9e6Qg%OVDCULyt~uTZqBaL8FT~KozgfbS$ER% zu#MhsajzF;kWPKOUfA>Hh{`2>^9o}W-`eiz>;2if@Xg9C<{+LMeL8kL(aSeh0T*p| z{}=!Em+_`sZ$)I%dEM7%#=Hnn?96G^*Wdrhy;^e4XWTb$-;MXa{%!i|y-Kj(Ebbgg zPuB)^>YsSCp@2`AGAd1H+dZF6Sp_s@!wDTop*al=P~~#op)H8nMb>4}SIRc(cUd0ShnJ{xW%=Pj63i^lhzI;0mV?$x^q7E#H3I(z;^ z{NCTziJRAN#hbQVfj#TCD$qUvzB8z>3x%|R*xKHvfqK@kG7?=#n*QymNbXx2pu1^N z-}Yhc9M}C3hM7@#O#_ z?l<)p#brIGel6Q5JqQ#m?{Qu@4GD<7>Xut@&pr2eKMqv><~x55KlrLQ#B-J4gk^IU z0}WXwK|LNle;R*s@G*S)&HoUWVmJs6=f^EzKrB5c)38j02Og3E2}sX4a{$7D2S0}! zr&svv+`<<5u$|u7bItH~&z{AfT|BClzeV+8)Ao_uupDH&Hd`ZUHYx65Ov)P3j^FFv zg)zB|znnUOdsI4}sdBLqAZYK#9ZH&h$MAOb-V?>~k>?-BA3yUD_Klr^2em>lfXu() zzdJiR&`r#PdUu+Wqpj@1pHkNn^V!8kHKr|V-0H)kNZ0mM@ zY7C(IHkic4-Qwg}dF|6C6UwW~_|LC@g9Zd{=f#n+QGEErAJzcW0dcdf-AI1|AZORB zG0vQH$KZp-J^))tM z0ed!W$GdNSBlc|E9>O4*4b$;EwMz|elPdv7=5}Uz4u3HEJU%&o4BZO!-L%+-SJb-< zAlKl$%>Z=$G!k_BwJVTLQd$D+ZT4C;kO5UQ9h#b#`M&NqA|uf&<=0QeKPCm)F8J z_V!3AoCE0mym^b4oTG}CDGf68yf{9NSq0Ertyc9m3(`q?epZ$5&-Xuyj~{+UUqiDv zA*$Dk{!g@8-`kIF)#krl%(o;!_tKoC&6x=Ve%`Gie=g!B^Q~{F4e)d3WH=M>Vhxv$ zT#`u!n-U{a^__t8T)d7Es?00Eadas?zKva^X=Mh<@`^J&*bxB7Kjl;<3uDuEeU9!k z|IK?ofd`H}AN%DfxVZWySN2&v-K6iSzzwtcjS;I<#Gnh8S@1tP5<8)k13!&AKHMge^(VUviIb+ z_FAH@q>@C}VFl2Czh}3VK(ydOV-f#*@)#bNIfv=OoW*tNIQkP%|AkH0VN*xEh3>&K z$MBICp2SBEJ{bd7_uzAmNCBn@-Hn0a@UZRxVhOf+a`0`sxq!cBjAbL(32;n}1b|2J z^ZdG?+|5Sh^I~K0`LcO>#hOjH})Qx07J(SbDFCRaH`A43^&L6%VZN1%oXz;s1>^Peq^KfHlVdoU1 z-Cace$fZFG0P|0n<30q^ckSAxrCHK${UlEvtNO8e=Qf;w=m`(nrQH(hG`VYOx{8A| zxi{Q3YW_+!r^&tUrRruoyOpXp*!}8E;hd4EhdveUykbl;2kfclb_d2Ct;14OpaWmL zaM!ZAZq4h1{NHisoqFrL*7%-;k4z3Y<_rt_u>()w{g2*<)05+VETHY|?|tp};*P6d z9><*niMjMr@RzeofnhFXhut(b~FD;yU6CBm%l1}8c$Wes`9c@S<>(?);_84Llfum_=Qt=dhD#x ztlrY8-;MqXvp~po>ymPr>1dn?1I*y zu79ltpwFqf@Xz->g8%;bmr-eG5w%Y(ICpjGE9W}Yvq!b9p4N7KjEjElR^ne@TE^7m z6voEJaQ?!176-z7uFN}jy&S)I^P8}#cOYCBNg$nW9U+=6~J?lvC| zY)5V^9e?L}nnqN8BC2lZ9)TPL@b3IE(UD`w*Lz3XsW|0WD5HX`vdY758A&t|dBjR*ezJ@xwaOd`G(4iPhf~EAEQ0@2LL(eI((?#FE9QfD3 z&5t4RJMT1ZYrdxu%AU&mYy9A!hXA#5e{Ahx9HBN2B`u0y@{N#?077EGddED&Sow z&@ZggL5Z+{LVs&zog-mezsI@5k_gXCBhW zN63!0%l_=k-+=$}-9L&OH*7%(3}nXZsJ%sFK++Dg^RFwc%ztOXbtuqA=X}>F>Ergn zHMnwUt-e3IU-iKjVRohvKnTOv{nlSA|XM)aHpKN-_?Qk4s+Jr|61DBvmL#y=0^)$YX(+f^Sbrew0;AY7Z)*gaWaly z#l8RBDSYAZbJ#kt8kY~Rv&X)yJ|)s0LyLD_X=sx+tIVPrM<*8Mv@R;zNm3Kvx5aktz;xCXO5Zn3|TUniOLDm{ShfSSvsk`WBd>mp6)aM zZ(sf_-t*x<#i8?O;`n&QR8A$CBBa4X9IJb?A2j)f>I zghB!&qC6&mB=OjkN_6X+SU%dv1c7_?p}js71PnBY3qt` zhrFwHhctMZ=#|cJFD&5b@+<}w1F^BK+iXKr6aIi=B5vv##!n1ifkX9K99^6?cT78C z5-6A*#|O?Fz#~)Vu(6|8OVr6npJG04+qMgLUU7ro1~{#L=8Rt4&&?wlub92>W07BE z;$RiQ?OhlV#|;MjMNLHZJUy3R!Sg0~GJ-84xOl>t<+s)=bLaN!v@#&Bui&%$pTx1T z^XR-dYl788-0=iEN6yAc2i9HLJh;oyCZ^v5qenGzvUURuV zHWA`v?N!QXekg4EzK=+W(jpcI?w#$JfB0D+o}|_nCoW=m&9J|6NY4|BneH6y$HK&v z2dfSBt1DX8mA5)L72-ZwOWT_F$E4ax^Rz>n_rpmj*$3a&?&)h&k+x&{xk`wn3y4Ht zfYp;=Gr*TgYp^>a)xVkPX*}}K!}#RKK8C*je(c)Kt_0%+NWn`|$JAGU^{b!8`yTk5 zK6CEe_H4QgfAqGW)aT7RXuE6*hLV1hO*3`lVNzhv!{*Z9CM{PIo+RdUfA;F`UaV2S zp7svCm0g27y}i0sOJhYYUsZXj@|Whxz31{>MT^u=TK-FktI9psCerkCi!7;WLT_&9 zIKet4xqZc&&Dh+#O3_)h)%ud(vC!U9i@>>)tOuYTYisYYlJr)qs7+sa#=&OVzE{e( z{idz$1kw#SZ`FEIM(1YmQy=|f934AnKr>yvS2%#K05a)1*s@MNJ4`&ZV4y%c-Amrr zO3L6AFegCXYM#ByjuypfySdYU(5?J#P(Xdd#tk@o>NM(f^`E=$Po+0CH;Yg0dmLK_ zSL51^Ti^v)Adp^1t*tFCSFX*nx+WnW>1-j>^)0&X{hJ=32TiEatjl=>V+4CTj>q{E zAujDD*9l)T-?WCd+|jBS0rUbvk{dG{NLxwCh}>k#!n*fMZ69fR!Xy&jL&82y&#a(H zI!)5od36kt5T){aJ0T)}d=WrgLYW+eJ!8(9e)e#ACFO7L{Ima#dtZ1Ki)vCrseg** zmBZs)sUl_Kcf~kG^SENqw*0~o6e)Bh>G|VNJmKpi)R)A+_<6z^Ag@=;>T^l4cpq`8E7)@IxNd`2freON0OEY zD-jTJOo4}S`meN1yNri&pB50O!*>C!Qowpo*J^xM-+Ii-x&qQOmiJ&_IyKJSK2O)@ zaL>#+JT!j+Lu$2udwZ`3>hybPX&eWOIa4oALoW}nBaV|{AHNoh^(ujb7NiVzjP)S!r z{VMHGyvGS%CTC&$^TJavpqQM|)~l{o0KLO7czjoh{`SvAw}K2US|(w5-xMld4bba# zp9hkpPfkr?_0Vbstc=$G`rJH*uG)c_lV_sh>U+gu6zMIix4JNZy|Bsx^==Tqm}=)A zws^w9JKULZc2~LCqAvWHU<;u0sr9(YL{bm7FpWK4JGFI_C>3+c|D z6kOf+#V_LHpZp}Qy8b#Xp^E@XIwsh#1kC^Fsr&KI?)?a!Qj$Bj5J$lLeYf6$ci(iY zQ8<+Ya!FhzyBt`}xutO=@4hKJyA+V*o|*V*%e$7&l5#i;@Us0pO(VBRkSkvm9je}Q zVYz)ST>oy^Cm1UEn)Kf1(u#! z6nN<93wYC&*J52C@j1e`O?_mD-`0|ABT4@!{W`PDOL_sFc8zqfh1PEc^GyrT{k1-h zV?@{@6^&k*cae^6Y@F0*!w?4jm&`Y>p*FCnKzUs@tc_s1Aj&{9t#)s6lQtW)QU*1Z zn3uNqWzxx}nHfML$&HkZrxcgm9yyaDa+8o_mh5}V?8|0x`#*ugrL#;jcsw<60l)m| z58`)}? zFt0i#kt&Z9EC7sD-ufqS{#_e)!0mLSa3fO_`0$et>GOm{JO|LjwQ4ppk){u;l|3n! zjLYbrGAdVW&!2pe!4DxxH`Hx!ZUGCA?DHLX^$oiZ{S$*H>!KE}vgK{yg6Og%9G3M-Qm^$|%btF#gYOz74oBJ?{ z5jpVVDmiJBQMy%Qe?q%#oH7IQ(|A(%Tzss4qP(U2P*uF?dsUgM^25XB!c;8?@O1&u#V4bA?-zvMQzv-3}^p?oNO$csYmTrz?oh3Zj9GFZB zi2TC&gIQswN^21U&1H%&nZIvCRYV$$>sA3=e$J7VVaYh}vH~dccfkf=E)j4~&ZNp@ z&|YB*@R`qn>A<^)5(pM{P^oH}I$SNjzts1Iw0xO#!kNUREK)jHk^qA!O1+)^^ZTCA zXUw+^4&%BFTk*`96V6B6PYYSjJTi77Nu&a#RTDkqVB_IiHGP?6Ih?QiOqS^TvTc{) z;Yapnv+$}buV(4N*SibN?L;%iD_LOK*wyFj@&3`1>UTm((zixAXHX$(v$EtzdMpv7 z;F6{9+`|@KA2y0Nra%Dr77UA!aKZgkdPUfBFQc4?JPRK#^kH^u4OXj_`=4KZCC)b% z@$CErK01C3=j#h`+j8#`9#A+d-J(^T|KG@y_>=Q5;2*EwjWf&hMr*W9#N5-}s`{C^ z{zWq_tWBO)3n#yH@B#eZiKp<7w_cB%2G(JNRwyU&_RFus52)YXv&Zpg`ya!-rw)el zBd&Va$b5Sfd`9vcjP=jVhnCqEZ2K{1;G8?${0Zj2OlPOFLNDeCf%L>)d^{#jm$r5O z_~kFdWxcDsl0R}et-$%8K6D=zmzSe9M^;-wvBm==d zcS1z@4^7g5Xb#vd2dMon?K{j8QuJtV4R?DZ^bM};jb7dEF4tECD{}9l;*`Fl7#1*-8{L;svodR~2u?j)tP-$B&A1jd zDY=N(r^>6j`Q zz4ZO+5}si%Y~yhHBB;Ey<^AJ-^;dZNkNpVVb=P~aY4sZ42KJsghW9`AW&Lz*P~T^M z?ak_U3;H|Redg(S%**I2G0eNT6nhp=G_NH*LFA7=s{m=r-y}58reF2V)5?|W#io-3 zx;g)O2Cun$72SB4od1>L&*ja9&n*&g`<$Jdu#_28K%IW1+<3OWgo~u_K(#8Tp}n2o zaqhOX>tikzbWhXc?iJ;0EZ40*6XDN!KYvyU8mL(kpjUU9{Uvcf#Se%>3nF9x-r1u>=V)P`? z)vXrT(-a()Y^Ao8X|uEw_wdAdedXP^f+D$Z8i4L5Jn3g;Hb&7PECPe)0|;jkhw%(z zngC~p?1a&qFYGJwm>#FcBpB_m^<3_SOSiGe?hE)sXn!16GGsRbB6?ensrq~uSW zJc0+0JkR(HiQBKfQ3rVD%t^#~1WJi#nU2H{68{x-lLGIO@%*mzArQ5#I4ozn<8+kt zwx>rwXJ=SSokm|*qt>-+9k0R6{VTCr3vjHS-i z`q59}-_0UWxGOyd`SxkEQ$%f3Cg~WrNE3YYv_%p!0$0Wk%&&~{9-Zwq$jj5p)yl8} z>aXuvr+;6bIg7vYKwbKBM17Xo+iYNDX%7GP_@kzu&=xukR&3w0sDRD@Yw$r5OuPME z(@_06y)c7ce*OV$>g>fU2G-)|6j)!c-YNe#t=o!MtlffXFBxU;AuB1YDY`+!*3g|Q}J*W*l zc=QF_d+=E-5IA>pvun~dR|cVp=k!@feZ8TyV~%YUwRkbhDI^oVc8u}kvWTz#3S+F* zS(2iE$wqHOXe=EY!Sb;)zU&=C1AcumqIWcJ6y8p<{szR_%w@uuxH+xXZ*P2X%%au$ zPIhy10i@G5_nw_w^c}^dIEk2Rvv5%8cG#kUx?Q9o1&NvgH*wM)22q$h=*dq}GglGP zSyXbY)!}bvsM+hAeE-x$ZkR*w0_J)*gfYY1mII_Co1bIc93xv**cRiI|2a^=#gN-vkcTz)Pk-IVst!lpFi zc}TqCwIt=I3y#ZzZ6KR)4;pIn*@iwMN0*e4x#^|yPZbx@S4O+isZgQhB_W8 z!Sd1v!IiebK9j*U8nhD$DZs!+GWfzfg!m99e5B!QT4~GR?3PbY#%J+l){)W0%w1|J_kjtk2R zJ`U#}E>8)a4e6!U`mXCfA4A!dGuv>V}Swb2M-=pAbq#q#X(mN6$^`cp@?>OXeGy1p8y09QX&SZh8f}knIElN8ht{s7 zjLJ$3eqrm?6`>IM=BkS(5zRFYV7o;my$gYyYQk5)Q`)HGmFA2*z{2u&1GCU@dSnE@ z{%?L6ANc&IFm>%VC={XN+C8`q@4e-%3ZQR830O7b{nEJ-Nqw<)S1$A=#LwNU=7}`D z98k--rFl%_OZ}~sPR>7%E2T%1d9ShzV8eV9pjFCqn#Ww-u%(0=M(@NonfZnSbaP%UvVj|Tg@^%>@2vit zN!3$RH4xec7DXt6omTWxmpU!`hCn=J1fIzfJAXy$uiw;N_aVY~J zh=8a0I(=~*Up}}G=cX>k;~e5e5&Hff1=1;vXGW)F(qTvllI~Bs{Zd97{(bT zsdaJw#=Zw|`^GDXyznw(lPl6Qv28Tu2(>PJq|2PXrO*zZo-$@c4XHTlBp5 znWOvg<ZsP%KEJ#a(Swgaly;Gh>9w%yBQTdRip*r#dT-hbApA2_XSC5@ z@Eijf!on}e$;c;`_8ozooi}ziqONtrzU}!oXT!U*+~!_!Bx7M{4}7tUXD75NR~s^9 zRHx;2nqz8u8b^*E!POhrqGNavizDY?wx{bIr%J#e%+3yB&Ub3?PM}(v?d$r81~Jr% zF{ZoH_3jx0_4YhqL9`|}VWd?gZLcjeMxVs&(rkyy;|mbl$wzCBlM&~f%Fd)1=EB5?iX_{pnYf%o3} z{lO!fJN>uLoInZq<@}{!DCf757nQMlRobZ^4oWU1{gv`l<#s8c_)=)c-BQ!6tO(;q zF0Cw2b}6*vCg<-`Y1+*2IGUx!?1crp*)T3#8h=`@G~G;lY3t|+w9_3)qF*f{|MctM zf1)z`6f-JduvE!l=S4poc3h2DYFs&5QvQ2@P7D~N-d7nam~>$dH;)XdRh z7q-2x2dl2T3Kt)HPABp6Z~i`T=7Z(~VZpiU^GNA=DfN`0BjJxTvs1V*Kclz3e=`B~ zZwNq_76^wOXO86d%Qp&uu^!f!^l`XJmQU8;LDTot@MIa+LaziET?2Z_eEkj4m^wz? zuB>UEs8m^k^*NF0Z&6nTAR_RT9Y)iE6LCu`ozu2pVWqNMiXB7%)#*9+aF!BL{zNt! z5xtkn>X;y#cfss@{&HoK(F0TSbNIr6r}Q~<4}uETWjLUbp=ZAFimNd?Gl|jZ$q;c` zRWeB|?T1O8qI%2JVO;#&f=V*kNt=*NA*GkcgfU+bH-_n6Mk&c*Tj!6!~CEry8F zP3+mkBKTy((zt18BkntQ%(tP*`VxNP!H?m-H~%EkE#dshx7B`Ol{ELm(JsG?IbaGbt28V!lUD(xcA_5IC9|}kJ-~8trNcZD*Xh` za|>Ix@>cuL5KaV(GFzTDDD+LKmeCE80boG@G46UMh=-D;C_(^!p!SJ5uCSuS9VSxBOwlT#A}?74d>6F$H8Zx!!@tD8B1r*D^~-wg*`v+77#SZ zZ6RjgfO5^r=ril~thwF7-fH`>EDTdkpqr9vX{C3eCDsLbRqdbJdyU$w7@h4?DNU;A%jmkS z(j%ZgxiqgA(pzgS_(q$*S1s-l6DD}Q3xG|>hIU~xc8x z#;uHNr>a5tCG(;Tk<;C0UdRGT)tBlZoo|4O0WD)9UsV&V2PDp^tTnbAaU73WDZR{h zszte)wP|j6^Y&$tY$t+ah*?^}Eg$DneHo7(KY~v^`zR*o=fNdlE>*{LItm0(XfzA& zTDuX)#?RY(C`D30c?pFQh|vUiL6MJ2Wb86=$JEHwpG*T_?V2_E?d-X;Nqix$+pd2F zZrpxFvmqslwXzi_^+t6?SGD}b@n<8q^NVwsTwcH%*KLpDb>pm$%Q_gh0F22LDUf!V zX5quuc~{3Wq$Sw6Zu>Hq82T!aL5i=LX9TIYX+Lao2ipx}^Wql{SUx$pZ}2=x>A@y3 z``bGE@cpZ|k!apBy_HR|KnecNLXquxuWL&46U~LNgqNKjSLW z$dvntTAlyg*~9p(`t`Q95n|9!bFTVkM)@{YA_Dbrbe*PHq;AkU5 zWm>Z^H;*e`vj zky!>H?$3%NEEIb^cv0P$XD#GF+qrN3UJK)OX#vL5I0$mQ-WR%WxlPHUdOC67#8G_U z?oaCkHVv;ue|HZm=V=ZEQqWTclDK=?PFIQ}7atGL<4^5VKWUkACKom>N1Fd!I%(O` z@VT(*{8UA^oLlOi`fXOmDjMZXl1I)@Rn|f7o7?5eTje+Pp9@pfR&(juCrQy4r>6Bb zbA6OW*>$No=9*4Vu!;90IaA9nbq=8=br%qA|M^bW97N^D;I!{hQ483)|rN!@> zS=Q&x>so&i{YX(89h012R^RG6u~AB)WlLAZ*RUOXhECh=$Zr8(a(vRs~J^T4(~IT*sMONvj=A zCSfN5bKC7Irn5@p82PAuRHf(AL7eLsy8t9vdXAK?{fgX#Q8ghYcC=OXlg~Vg;|g3$ z>C%A*Q%RSJz@TGL$&Dm3S1FKw+=KKe(dZGq^NJJsJf_11%CCu~T)|V`aIZVJZ^y{U z2xjNz%l<4eUcTcB+;ZhLI={Rvr1it&j?Vsa|A{}k#^kYyvu2yS38?)%HhxAck$d^z zh5$|_lTJH37q%(+zGrRG-FU0>vRre9&p z3n{&}cvlvQSZYj`#2~C`@51HXtFW_c07GqEn5-}2*!+}M=(beU^$vtFwYV`~G~gSu z4e#qqG6iMQCm7ch`7a8KrKCP1EJ@W|9J$6@S z=;ha-ZLmKQ6!`H@*c~0xwFDVxY~6i70BZ{zfZD$rpeqS}7%+T;31xN*0;;`kzTp}i ze*7t{UbV_9iDUiMMQiydX*22D2ODhM;+Q+`9kjArU@r`S_kBSask)|}-i~XcDl&|& z3{m+PVeQ?s#`r6@ThYy>+Mi6uK^CWQ=g>vCZ#`78<13wi^>1zg8?W7sX{C;fR+=8SOZcKvn5y`5 zc~9M%(UHejMY~+wxvYo)F0`mA}2Rubo~jBBmAZR;jT-SYgT zAdZxV(sO=E{S;82Z!D|%kgiEx)<=qea_hDDudjc*RyL<4?sW8(@Pe98>owZVQ>&(F zjt04YnbJRPGb|ob;5V^2uSXIB>fZ<T$*HDMRhcmINDM0p`g8mCBMdAFL#~2 zj?7JBTmf-=VWS?4hFUvu`_MMLVPGSko*lzy$BrwoJ|FZF%24y}Guw}K`5XgaB-6dz`LsI-D6l z_3?Fc^_pHxuxHZsXJM{pbBCjfhLKLYGvUH3@{{^pI&E<$i5Q(@ciZQ6e5zd+ioq7k zVVuxe*KxS{CeSJ3qiq|Dw6R-S3Ricw4LWJOS45{tB1^X#@cGYOdFSemr0D(0xR%L zQt{>1LP^6LSZq-Myiihf1+?4hEgByQ?ahF5-GAx5s9TU;SK!w?Nx9tHH(Y@`H(ZJJ z9X)1SJI$R6FBHn1^koq4s<%~aH?N{mjuy@6o_n|E8>vW6jiXf~!r$IBeALl23wZXF z8)w(dZ9F+EZ)bN&#uDgK()WxLFCQcD)WSTz z@ch#_Iex(t&H*5&)XB4x(t#?8z;Bw0^JkJ==alda7u|*Y5e?F(LT=OUo-Kf6_Ut{S z6Z5lk_b$BVEpPTtGgH&J`@?^wo>Q5ynT95ljM;j6>s#J}zyAE^AyaqvrR;y?yWWCd z`=OsgQ${Pvwr_j9ZHwGCPGDL|Pv3U`2lW%y-TCWRQuGhK<_B^0z#7C7?@%}N?tquC zGwUw>GY0F%y@=w{_S~-pEI}*0{e}bTGIDcZIZC5ol6t}O4WCXSaYTY4s*tsifL;?%UDK=bP7^ty8Z}-Gb19m9r2@ zxdjk_IBeY&+I~z)7+)AWi;<~|c=p^WjLlBR4OYfr%$CNxd5>%yaQ3|?#|&sAFevQV z>00t_-G!6jT6-rk4_B?hAE^*_MZ(lVJm>py&)eB=#rVyA=`pJmu55aDY{Bp?FEcT! zI2v}`EbQtdU+{8cF%YgQbN$8nHEYr`A(M%7_TIf%dgz%zJ7Eh}FksGBKd*c1n=pFh zD9$~91Y0+6!Iq61O@11tMbJBebzSFHJG7BV*iSf+|XCC3A_E>FVxrxoeMqay(YqffT5dY3JygwHOOe|JD+rsxeNQ-_&gzKAV+xjG=DA<~UG=;a zQ{gwFL(YFQw`S?2Wm4w3bEom_=qcQ~eK*$j^q~?cu9Rl(Jx9YN{T1ogB=1q2&aEm9 z?kCM}mEmz+D$SeacO`n};#kQK|K>)yElo~CE-au;NyJ;}-tx9q{iFps(nUefzdZO9 zK6~a69+*6%kJQm3qUh>CUq=U4(So?*J^I!B+uy35biJB-cDA+ZV_I~M+vx0&THh3H zZzlzmw8QP%zBPEm>MiJ267xo3E;(;#sks@W>S~PE$6*yXmrXR=SJK#)F>9ZiOQ{3( zPP^FFcK4yTy#s$A=I;ccS6!QCjB^pCZ<%A+%a3)e?68K9 z$V;sw#YRAB=_qYpQpOVQTpk@TyQBpIG;hLbPePM)T{;AspFDLG4{N3+( zJ9_*3qtItgox(jI{&*N)c+JW3a1#L#^*S)nkL#|x4qv_hexJ8g!b8?CyYWVR^k@IM zPd9BFr8-QRj?^Sf3#Ie>hablu9(ls1ji^rg)WrC~*SrI}38c#aNUK1q0U0q+l{TNe z?ws^m+gn9Z+}?wAncRhL!-PC!(vtBafsT*G;S|xhg@txuwy*cKv}>k-z&LGRzo3Bi zOtB1iuY1UU{@OUV7{T*J={!7n7JDbp;qcr=Uw!}?kdC;{*w{cT|8lvZx4M&Q>FV@t z1#vgCajM)10xQHwkz z88|&Pj*|+EUr>_xLt~?uTUrd`ARiN0aRqA|#&41IM_GEdEG(mIbkb~TuNgqC@0+Jr zYbUH77+^R4tbf6n0fh7Fu7;#@OUL-cjx_YoKkyWa$r)sg%J1g4zZoq(U4D#qR}+RR zWJfb>mz-ghUIzcJii;wS$M;+H(y!))&)<8m zev&zTepE}(39Q2{?5N-*NskoC+4`+~=V_sdVSY>FeicBdii6WC=eAP#O8|HG9odf$ zJ-b&Qt8vHFe)zgQ_>pV(teke5PL-(w>CFJsO5v(#TScE-dOVFwrDa;KT;4AQtT#(D z4cE-R8C{uMeR@XkCTeYM)j$A+KzhHurG-Fxi@vH+Yh7$z&9l8$e%zl|Ne=&}mf3X2i+Sfa-f zGgd9D@r5ott+&;*4vc1XTk3ZebgtSxnx)YSv>WK`(i-3YuJ2F(x&riQRoQ;)L?U9P z$))VE^A%QHc^H`s>1c{LcET!CF=(cXFn^P-k(9LgM2vhnyjfv#E2Wk5leAZQf8x|J zJap^@4bmOu9AGFMgK5(>AlyjU+)5~?cMLi#RAAo)V>k(%|JBnE8<5V+l`DwL5dV$~ zRtmrSW!K}CuYXUns63XdqT{)htWuzpIndP*5Rr5mK| zlYn5cTt`bsdl(D+@Rx>7+GAvI!?1QiOwVv<4|?opI!RHI#Q5s z+lB{Jt^^_|6Z4&t8|Oa1fVl^sHrv`OM$X>tn=$;V8^ic!$35S6Z6D=rmP`)B`#Z&ZsI4Y80C@{0m$B^t4U7V;LGGSv=e5ocJE}|o$CAq zIiJ4*MMJIWd*5ZAh4X!u&GjGsmF`O4nRG^HrttoU?!j}T=0*uxcv@D3?6eiNwx?eM z^;>uBuFh9#?@X?2m-6kxrr*uzeW`U*m2OqIG%w9ScdlGA(<%O@^;K2>Jl>VkqaY-$ zzBmO^?M6VIOVA0Jw<^$1<8jT7*IK!py&_Xzcc^|dYEGduw9Jn**E7fmWCrNshfV(R0l(xCJ?DOv$sDE9Q#7WQW#U@r*YxrfCTgrR)8=K9U~&D)oW zo+|#+ljHc*vyWrn*;8hv5DZd^?dwnoaIoP0%Mii=x+t$QWSZ{7+_8Ecjw(qV zok0qjL_#!~lr|-R%vQ$hzvnGz?})b^Id8p63Vt{`L!J&0HZ2)POjWa9fphYC;lhO^ zRu+!TKo9P?;bskzOOd!xu0GPSjK%8QA<8!jkfv19GWz)B*$`NSnCP6>|33CCHh1+K zkS^o;4BMMcN)oQ#-HBA^Px$zQ9~s&T%d7PEGExi|ffDdxR)+jUafOydEAeX;Am7oo zN}nyKaHC7}I5i%UqjKn0jNu8aEBB z#k?BV&McU--F{qrX)#@K3;pf2qopa1bv^eUos`F?Q_C6zt9sJle1WS?yy>U zn+p>ib{k~ z;pYic7PBBwbCN6H3=$f%dUn;P+mPL5<7g~o2x5-<3lE%~Qb7Oh=X}A&!MNXf#c)!_ zSfs}0a{HGr1W^3f-*^Ld?cVJY(U1PFZrt(uH(}G7wK#C%7^de{oh&VDMRNO(AI0mg ze>pnZ+ff2SImVy2yBwd9F!&`usl9Vc!{*|xN;k(}RHXCi=byrFJ@9#)R^th^@#$h2 z(@K`Pq-eXOhO%}=%XuYD-+Sr^&Z~vK%La$h)7FlZzsb@nM}suKX<5?za(5nQl^>oT z?k|^a66rmwy8?*#k(tpR$2fsN)SS{LFMLnF@@N;wV=b~GYU{anA9CH?h^mP*?|J1k?7*S(lngl23v>ANz9*E_{2=DlU?LN6 z;ob#8cD&@Afe6o(T4s+wOGY9Ab*5i*D3HE=Xsy0Kna(3||8WW3LzD@fspNH=H{t4+ z-H^+MHni=XB@o zprAzxux%6ITYg=fVY{`BPsZthwKomQ+q(4qZ`e5K70o6P)zxd(%fNKMxUpe|U zZ{pJUwQhVzM4G-!vm6%(-l32_-#jjUw>2kT;x8J%B0C$uB<=09Xi?x;Mq{^t2gq)| z)IS-3mUp*ppA|+Vj8^y*VO|%m%mR(^Ni00{4CE5i)-IB#GJ`L_2AykG!|%V~V

z>u4vF@(Vv?ae`%xa$Y_)iu#E&z|34yW}B~+rm9{Dpm+86VeG^y@D4~Ioq`VZ59lD~ zzH&ETnB6>@pQ?!fS{d0wc7;#c&6G7?5BBW1biRCr7~Ko3j(wCu#55A3q@~@1ZkCSo z>&8Q2?FCDdFJ!vDP~y#`%Y($8n{UPqH{5_6b57dafAYJ(PhY+E)S>6~4zr_YPvf%> z-H$iF;$|&XFNLcD4@ummXBvjvv9dRfBXv)KMly#qGqZ{>_^q$qjn626P78RmOAEND ze#=ErElO{TguPVzX;F1>eBy%kPt|ely3NhgO7oJ`QOWO0wa*2uBa+!n(s87s&HwZld_361+%+4^jNJyv;v?`S4H80 zc93>E5U%YSK}UsqZxjxQhwIYxu^YNJjWo3Dr@{|PV!qtgfbm937CSMV;^gIU^zf*uZ4dlyq3tfhzL@Yfo8@GC%w}*F&7Ef%< zRRcU{1VpCOie#Xa$za%Q0YD@JqDa%tFb6*UOkTY74J(_f=I$xT+q)f9NW@J0I?tO(A8ZG%A;{EcH|8Bt8AyyD1_A4@thsjkLROCrSE+1f&a==%o;Z zJ#T!~9t^D+j?&>$YBEb|<$vVZ38Vl#B+ter`$k^ZDGB<-#6`@{%~^Yy%}o2yDy&|& z2Cv_+-S`u!BrA*|x#DEoPBML%`=uA0_Q(6m`2Rd6y=hfCJ3HjEpJ`A>XU{N+Ox;&2) z3)48Oq`fmrQqJ#851=2(qC;39kV#&p{}=r7u)NNr{30k~>#VinY6a9^rX=cg_v=N~ zmPjdGxK2&7VM$th+n%KK?!0XU@C&a%Ot8LVqKX$uctr`6vK>{9;NcL$N8n5bP#*#H zG(LnOETqL8by;K*M#Wbz(0O4hSP4GYa;8Vv|AKvow#@yMGbEz^;}%$ijc@7T2`oN) z$lp`%rO`e<^X%Qe1q0Vy?iW*he+ej;71v4CMd&|p;Y2d#2e-7jEj)J+#l*A$==SQe zkSA7t7Z2rgodW1XgSc?$Xo(IQq^q=7ty-l$ySY=axrTnkY>S#o zuU=3*5beynaN#^AmEDB;O-`!k#5l&sCvZ`{&&;ScI6bYy)*FSMOGuGhwv1`nS86M{ zZ()GC=u~Z8wfCY1tGKOhkG&M^8P1J6Ws*)h?w;hQZr|(fPIJY7=yufFg5~yB3(S-J zrgW_W)2OJcsvRflC;$-w9Wqw&5H);jzMz9Wxd%L6v6_f%~vA+ z27;uVuf&E;(l!qF;XLptyt@pJg8Ac2jfi6oJe7>{o`P)$9#1p_Qvh5+a zE5pC1eyj3e&66W1l+^rjoVqw3>csMyjv>>nKIuyE@1c|UZIRw3fN{QHm5YOS!GbaH zwB3FC>a{pF#z1<~Kg<2vy_RW5XV3ZecAqas!vV$A9(?9meU>sCvGjkU?+u$aVOmY5 z1ki;@#xceSwhs+pyt4)W@XD7*dE@ny=A5^Ms2#D1A|i*i-`LfQX|=lj?9_RPgc%gh zoBH?U#dG+pQwQ+ob=%R`*6tJa*R1(`m^Y{8+viVOKdug4-P?9dujzSq_=OsthH)iE zepO3{-nPD}qZg}MJ2cQ8S)9e`rCEI?+MELA4OR~_L5bz|#>ZSl;ZjMx$(OW4keO); zd!Cl;!kKz_pvH4SizAnH_v7ZljcP%mQ%Tfk_3F9lPcjgVjWupQ1A+wGZ95Krz-J-v zN2c-0Oo=(Y84wGAoA-Z_2je_m8;=OnA@d_SaYDY5cyxHhh;^R5sKH@a@D6QTButd< zv);}6^*~OryU*dn_^{j_COn=0UwZ1Gl2V@wan&r9c^^~ut=-)idgZm6F_o-bwn)_Mp=$1Y$= zNpeknxwIq=pt zbN<^50R@)n{_y!Z%qXx$vOe) zLZxSJ4k@%U%EE3@H@|uW&}kmm=gJ9;tDinw-rk~qt$G`~6iMCY^2zHcSBGg`fa6$bt<`$nltuUgh?=1318cuVHd&8a*A>F z%lOk&jgxNMgnS@)pKj6zbK&hrAgTUL2X^VE67);0E0H`)#1&lF{k+y+aOp-y#9qI7lfL&nH+b{a+1z^{ob9VBzmUN*<)Kibif%=RA>V@?A6E+>;h~(>&X-OB!jx3vKG<(PZ=%shJ=dlAJyuGs! z0^W}ZmBxr7Sr3qY+zk0i&c@rm?DOZ-W_fVsu;9E#5){EkNH&-H{$?64Qif4LWCn5( zPxQP?iLhuSl~j7+iTy@XJL?n9pHI`;uY1EDwD$GD&nwZ~5N?=}((T5OjO=_|4WR;4 z2}F#HwO&4R9`%v)bfty@=nJ7N>@0OOPuek|cgA6{bK7G^h zTI}9>S=?3~ZBuZV@)|79(KAgy2O6s0)3}5vm4o}GGv>eXmCxeIGbi*J^81b+&`Kni zlyvs&*|SS`slSsB`(bfbbX4HfTVtI+MFO$9wRaY3I?Cut%9^@t{%4)$H-%9f)| zjviI+slTLsXT#>=OzX2MTfp?n+;`D1L&FS{pYVDn0 z{;q#5R)JefO-r!n=jW9qeiC)H7)fBB;;n3F>HA7FV`cL7L<<3=yp0wzN%?7e8;TD5 zQ=q%iZk~1Z-JqY6kT*0KR|^gL*OI4s3Y>HKYF6L5_LBAoXXpIq(zw)cnf_0T;=bP3 zuU!&A=jY6`w9CA46Qs;Jcj2FYB4pNXfOb}4kVcrFbJ4o`;uD;Vl5+{L2{42;k0uKa zLd406mBF{0*1y#h$x)ueIdfJ&mZn}h0zgEprTJUlCBZ+1w-%+Rh#8)!0?V<9^d$kt zRUJ5~_7zQ(3pUB5O)HmHZ1v*XL9R)YnO>O3XP$ck51lxuB+|<^Ym&8P%arl?2LXg2 zaR22*IYXEDknAaBEUk3$8ZEn=p$(qe^mcb8Ngr1ry_G%|@=w=FxjW7D>6*vcJND9m)JiCSi8p z!j5eYmD*enDo{>WqHR>byj|s$u0f+S<);>B^wlhjcALGl`HtdcMfQsU?wRRJe~mHO+`iu?xZc5Onba6WwOcWop?8q)t2f;a!#7V8Pns=pi3~O zPZOY4Vs>EN1Pf;f=6D1V@d&Pek>>oR!zZzH;0R4xKA!Ls>}Tu-cJD;j=Jom}07v)e zNQZ!@vmvhx?bf%Bgn^3)6o8}4Kh>AkCa>~q0Qzi9hp@tv4iZ?kX`_}BC*$Lh?*a#?z-zP^!D{4%M17_lisdF0nMAQz7~IZ z?_cZKAKCvbe)LUmEpK&q?`eIk1dbT3OY!9Fd7KrXMx^0DcbT8U`ycuu?mqOa5wIxG z1hvQ`mg}BQw6knUZ*32C6fYYKRx!Z`{a;q?VUZ-h%AEk|-qT0*w)b0i>{1KyZ6V(- zZ#nRo(koLR$bq`tf(Ey*s_QhZT)tBO&El}Y{M?x{7#|zc-wtrMSy0~2?;E#v4z!cK z0pbR*8*px<>m+4wWk9{9W`KIDy$X#a*YmTpdMmuUE-hQ;X64D1JM~jV(`M0 z8YY*1v$S*dm&W7#R(dP5DX7iFATzZcXWFLb|Cf zVpc>ZRtdiyS5F!uOpjH0kYEnJ;(%zvOYHqNWSKD2Nw`vxIIVJnhBY+tFUy7=$XdkT z?Hzhw4?|m6>h_$uJNM-cpMjG~2;_=2^~;nybsVCvd}e%{Y=X!bo;*)8Y;!}t!?H=- zF|NGsWv)|8nFUoM$#O)FOa~~nu_XOwhBrmhBKh#~!+3J!SR9V=caA~Q)uI8KK0z=dZ8h3dHM3E zZEf4mov1WIM0G-7{?RXe1(l;lLVM|%O-45$UIT)N+@2gegXyJ(pv~Pg!Qnj}hN847 z&z6Wjj2lyu35)I({rwxh8~@<3PZ=P?+?11Q8Gd#{e`uEmA5O@!pG%(LpVM=iDxd3 z;+ctaE(0;h_8Dhwfz2PD))rj5ZYO0pFYW@$s9e0yxOr9hzRBY-C9sl58PfHB{1hfb zz9sL+Zrm23KGgr#$ys!aO&hwfaW6Ds+pb+W78bermX9$t*DtsS_&hZ(PGfQ35lFQN zXX4@vNT@e%26pX0-&H%{7Bd4!f++6L9+@2wjfuXV`5XsUHwa4imd}styN3f z^Y!i*6#N!cFc}mi2024t}*NV@X@1s3+(zeYqhkKww>0rRGhY{(lux`Bh}Vg zn`jJV{!{v%bRV`Ev!5C^#%IAuGl0XxR=JBD(%HDd60t`V*jPh!Z;@JsLXDkt7@eKM?9vi? z6mW_1%gdSLl~*b!_rv4hb7Zr6kuo1&4mjrWoJpH@iO@E7I!8|Th_|Z+90JNjW48sJ z05!Eqx>x+n{0t6W9MkjSh>hmbj3qk} zQ0M!eMJ6iAzY-<}afJ&OjcqxIqR@`jozRCDd2lR5yd^b?hcnqiMBDj7f#fB3`jx;r zH&32Ej)zYk#)7>Q+hrsICzj5Xd{M|~@{Z@&a1V9>1kA6aY0}}NZSD-L6g+ZPE(a)V z1U_Dx#BaOmdVE@ebUFhUG1VoKh$w^WltihDYMl8az|gB0kRCO0xoz$y+sQU#!H*y) zsr&LWI=i~u5M}9R+n!+KoMosF3lD@b3gGz0g9`c>S(w4EJoi;RqJa9(DxkixvlkA;nr3d&FTenq ztvl8v^cR6~(sbUXf328PV;Y?yKeaT2C40nz-#u=^mgA90TahfiD4eB}`!U=S=h3!V zAHR3Abs$)pzD|=f=*jhM($=x$d6evS19DSE-qO>LEsC*v^_nfXfAlz>RY0BhCHM5> zj*I~`BMdxTWY)o+0Bd08SWk?SLK3&5Pm$@L!2>?M5I6mARfZ zt1)@%42#{>h5L+Cf8p>U9p&)qVa>1;01u$s%!OJ{09^}Mn6epgZnlWiX>lc|)AL8I z#hh3t-i7**xkgZEZOtSN*M3ykhBN@1&aX--J$MkX_npgd!r9SSgBKw1RS4zrV;LLY zN=C2+r)YjRX}smlZwVt9XV&?ZMnTg&$_jxWmVq|U01bLy{Ij!@7VM&PIz3+ z@{_pJdGgc8Pw4jDra+p$KV1E2J39TS&(OU1u@n37*!Tz@yD*}6*)&G)G&X=!WVJCV!*aF}n#BY^DMOM-*@vMk2A$U7C$?`#7#Ak;? zzM5=<&zOjwmihQ*I-gFZBVbNi7CjT`K=~zvkG9JBDVUX-PoB=&)=PQ3wC%&q&OVUL z<~wbx*|y6T7<}1SNAz~N!8_-Mh)s&?FzwKp`h77q?#f8_ng7)@kKp<9XY|nnS03X> zM&B-+AA`Wlp2W|~%BSTxGL|{!-u-yW>M-&t0S@+zbX1^AO>Uc44dKYxsAl-Q z5gAI@zy!`Ozvk+w47?5sRtL^);=*}MjGjl{guJ?Dw`y>ep73U-rpllPu3F6*^p4eo z+V0TQ1>CXiYP2iRDl)NiibP``5cM}dtR)jes&`?RlHSq^`-2l_%s458kK2L#sC}x( z3p3;RvI3A4$29|MLj56bGrV5uy+gEMprswdtz8&sHFx0Bnex+0Qhre_PLNcfA;U~! z-k8MEKFf;gl6K45i39njJ<7=5wWl~PyVUFFR-PBr7Tmq)anG@DGUVHZ^JnsX#9wODA>_0bc$SphQcqrw(egs5EC)9=Fj(1 zSdg=i$2Vs$Lj!OHR1vhP=dQ&T?3j^S+jTuXYC+j#jG6;W-cQYP0A7nP!sPG3qcGdz zJ(y$#qchxekGMZ;E_?*xU5F%43gm8GkHPD9=}&(q+JST+0*O+Fp2!zwq8%$TrnTE` zCLQ3mG^E%1`HNV3{+Oo^Na9b@bQecW1X}xv&6Y*kZ@7LpPCxOiUYPLyYRSA)6H#4_ zO^l;kG3vcNJqVKKny**C2v3^brKuBhA9`qqZo!KkGBDw69closrRs%OZWW(3N5U}s z&S}8Ba7Bq1aRk`nh^1@IW-Kh;gkzMonjPzFVca8szw_I_jqd(FWSRD6Ot_$}?rr+o zwY>+P)z3YretBJbZ5MPyJ?$NYXClG0l7WZo|I!?vH3*j?A2FV4n68*sHRP zz@dPuu4te?gC1qIHJ>xHGZ_BOwj zy+Z+MdOkIA7XSY7d-1*}zJv$Q9>+1oyJ>pTeWA2Vf&C~BI>+2fisMw=4LbV5{iu3d zT3W)$^hG>y<|zL6fyZ&*=|h@t+BPtxCAf5)gBHqK>`@lVAJuhxdwaetRqZP0H?5~M zj$9q2@uc-#m5*F~rEMtZm$$1l@45Fhj;eOapGjHT)FR~8{y|Oqueb3MbbhaDL|S2n zHzaK}d81pXiT2XwAMPBM^zm~L zRJkR&$pCDVVEm9oU2ivo?wsEUpiX&l+u6BYx(HE{p!0%6@i|N5!O#$)J#k~L%Kglq zE0hA&?JPv$^UU5e$wcJ{j!h;4o?z9ann^Q~WQIb;VH^V?%SS6xkUhP)fO}qe#%gA7 znjMzi7#M-&$4g43geU=Q)>8DJR#Nn=5u&u%5h9j89!^K!j?y*+L)#zSv|Bs<*Tawc{^QzlDIH?q z6_apgaTdSz!o&DWwYvW^J8r=38+Jw=MY6i;P-D~Dwl2*a&nw1nvayJ%Vo8k|PKqoO zE$GRDuMBy0-wq51P4E`ja5}TGMzS^#NrYKmVcVHInjoXFAzw)RczenRlSsx*yquR% z-jD|m<`r%F6|;8R#>+9aynvH4lX&?22~007A|2&*bOozx-+w%@SQiMHb5s!iFTyOP zGbNKm810>((2#gxxo2=fisc5{re@JKIgfgKH#&Rz@m=dKQ)OJl@wrKiEzM(2ElLO% zjc~N$pjP_${Q0#gqqgl@H_6INfPC@rDag5b>(wz)NjWpqP2Y~qSatOkY%Yk$z%Xuw zcBs!-Bd+6Izw%>E#r#OG^N;U0{m)6w9Ua_RgTiiUZ?T0g*jakzw_~6W^@YWdc}SNr z1Tc@7p343CLkG2=;nk~+vy_^tY>2Q+L(rLWl9F4Ht0i-y&`PPu54}4Mp&<=lrXtXb z6(pTETj*>4iZ#bW>2tEmYYX5d>Cx^YOH-~~&d>is=rM;iBj2~&7%A9TSs?5>B12(+ z@QypNSxJWJ=8IORs4C?FlKD9WCVO^Y3;dNy`-_i0Y`&WTk~E$i2uO{TY>aVz88QEm zxR;nB`%PD#(H3_yJ!(<4UD177EfUsg;i6$coS=GDl5|qyrQ1ShyFNnEIz6wzK`U0P zw$eE}tMwUp1#-6C_jR?SzunyZ+~{h<-0FUPL;;PWIW0Kvz~-pPJy#D^@ualKh0D>7 z*~yjsXYA?p2jbo9l`JQ_TwL3do$j7 z&CT{sawWl6zeWq`6e>(}{uHD_9lBq6k)6dd>g5#{#EP0rp8yzxvC-otcV4qO7_8R+eS zGfO5g9YBo85cvy(UX+MuT=dGT43lEQ_ONnGR#xPJOUB3wJ6iFifwINkQTq2`95ClA z%Usfxdq$bg8beIs-vK98s4}H7QHHS2Vg<66XvX=q6@oJUree{hl$h{s!-oYBW#2tB z1Bh>;jJCP77wdI@&fJ8PnOGP@DcOYXd z*0W_EX$#vx_v%VNu;Z9qu7@_R{(tb;Gx(k@S7JkVe>5Hlmak~sP4XfWf08p4Nqcf3 ze?)=w9wpuW4=;Qz6knLFqf9?9>g&{suVTW)%)GWcvp9qQuo^a~j;8@N0aVClPXc208rv9Q(F-`0t%2RpEPa4q&toWuT! zQH;z^!b#+v)^h#M#{^zZmK1ymWkkm)Ttq7w(@e&8j&T&eUswVa@b4{3(%XJvN<9~K z8>HTOsxgJr^V8^TZNoa%cj+Ad^{wqVw=j!ys$HF&n|9pU%m|kS8fm9S@~UEy4@WnDl19memOlExa7Jh{6eeCkNu`? z_>vYLTNy(~5_H!=UB50^!)Hus&)Q+kPuk--5uuu1W+7Fh>xHx&J9y$DiVPMsTUbq z{O$y|#l7H^^Zikl=qJ^9>d3xKk>l&@uowm6@tR(5#a0&49rWxSO_>1_6s<;@x)(rd?s(lHP?yCA55_E$` z9kcZ%O$!3(Byp|jr}`1NQ{VNT4zwysdh5cnX)o$WHv>!&;8#hkB0$;Mt>a%B=*Ij| zFEtti&ehY6Up-u_buPcDed?B$sfwn#c+&6G>B1-7CPQG4Lk1&r1Labo61SZ`MU$; zVe6M*%mJdwH#ILt+MT0_&m?lFV-s)uUT$x~rw@oyy~xC8uI0t)mj{`2r$AHV7BhenGh7)u8AwuErr%GEoWN)H zKZ#S5W2UV#EsYUxCYkh)<;5}ToHmX*f5#0sNUee3M}4Bo7yr#S{BZ8u)x?fj&SNoHd7 z$9wFL=y+xC5MDpL757{?uFrw{;f%%I=LIJH_ zQheeTP81_S;C$I`eRXZa{3Ox^9@qYy@gUb1BZRS|PQZ*Tej8!bzC3WZMT+lYm(lil zY3tR4a-X0K?kqQDATHx_%5uMOZUql3UEbj1carsC#oS)CYAvoBT%+E5bX%EOT3~rY z;O%T)1)CS$BA8(5@Pr*hVUlCL-_l6P5Ktg~52ZB~r9t`l47yH_p>1+jwU==G#J4|X zXnXrbRfc1;Btf6kM=rJ~MscTVN2?U0zN{!QXRlLqN4hllsJXU`py^#C?R9z&yu;iVDV#(~R$eYH`;j zsWFN)X3spn&+oMHeR_OKt<2ZsZRu7aoA29pm8WmRTGU!=xOnUoN`Qxc4L+*y3>tgd zvV4AQ41=o%^|ozahb}4^+FMNv37F&>VMn1>*j8A6$)<10U4vT?qwugioi0KV7R4aL z(LUE6TJ-KG$M?Y2VHjIoJJkyZh{tu`?>sp|oBE}H^GkTu%{SxVxe>h((%aQtrjN+9 zCr&&2{p6t+aCGFf-re=H-}jDqJg%63s^aqBxiHP*$%V<7&piK>zCumX@rk8nGq)*V zLEF(g)nW9I?H-)>sYUgE_3Wi@HE#B{DLNBa zSA0-sTbq}t5B0CY)-78!?=pVwyor~7b(7At0`4DE>EF6zH`etHX#7yKy@$5Pw|8}} zTy9?0oJsqNGwFEL%p~y(UiZ!Fn5WO{+u8GH8b0@4Ri=t^(U`WTyH`og2XuY>U7J>u zzMY%}-^z_JY1@IjYy_%@ie?ogGVe6tN;VkBiMlUgIE)ymATUX(F%yE!C++JyGZ2_W z>>MFVH1c*yBWos+%aJilTAL52sEy84eLXe4Q+joG@36LzNWl%?7VK#++?MmQ|56bc zXz{xoti8#D%r^s+`<+Y%c+ap}kDiZVo!IjM?wRZOY*3Jh)4C*p9x=74QzkuT$0c0I zggreufqM=kn;6};fvd0jhUX_6#;&(_Ov|Ka^86G^$R8AdxPK#|o) zO=Ma}(ny~Nl_cui$y1C*A>%R7H0QW#{@Rkbw)x1@m|yVj($efJ=Z@ovu@T%jv?0_t zYqPGgi`1U=&7|07Tc>SH;aoIaC(xd*=vjpiyzF+o>*;&6bUfGAw8rK37Az?yt2Voc z*4YKWJ@K#49nrtHuHS)oZo6Jf)P3IB_#32)NZN+msvg>$qhBP!M%rf0&ke0Sya&9j zj@)9ViP{A~DQyQ&q@aNEv6x(U^gAUBVEI+4F@8S&D-cNVQJQqqeYw*2y`q zIo+ zOWUpAO5_-53+OYW<~+8UpJ=LlT|e1ki- z;KKgHq3o`a8qsk$zs_sK4GxyBBprWd-*dR(>T9&}OY&l*;hF$2$5)+_I3xWogx#ht z8@lf+xt!k0yG*-=$WO!EOheo9iNZvWx&nW+HNM_33le&pJkhAIWd)m;lJS7C45kkV zGcVOdI5O!Z=8nIp3D%bEg2sE^{T{sI&O0%u`qIhq3)o>79HaJ=nKuIYWW>1lcbjd( zQwj)8&CY6xdgQMP7{&}u?ADCe$oZ}E&(q-6g#mB0!;8+E*ZO*NT?{BN*j8W0v;y%B zVcP&{8n;@<^}X731wq9`RG^h$!8x!)(k38zHzm=rGY@R3e(l1 zz_T|!o#!x17uc6=8HDPOb;YB!I*^{yBGvCPo73 zf1Adk<5GiXLl)&hkH5=!0m1{}stV@S#?tbu-<}F~D zTbnceU%-PG>uuhi@1B^O^QDmBRUv(q@HUz*EO>VYOijCi=L@yH{qXL-UZuZ^O@M{5 zdDuC;Ui*LG$bnMXd;9v!*9#@Icjvr)-PVkRXK)Px^wTVFK&cTsx~MULCP6xfzNnra z>?A=NYVxcrAko_2%cueTr>DP+KYi_;dh&H`KscJRy7$s3OQTYIWX!(%cKL{CXKA^< zqX+N5?#=j}qmSV$V<#E;gxQX0nR#O1>K(0U)Vk5Ks6g5LlD|slug@OQzZ(YD;=f+| zM$L?Hpy^VR(S>@f9hd?{WsN|vBLffTsmCFE@b8FTl$e4brDuipeMCn2l771_qMS$3 zMSpoKiN-U*`id=!*(u`29z+e>^|QjhxIAeYpIVo6y3TQg=NO0GuZ1W zih_-EG8${ixb54r2z_GV@Chs)AMv#JywJiDv#W)8ifA_R5;^*(C5>kZ#wXCIz@ES>{g)vEhdn^5ykUq$v9Y84ujff%{D%x z#oa8Hyyrdd!7tu*m-efr=c&noGLf{Qgm2iiX05M_>3Iv#qc#U*K}j?IqZ#OE29R9% zG)!vzSt6;yb7OoGHM#@6yHjt`r5}woQ|i~!V(PeE^}`+o99!L3+pgYJowcaBfx!9f z=G7>)9uo`98x0TC+vbfVK0R+4CHo2?4` zI!*0#*ZU`)-fMO$q-Nt9^&1Y~tu^hxL*Ia6Z$VLU5HRmilJo9%1JFAN_}9$UY{awE zZU>{I%JiFhZ!d0q^{epo*B-{)#Yy;aSeaAj&*68z^cno}_xvE*tgSvGWUtT5(x!@b zxwe&~&q{4|CHK@XPYZ4+6Z!s9)&DMKfnkOIKhV*MVFk`x{~jdfsc8*W%9uuktxBm0 zXRYB^DOWbUQ^Z@QkU*Pg%X}%0GB|MaT=|4LczZP~^|_oy4Z(i9h?CTj#%;q#TY_2T za@xvV93c}d-Y}T)Nr}Rn^XWosdK9=6x(7oYoX(9p>1a&#a@wwJaW+F)BhV@{MS^EV zD{~s6ml1(HQ|a3pQ#L43X=Dmmkd(8kle)TMj2e$~B~xx2vOFOr0|noqKq=5U^Svhy z=c9C|x+1dO19H7N30XF+6wnq|Xmv7QZ>&Bug)9 z&ob_9k*qMGU(mNDqq0XpFABr*tjx~gb@~}Vx?$SZuV1HDg&T&Wvsz2z>yKo)4Olk< z=#IJLSuc%8w%yT{B?Ci47@a%KO5&D`^tLqAwODpvt z$yi-6Vs*ucwJk2AMQ_bBF+VkV9&dW!FR@O=_Y+&M#hcb$W>XB3a$lF=Z3mvgcIVEy zKjS4x)LrNzQx}#LwqNkD#-fPYBYc~cK0V1&MB*0+sY@@Jliastu-vwPpC6Z=3=(>- zK|bWi1aQ)IE`8_Vnw~w1QR`|O#OD4XpB4dh0`25?MoGSAmlibRM)ov@EH~o!B=l*r+zN#^UMl_DL0popfFaVMdd-Xwgh_)W*Kc#<$#5htZxNy zOB>uDR_{mJ7jWn0yYSt;!&uYNg^!FJz=w`Mhw&4qapBNW6l&o>WnOjEK17|E^@*ra zL{IZ){f^79^}4IIy*tL`#vZp&SCd8&n%m}U%EfO95*aTmb@v(&dzCQfMZ~QuLd-H8N zzjXGTq~~Nh+cS3+vw1mcOBH-sG%Pja57IW8@&hXw4jOnk9=-|4TGQKamBcB^)a#MA zVn!`|(ax}5B@t9Wy^pr8D^PfGVNOeRNt&ypQ($s&{SX$12hed&2@S+7=GPAfWpECd zQ`=EFLXtg_oy_Mwbfqc(|3oHxHZoO=(zVRJgiYh^NITI^*4{Ish$N zKYr>6M$V5qPw)+7o8nzItd7f~+CA+|pflzjDjwRt?tnUhay?Fy6kYx3tTVM~+VQ$r zTlQB%lAvv|sH0DT^s8QbGxp#6RV++QMQMNdp|9ZIzwJj=H?(__$$ELYOqZ&6B(% zc@mjCQ0JuciX=^Uj8`*5tVZ46W!WTMmv+(-;vEGnV@=w7@D`YNzx%;v!1D_{PL>Um zcE+)ka1^tRmn|e9lihRx7oo5&uItM4;k4h%5Tg#s5a z*n3_t0Bk^$zsZ*`xlPtB%9wMM(i3A=F?yca3;|6T7hI-bCV-QWHyHzZmIu#6!u4H4 z7q@ue8C?;!(DVN6deROB(rK~cYe!$O{(<3DL;75&TFO__H)*z{)bVp6@bPEyL))$e2iPun z)fnRP#N%`H5td7pe$3x`|Mw(n`hMXc? zY^$M@m@W|)mK1;emlqz!pQ*B3Kd=Trrs5@l6;ciD*V&}qhRuO~dY!2I#aTpkA^i~nvTqq$1-^g~^hQXpD6Nq30!#^1TxAK{t|aTj9K^e_)3)NtuRgA~fQNkA z0+8S7`7n7jz~&kT*6rA|YUehbf9@cBLvtV=ESwv-6L4?RLrA|crKcMh_8mB&B7OlnLE+l*7FtxG#uEj={wMMi!_SpjLJCUpCYz^ zyI?((=vi}Z$I+VcnSAc!M-$yb@IU%TKZU#Aa~JyiO&_7H?N6OL>irP-;e2aEyYxM^ zQ{ks)XW-goUsq41zX*&pOQQ-@ByP{0JB7U?$FP59Tq}6%QJ`VCy&JC_-lWePH{&%l zE&XvaH%=;3ETb_oi9!MOmVrJEHix@=eXyl0#Sb#+s+}&(~lpyZVztVzN?(4v}~!Hqn(s_zAdF4^Y86M6K1liepiZHgn?SxMW+^f zof-Y~4h}6n7h!%-iyGLx4uRk{>YCx*Y1=eC=`1q=aeC6u1p?`vR+>)d&-M9n-N%el z#-&zTkZsXdxDv=G-2j_k^$Hxj=YG`ntrjM+sijH9<1MI#-cHpqDc`ii#VKq|XvWK* z(jui5uaDF|jW-uA7Z1-vj^9qhSJh`4S4A0`_5U_CHj$FOZzM2x#+*S%%yUGHst9u< z1WL@7F%o&%CCcCSGfq?ZSj?Ak6|7YUf||imMyd0>CyX=q#OuLxQ}7dHi9u!)fU#ov z5f##8?k1QN%tsp((KKyD6=}I|EfJ^5RG8JVT21gHnD9~z&z$9Jq*R?EmfV+}SKjh% z(kuJnM-*yc+uj`+SSI7KgC=Vc9MjrQ=McnC+b;{bTKwsQNqmAJ+F`zienjDV%Gw zylqOvEnav(@bqL6ARSSiVk>vVWPN8Z{^W+Y;!jUKhYycDukG5@7}7esh$RKcmpo%^ zBqDBmsgGX>-#5-L&EcNWWBPZU0_wM}*@ic2se0%O;NzoUTC!Q0q_yMiS|)8=`t}oO z<-$qneZ>9|kA*%RMmt#%r_L*1Ktc*qU3h87%fkDOYc<5}a#rwuAYwNjUb2!%8R%rz z5JUtTx&DR%X|ItUJW3!+AKae|1;`p(7wdO!$JVPZ*Wj>_L0fo4XM`T_wzF#tA_I`O z8{64jB7w9G@arRGLKB!#V(Rhz3IHu4>bS7dZ&G$gJAn9b7%?xbCRN3)-hBlQfBFmH zV8J95<{+0cYAkq-?S^y*@7Y;(y2k10=bpuNN|N58q()A8X3d~;vz43{K4Cyb(J+#B z0`o+_mKNFwUDx!`xSiUgJ-bd%4f5Z!_Q-$bUD~|LrTtcJ?5DBbe_+Ei2cWp>UTPe-k)Au z#r$RC<8ku3<0i?|eMk1=zEg+r+`>gI>DG0kq}pSu;3vf#o)|rVkDlCzUwP$QG%(J^ zp%-hc9)NO|NY8`fKh&=_*pHU}9uysInA^Dljjj%T?VeLW78Jp`Z>bfG_KeS>roOus zp!~jFH{ge_yBU>qsRGX_FiUx`m4Kmp&(&!T^yR>PYSKjn`=lKp`j#aj?7iax*KFGA z74hl#6zy>N7DwWU`F2VUsV})7>1?2QwN%~$b=v0M)6uDU{Q0FNEj3@Nw`w4-7Y2ms zk0fJn@9oB_o36!$FFfG0KcE&{277wVju!>a^)-mK1{!HSq;yz`cdeoqk8346Zl9~S zT)Nzj=Q+(gkE_COuKyFA2bHjaFH(J@O>3B{g5j&eQQ0hG3Pul^w!RQDV;>T7YZwAy zA~%@9g_Tj*;HSEAaEUD|jBNx}p+Y|0_I5oTBtVr97ApZ%S=DIVyU%bkKt+WK>2fJx z3Ld@Gq-YMDb2SsyJ%6Bc7csMEA>A(Y=S;@QM`-9#>~&1C4G z)RFO4&T~$p9`RCAmRfsdE!Z?K%uL~{N1xY<;5?IG^2(?s1LTMqLCNSP1H&Jk%a)~W z#jYpTDu-@_&Atuh5Swyc83E43t@?t>qh zozhBAH1nyX=Ng!kC(RhDI%zS3u&9;$m9y#T>(c-o5RetM=IWiUT3eW(*HXFZvGK%r zIABw#Nt&eSJzF>W{>sbr-pB93|L?VTXt3<^Q^`~?CS0)mM`ICOSVt%%z08lW`bp>D z-+kGQcrC~hZi@lZqqk@W2`?rUJnKGED1#U0vh#N%P3FPKAA(Qj#;FV_MFYJ+^;BP_2{mgUES&-hY z&!JQQcW1@TnnJ@4O(Zd-G1io;a1wGVdd(aWprZp)Z(%2Tws?*21ZE0?@b)@cx}Qzg zgOVE3!T=xJ^v!d0*Lh8ZIn23PWqf;X-lNZ%@45NrFqF`ODg)=P1@?7zYl&nfI7`Ri zi1{^c2TmN-&t2QLqmrlK@nzzO+_*jS``qYh{LbTF#yLq>q_t{V7t70FJJ#QtaacfEpKX<@|#ueY1mx6QK8E5XTwH}X2smg!!v)hu&;$; z!d^|Mo`b5~w_J7=9(?{;`hFk<1JzJiBSuOTaMc zEZlEtk-`!@+Fb72kIf%nScyvuZgRlLficOM8P+mmTIb?K81&QfrQdF_lQNpD(lF@` zh7>sQTvnMyHM#9QaY$dC#^Wq4{-lx7cafnR>yDi9l(8=zUKV8<1v?JoWbDVj7~V6m z`r*93%OEd9!D3)!qnJApqU#N5cB|cWqzT*4vrzf9N zzh|^Pt;Bb!HbLyid?BNWTSz4HfgO)(VcY#|qmKJ7oWOnOPhd?)w+88NShE$o)zhC* zmNIG;ZcDMNT^ZH3p_&KSt~rRXxa8uD?y$C03*!i{S>d;QyRF%-o?QIV!V$}}6zqJq z?WZnGAX^i!%yOZf1ve^H+*qUOQUb4u#cd?#h+eGn;aL^xs+=a>Ct zsF&1@FC2NGN`KXuH$OeAq~+%@d461jbU(*A>A0Q${nOGXWI`gxDAPAxU$$ZUHf*?T z8(P|0;U(a%eMIdfETHfOAy@aLq1?B{B9t`ddu+)i%O;x(T$`JH{ut)e&pUvX%e~Cn z?WjJW@PIs&*~GVT&yBdS|Da8q+!7r@u1*C|0)IsqI|J>k=vmr#WxuefK>9wCq+g5n z_BI{9QPjL*H$AK(oX^+47O4Tev>NY=8lCH}`Ek~yYWlDS2?S_p!GjboQ9aRdz19{Z zCAV8|3)X%(cyl{DNUGk`+lycRrC-LKcixF~>^yq@oK_0AtgrM74Qb8507}d{5_Q}67a~qHv=&V&qcqZ6BqD5pZqGGym(GufvGjB(=je#cQG(v zZk|Sh3ng9u=!L`h(ie~57p}bp*R0#Dw@7a2Zs0bLi3g4tVl?7}~K`Rh!28MRT6HbuI9rd$4uwdi<+9|AC$I?F|z!c`udS_ATBHvvh0LhI631 z8Ev`UrP^oeA`R!qi>mx2`K2Q-8+-e|AqwCw#dt05A^#@heB$B8IH!bjGlTb)v6DbF z>k1#AbGJ5-nRi3$uthpb$xXPKEcq1E?7$37!UOU!A#$&t!Ilohhj1ylxYX_6XhIbh zVQ`aWX*TabRq2W}Z!B)Ed@gFdA~4)8tZ`!LOEfKWygn!(E9$;PZ1DCV5EnIs_tQuc z4bn0rZ4*rfe<8faZ&3lvoj<(DIwtdoRseSb3-#p?Z^G0{Ru~y7KQKBjY0K0}mPeUR zx0WWZ?R)8v40T@EgnUR#XDJhVA77A~y13m0!K%W)N!Csv6M!ZeW8mVZ*Gy+9fq3sMl(2;5VAwVQWX(K$Q_SEw3mH)j`S@!luy!GGIxyS`q9D|{2B93kpGtXY61 zoE`j}LK#kHaAcv(Pi?sxZ(X}h1NG039y3~+RT$FX>wt^t4kk`lhb9ijv^aYkFKvPU zvI6U0K7Sm;7OcN^n85mKlNG^tSs;3bxLx}SZ4D@QKUoTGLAYRw5^Yukh!6CLfG;>` zN2Y&3d^T;~#%$dgCe8Jka({OH-)-CWDF{}ch#qnOga!ElwDWes zfO=w=y7PAEau@X%(9zU7E}kFLJ56RMr_dnX8mw)&{ufWsKvSm7xl5b6dS5lP8kb*x zjlOEp=R+o-+!vt;^=}9sD{_;CovR8f?bXCB-FOlfC0YBB5j8RU!g0*KaLkH;g>Qdh zP*-Nc-hED1`ZSIR;2bjaRr*x+q7Y7Fh`tegg`BV5uKlM}S@9+2Rg3=qCox#(mj_U2e zelA1cB{vKX^NK`iA%l-=X}eSAV_&`3)9Ot(-HbB*6W*ZeI~TvQ|J3tO;eRM_UT-xw z64dH-JsuY#0CHI?7&tf2g1w(t82MM9ycf4`xE!;~i=kmyP44d4lW&QvpZi+}$bx{m z2jon8uHHK)=g>KAt_%F(>-OMBu1jctct&?p3^vUAhiOd zcdhEj;`l|wYuMkG;WhZHfAb%4=O_OdCnJVP!mn~!{;IAP1M84_e5ui|1xm}IwD!tY}fuwfWzcfJHNnMzVl{YV5LW(o=i#X`!1X^ zmmn<+Wp#15OjQif)?G42CIw8!pQ{Xkw7O*S9)%I1#mU57uy&tH%U4=-X;#OlC&%%{ z7oNk^(n6_y_=%CdxvY9{9;`#L?H@12V}2ZOSCU4{kgm3s<#V;sWX(HXA^msZn0?5k z9XJL!WWq7RW%?!M)l4Xu7Wz2(Gn?HZ&#FsgeX>hGkzb?odpyUl%qxy5nN7fI1O*9>uzNA2zK%mnUwpS1XazYWj}Py6}Wi#1frQO zG$F7KZsPF@*uWYwi&!{2K1PJJwIrRcV7pdmT&Vh!#o;NT2BkHgxXzHlC2eC%OuwP|=Q_UyU_S;OOF zJV(i$8M=w#cOJS2UpV=Kk&xF5yC7n>vpYbZfOT_Zfct4N-KVo1pBXz2IWuGPQs|v5 zt{b`hMSYw;md!bJ;kL0Cc6&R!<>Idb z;<bf6#|1WU_y{4o;nEvwhylmFl@{O0}#@fXLQfj_?KCl*r|btyHtURKW~ z(#THWyrrRU<5;NI?6D4sR@IlNuBo38a3_#XccCxRkvkQJ!1|&E_R9+Nzh?C&{EvV0 z)7a8Ipm{Pg{Ft^#N7ss#+GCEcDGjUqqk?YSE*D2Ld+sJ>ybZr9OxhONHz|Olt3LZX zy6~-FT1z$-5P7o>9U;gj<7V)$h2 z^Sojf?ziX6{lLN}#iSj@K-w{b(x>4V50{?1rYU03fP}FQak~Bxr08kd0fc*CP5%m? zslW^)t_zt|f?McH>nkVeFH)LH#C~LGwT?El9f_`>qSgFObmmQi^eN5FX$eEkNYS}Zr3Abrp0P8RnMW})lvS^aU#=oaq--FY`kIz4vak-LNU6z`ky_18Ut5fj+UNI z-$;ar%XescT!D1j?*2XIj`U2T!r7GuGvRsc^vDPA<3h&e%GRHa|AziG_=}f+FaCP; zC_Zp%AI>SD?&bnhpC-cSXp8UH8N>4?%sXv^e|+Ms{*l?z-H%-gu-`Da4x76M(Am=B z8FYzqJCjLS=-n;s6dpeEaYmLtWfGq&M*L+=w1}04;eC|eMNCs3r-n1EzqxXR^0EGa z&}XCe$m8(kkiI!c8Ry=Eb6M8pE~7jq?VdgPQ3CF^xUhg}1;nQ(36xK0Q06nH^TQh~ z3mi2SiI=Uv4Bva@^?2Q7SK+rGyc@RB1mMb|S+#0V1M~jDL7h%|t)4DxQFu-Co;o7X zOCUa`;Oq-FW|e@w9ix0|f`zr?6Fuh+ozTCi>`WP(FB6?T*BYNC!uAc_fLKY)C{N)B zuiA-|_kJy|V8Lc}VWo4NSi^M|fW+Ock<8!STvyshQ{ga}7`K5+M^Ff});3H9zf ze>BQRcB06o;nHXM7@e8IZ+!JLc>4TleT{BTDsJwcFN1ZWbHRW*9re=VtP4A!=Ekm0 z)aDk@n4X7RUJh+Iljp)-#U_~KTsGMDc2X$1VEQ>Jf+J1sC0?}()HQQUXpWS(&V@R`YvYC^eA2F5TUDM`7-@#@T#RiJ5Bb@_8!+j9#ONj9Ur zET~EDQQFRa=0sp@9T*p}-T6qZ3GzI0=EpJph|&QBR- ziM7IB@ng0F(!HY&sGDu@M#4nblyr1N&L=DZ^eer-l$C@X)jUzfzNv~svF+r0*R&1L#`zk ze($5icdxk&x2@TRyU!oTpIM;p0uxh66Tj2t#e1HvUm3nmu^=`Ym|bhHZFUjg`OfnLk$&^r?ucvI7WKtscT< zJGN_J9+g+dAUe%=VF4bHfNmi)GtC!KOt_-dR$TGIr;O^=^~<@#2{`v>#2HPfVo6Fl zIvx+K!V;@UItekASMI><$XP7uT?q(8n+Ar7){6fq5>3Nd8}g+pA2K7DB<>HGKL z`rTKfLxFTsw%5|ys(~CGCDLoR1WvS6S89b;4SJHc=)y|N3!^}e!V-Qg6d0*#C7wcq zeYzW6gLYv6CymkWH0VYpDSzL8`cK%SfH|L=>k6D4RTAyvW25$*IV)jla1*#BXLV;U)hH<#%X5%Rs!bEwDN)7G1YcXs{rT0@HqiUrOqPf zJ27GQ)^DEnVIoPlv`0~i7~=ChN;|=YisT+_x`KU6YwHGPnVhc{h(Me3lQn{GgDmqY zkyoGJd;{gwl*lmeHa{Fh$|PJ;>d0O4^ke|`Re3Ef3S>T_~Uv5BG1K}g5j_Nzj>72Rbep9AT#$Z%N?b1G@3X(qK z-oLAR7OG%r`34Q3%;8cAcUCR4<(Sd7QdeEaK=tje;+0IH^ zF%2V#qD4!yh{+rr8pPR=GZCG*2|H6eO3m0QY#3g5tbN&~6yIc| zE$?l^+bmE&rhxi>j4sak`j^RgANRliKYRZHW?6Ea2cm!8Ti#b!bya&iJ<~njUz|+1WAA#I+3D*?7mMF_@20W-%6yU_jXqX)ULFmT~Q)Ptq2l^g!gVRe%eoa@2bnY zTlb#ah_sP;&aH0lx6+E_fVk%O!>ObRIaqFoomuQ~1J_ z)8-9p*jWH`$KVERA;8Xm`N)ssI&Qu2^F)q)-yXgkp`AFMEFwf)Eq+z(L!Y*9FS)Ng zPEQq6%jlJy_hNRm?Z@O-mt*Tt$5ePGr#Q0lt&=}T52&1cV!_hV5*8O1F*i47{#t8I z*JrNvRa9}OyZEyVoJsp2F-h;(doNzIV;>&gyu*O-|LQ*oc^K`v1T$4Yu&Q4wK;%FD@+Wb1=4SYgUApyj_F{pw zstnYXDx6un1>bN^;^2fC0gyYOUZ{A@$1`!)Hha75Hv^$h=RXu$A{+^FU8yQ4%aoLx0|rKlJ`(q$cx0`_z@N*OqW zMZtI-hEAB%yjx|8wm-^R+0--!f>A|Mjx>8q-DN#;FrEW=`A$^4R95*bmlYi|Lc8Ge zQsO$5@TX--nPs#WVq|3vNBTyxy#6LsnUW?3h zb)myIvYnHHNrIGpdvR_WUwZK{W>=O%I26|9*u33p)p@_9VI&QMYBj;5V`BlJmqt%mXHpaLnJd>xi@qDzPM<@QO?!3PyP@yt zcmD86^iGb)_K*`_#c!^zEaGoH^=J6U_rC#mup>1s&v4Glp;7^$$k~k(B>cGTXFt{u z=3)+7)3gwn-M37zMtv8)e&q~4e&qxXuFRz6cWX%S)(n-5N`#h!E4z2dCFduCrLIZK z4VDYb^G3t|of{X!y0mw2l(g$Z80ltaWdNJ$ue;t6=A8~@vbjpFsIPA_Hr+gvfE%Kl z;DJ9p17zb$ZFACo8p(Lh1!J}>G5sj?i1QqIc?0=L^Ne%F0%e&-oUbxpKMrlXu$FXz zfcPr^E?Gd%&jKCtbiWn}fEPHhnR1-{**Z+Wn+JvsAb-vFeMVc(8uCw{cmez}6U zS=Ue~t$pL;G~LM%|>n~u+ZYA7h)7T`_41M_8ptMA3k=}n7h}F6HYI*P|jqX_czy!3lN^W zb*Ir~;YQ7;&wQQn+-}WnO`KK}R?AEA2*oSX82;=}{S-dT5W0^-`Iyq_*j2aM9I^nZ^nYnWi&&FA@-L)-eaxYwn)w-O0ePw+CzE!)ThL(xj6& zYUjaT^!9e6@xlosWRbjy>ZtjGv!_Sl7_Jr1K=*O|I9V^O2c)Xn-CxUZZ#R#1KwxAu zRGR*JWy<4Z{U`5D3jMukvEZ%Q3geL5mf>^D&<<7>Xxuk4f&1z27t~{c*SxSYZyd++ zx#l+-OY~befSrBQuoKzw&K~s9FW<}UIi_AtIpVKd&Zh?du^IfLZlMyN+^R3u8M~ov z{JX03Zq;uxzjpbzdnWrZ2N3kGud@qd?4zi&3xCmrHF0-~h1SwzIx1@%Tp(jpI$w{rZGc5!5XX)Qi< zKZQup@>QPc)1n&M?vr(|I1X#ZZd|yCo0qR5ukVo~M{t4OF-YIPe}CxXkd`P%0xY0Q z?U;ZJ&k%-rlvKqsB63V4f_x5aBKtdYXYkEct9>wg%l%ZKzXi1LDSZJO%Z z7MN?xu(EcdqmF^TzS!P5v+Iw6O1j^UK?djJySC%%iBr%OgwitvE7zw`pP>?s3^>0K zZlu;CBktSb^6~N|gepWO4H`SZivjruT?peuxQh=5G#oWdfVqrU$Tr<7J{Kk%|| zkB^Cc@yf}a=I<&o5&!=DVSMxISuEA#TEXAd9Mi5`LBbpmG}D{^rh-t{6t$QZ1m%+J z(y4`MoSL5wG57^**@wG&FiOnK?x79nA|_{Z-vGLViM;mh0H!L0BCcIi>Aktf{!ojGi$Nf9>`=3@G13g1u+1oWs*s&*L9`!-I0ff#zvBg$@E| zJGN~zz?`p(tc|p!2mV^?0Meredi0aPkj7?a`1J6uO#;4N3dS%#~nKapyodqJwIGE>S z)mmnMZPBv6K^}VOVf=r7`IoU}^Jd7En?150x^&I}bT4=*uj{Fu9`89nuyqGU1~N13 zoCg$iDSvkK>}mYszxXEsob$12zxduCMu``w0&hN@s=VpzTJjKF0Gll<(wsGM;}X-{W`{2iF~-7C;JEg z!Z?7tonQdEpKQMRp}j=JoSx^?EwU?)NywhqbX68R$sxQ+t^+TSYRLnmDL+>-bq$eV z8nX2YIdiTN8IcSSN0Uw!zPlwT_d!MG8n+QYk*>CzG`;z#DW8)=R6`G+p(SH^O7*ch1C}CGe3prFP}^I za%LfDt3)0N11>s8#x=3Ds`yrtcmj_;`Y7&lK;5>Fpy>{bC^3V5`J@Nw1XK#mO=umR&TQLd@t)0=KEHYc z-&nZ;Z%P)V$bKPkVCpHgU=nz!=ntNawL6lHkNV-f#)d}_n@7%+q&H&wxc0tz@iKNj zd_OvSNL#V8;=V7Bh5n&IJaqp9ICABp@z!hRzt`eJ40aV;{kKm)XG|P_V)ujSH&S5V zhw0eWqHJ1DbU&sNfCEvV7_G?WQxuT`MUq?mcA~2fe`oI__}QKJ<6G00@%ig#ad7^6 z0RBecsg$46fqoS_6D+!dsX!NfJ-iM+q2$OB*UyJ>#(UT|_YR_q z7jL(xeZpwm>lh+H-^CyynWug%%XW0nwFk>yqb__bgmNi2uCpNKVVpCrb^Sce=D9|k zM;a*6(6G+ZcwZu*Xv`Uxm$62md!7K*T8n-Oh%eK-h}m5JfkxC~$|fn5teIluyPC)6 ze7%W4(5nfMKfG}p9-7>4o*kdQhL0V63ZFW05DW733Q2?QV8Q6f2=?yUZGL@wrf1s6 zLWF(@?Z)Y)jsXL3*YUjW0LJ|}??rsa-?Wmpx6cZfvF~?T|E2SnF@OAYtdBd&F)%FV zM?;$H-~G%d@Xzq$f9x`I1kjc-o?o%6f}ucgv)P~{6LEtU?yUZ`lC$g$R(fca`dD`NSQ zt+4Gtal0VtLRI~5)vpz8RT)=4Q|nl>ia5pr9BIrq(BEIgedg_A z^#-GuSh5&_NzJ}b`b4Dyg2XgQrRLG@VRBzeOSS7;k++=KqQ2>b0dhxLxk-E4KuFu) zr4e`(Sa7P8Xs3!ghY$05XGsS}tlh-bxkU*Mbd}>h_`dgTt!}((;u=mkdV4SpEmv7z(eNtt~ zz+rxV9$)<87jg08CA{phmtlaIj|7Kk+wTeBQ>it9izJJmzSG~e4(EBDf;UJBhrr(Dt7PZiCzAhC0)A=L#`t@`8=a0M%lRfoX-aymF2ug?15SzA81FH+x1=VSSRPN7sCY=o>fgq`w`wy0V1-aPbHZ&Rxf~#!{J9 zg~b;32q7JbteiWMG^n@&_;j2=C%15_+JfW1VKcweq;coFW3x9xd6WQj!`(d?>g+~$ zt%IcRJ?58zJ3A2^>arfJ2RhwvS62X%=`iv0Q$qrKH$WZG3!^)k@5w#=$H(cgGa$ZR zBJG*EZ?0JfL8~hUh_BJJWj28&Mt`}n>c%t@hQ_buzP*(jWOf^wYj4r^rhJZ``^2Om znlHBW58+kY_TtWwjd&FS^FeBt7YUF*Lt65WeD#lUoIurlLpYH0b5TdJ+Xe#T8%9R4 zW9v53#(K9%aCJ!OrrAb=2)sj^X>Ub<J~f^@^i%`ImNoiad_`S4wP48uKt`a;G_2%;G~I|>LS z0WfCmNanjB%rQT1f9G&Br(L2Kd_ra$#Ph64XLENJG`jdLwCvUvZTpkW*9{P{=Vw}< z9~w3um2Gn4B!28eKZ>`!`7NWlrg9PLT`v!1*dC3SbxLta>=-G}fXl@y| z+Y3Ap&(AMUfO=s7cdNSt^?_cj&CJEZ%WiL4({=;+1-SRsXF)@0oK&@qZ|4kDm7d9O z+TB+@ck{UAga@rwGtjoW-;XcHL_0tz%Ob}JknrhA9GPBLcfMQe>Hzz^uT#5qQn@;* zJ?-`n>jaR&WJ@XM2v8wR0NEr+$q-)zhqo~iAft6Dt(L2)~l*n8V zgOqM>KkIG6IFzwpE(*OLH^$(uu>ETUmxq21aBawXCxTL4i56b(S8i!*kE86{5AY zXEuQZ#JTvkAH5SbY2dCCMihc!U0PVgg%c;SFk{atK6UvL?%R6@{q4h_J@E~bgVO8u zCV`w|N6p`T_ur3u@4F8J0|TOdmf_UCE#Y}a9qa50r?c+#T1z|6i>xj-(gLYq4t2&o z-w7D^07N0{U*L1KIlQkmiUC!({b5YrjXK9@R=Wvwtu`8{GA|RuEIxAuU${Aq4Li4? zZ*&BUbdAnVn|)&~#ND`kt9jN92ArL~p3WfwmQ!K_TwYnkUwh&cq)C4OKd|F& zH!o5!XFqpiDbv;V@RHc!_N-#u0iYn3xd#6Nkimcq(n2$jjP5N_(4&spq9yFS9E#4Uop`)|@6b-ZO zg+Yj}qrbl&?|Ij|@O|%o4+aPN@zx7=y%Ok%R{eS4{1$^viB z72j+2nLz@|H4<2oW|}R9TFzM^hnlyVsNk9To$-p!PTh!hchFkhODO?$`Z zVx@t+&wt(95>p^c;)t+cB(vnJ_c-fv*2s$Ek{9DX#LF4F9H_N3J-J=2o5()7mqArM zuOJY&lV4TdK98#BKHa>Yp*Q5@bN7XJ_>_HT(@ss&q-MP>4Ae+~hFq}HP;RNbee-9P zgw|F_ERL~md_A?afNz{Wj!U!C$TeTduSj14Yx5)aeaAUv86wYshBncW=*oKgc!3e- zX93V_QI)7Y_vPSy=9kW#!PN^F?Q>=O^r5}`@yy9%*tB5`Z-4ZaIC1F$zV^a1CbO?S z`!sG91_lT5vPT}lzPs*9%BM5$>)JEbT3kb#zU!>6v8AenMP9R*q}fvNl3t9XHPe_K5{?K%+dF| zHqnC)s3p=M9zKJu9esG^``(q%kaIIrxH>-*>l>3b|3+(+_=4}>zWEM3zUKjK>gf+{ zs>N6caRXx~l2_?CO@Afx8|&roy;*vZjdM9)+%ibc|45^OtPo^;c;y4AH+WTeQ;tMT@M+v z+80Tvbb|h#xpo0h(;q*x?S@nckd?oev>TISWArzU5d!8?^9|Q_T8vpqy9(Ft?2Ww$ zv;)C(-GASbd!LI|po{#U^zQECO4`D#k3T8G@9&JX+m(mF)WVsI#;m>67g^thUni(E zg1RGT2&9WM;!BH@tC+jq{Q6gr=KMV6$@|sasBBqbVn!@bNvpKT$G`SKJ>;J;z;Z13 z^84Vtts{(h{|wkPIf;F{_nKFHY?StG=sZXlEvv%kYi|2@z3uJz;Sc;E23Q;I1grde zb9oV`rmkXUX({xLr!6%V>mA3ILO%iWeVeyogaCR{=A5C=>m%Ot@o69V-~JT=^n;M$ z$KUcUv)?>1X_quf(55At3%^gm{8PuCvrR+Kmx?A9Ntn#uqx(r%(c`3<{UqGFL3tKy z&9sc-Iss-$92G9vYxwFtJ4p-PnQF`p_|_RPZ&{FC=n`P>I({CDFC0(E%J^GfWqxNT zhs<$c*2JCCXcorwy?d>N9#YqvbI|X7v|Nv@jZ1w{_0Het*UCB?I043JMmWR3<-uwBSWL3E*{icaf03+irc!%e!94>r(Z;-8dA_n9kL-R}NV> zIXu6dyE#k0{2HFOn-5jvG=INdy{gKZ|5n9q7yO#KUj6g@+m%z)rIXgOO#{P!>4CWq z9hP2KfM&Olc@uL<>+|c_e!^78q@hGi86pu*o@&o@3x-SiuayNlAq8D3k1T~2p6Rt% zTOh}`f|LayF>`mt%)z$=aQ?n6Sk5Mj%E4R3fK5`W7^pf){`Lg}r5Fkr|PMdw^ zI+Q-vJNd07emV#+e7yqWTqr#H@Yc7y6}qb{DW@!7XcJfy>T*wM zF(%VWb2~oMv4SN&h9`^_JIAB&td`P>(3h40y`&nrQmh(NcLQu%%I!t3VRUdP0`y9X zB56%3ZdxOa+W7$jL8M&_vHN;}m+dF;KICKtDvYxoBV)!~nVC{Vb}S)M-mKFLI#?_9 zXID<*ndvL|2?FVF*}T_MNWTq2rzQ3Lczmb>X7GKIexn6V_uCZW;aTa3O=)2mk4lbL z9p>@NN4Mb7kh|S@gbTrC0&-D z`NmSI_Csa#Bd!@bt_=XBbpU5Tn7@INWl+)LNS4#nF`KVS)|Hqh(z2vcVwvU9U7naQ zbIMFQ&pQI&>;)S=ofeSy_Zlr*jRZ(?o!GP1fxX>>27GVoB~U(MK{;#2Srh)^%nh8N zzu!9i1Of8%xUx7O*F4!7hB1!)?dvzr8uFnLOb{p^ByFjrm3|HsiSG&dUOW@y&x}c> zleLq7gYd}gDHlRMHvy8oefmm8hf1Di7Ez<@li|V!)PXX9pv-nxvy%@gqPd)?gRVr_y<3P-8*+7 z`Uz23COUYMfH_;IrCfo6=e=a${^}&{=dOuOX0Lfthw!ngs9#ziHevtukN>_s+gyD5 z*#3L*%Rl;4*uP_U+Me=SGMwxsOtS0{|BTSK%3=PUQ^LD5+&?JbY{#-k~_`4!Lk+Bor~@9Lw& zP^3xb(+GS%EIg>OkePsZ;l9;U3$H86n}=J0Ksje5k1Y5yGftCPy= zfw?siuNkW>OE;CHsS3!ah`aWDsnO+s$sVZ-hnZ_-FxJ)C@>vq1EeDSTydzlH8LN?E9#7)PkAPS-K_Urhsfk za7A`3m}CwGbCQE|d0h#V^Jg+G!JNk0Asxh$VC!h^o@@0>dFK;M7D2|5W%Ls>%MUU? zWCI|-29OGMs6&iTp(7D=RY{$)R;#XR2%(5Mfly+fm#FJg=Lwi!IZuj@ zH8)CqK301x1s764W8(VrT}|1g$J4Z^rJ)g?`?OLp0>7^LHgj>bNX*Y6zMZ`}gA<1j zV}%x<*jj%4*|u#KQ9H%|9KCQ3$LR0vk9{u&yL#~PuYVac3-h>D_#b}!=dg3rX8i2G z{vYw+!w(s=_bQ>5FNI;;BttG1Y_C-VVx7*^8hEkR2*AspqaEj}?NeqR>;Td#+os^w z?5AtXcxR(-_MzLO*KEGyLqj-$m}k2n--aF12X>)guenHGn#gBX=78%L@Yu#}NjvNw zn?&QJ zs*z6jlvXMgNk)kb2_PKkxr*~wL1rrq}MyVK(O8lQpA zTR&PD8ymyV{^U>L06P{9ZEpMQ?DP#|P1N)tRmmdSnddc$9D_AyTgN7_ZEU06Q=G5m z6>EFiZrV6Z{GHE!0>As2PhyS*q%O*V9lP+65B)SAJ8*BgA1lBDo3nrP+n*2n%vV<% zn5Ma7#K$}rP5E5jbiLA{v0}98HT2QC)7jgDI)TB}`9(BWRuH?;?AQ-7l0gXew8y{uaO|RXUhZ@j-3uPU$a1o0Xl2WwH9ZfTWz{6V7=Vt za*eM7jrqNAXBo7$cA36;(;WL+CxK&UM;HCp4M1a%u8hZet2sA55Mb$%)S$Nwf>-l- zc?%n3O+)R%$4qvy`Fn4#@w{FtuTtgu1pc_3U--UVTgb((sx7O&$)B$mziM8zi@zR7 zZU^Msl`HSxcJcE#+2YUrK1#s+FQWsv+X)?!D{@bCWa8{%TrrQ@Bo=^`OVEDXu2GX! zN-!~6ytU~>wy10_og|eeS)JWFAu(}|4-;@6g2>}`wAk&6q05CRPpagD6qHwj^sGEp zjA)WyDGQzamV^Of%t$8KX5tuPEqx1!y<~Zfvvt%-FGR8YWDlhac{s0H_AeW#lCjoR zbrpobzrXDEna{5Z&%=m>MrYs2HiFCtl97yWLK_6WN{6EOv{LP+%ohP<`1Xy=69Q8^ zON-mtQ>QUAb<;L&l_18Mi8hOt+1HeZPk#GrxK51MPrU8j2B81;pM3)Bh1Wj#h&j^c z#pB1!-`)4zOMiD86L{^%glNOqW}e4355g zSD=+)yoLTHKApf6n`7NOI%&&9&v@CtbK_bB?l$*_NxRN~{qWp%9HHNQ-s^1M`34EJMcKZL!akK$XjAt9{@J~+a}M`_Mb_Re zSgVPvs|)5F+9eXmU0zwBcMao3e_^d*!Mvn-G3%tQLW8MPh?w@k9PcY8ap(qr)_i2X zTTZCRPg>#}bB(yaKhT&!Pi1~?4C*3b);;gCaP|V0E?+g8MCCl2d!cIp#$i0U-|$A{49~ggDV&_X<^(H%w5SV~ zM(*CaKh@YgvH?3uYu?lDT;q1YLGDr!O zyKsGB4*!aPdDv%8w0ml5%2?Z}y7kaa3rmu>U7~5ck{oW4Uab(g?x%I9o7$;&WC%s8 zA4^ly<|LJtZG@0ttri#}N}txO{X5a@?i2uA2XL4_3xHrR!>Uzxy^V+s~-Y-@Cl?4XJ^Ru%C7vJF&fmY%1Zh$4L5+?Y+%L0VQZqP2@>`LS5%kZ%#Sqin){ z`ot-mKXV3giwf)lD~X>dW3yER4V3Rm|L)9{OZeyi=|5oCrY-n~Kk?TIr2h#$6ZN3Oi72y=+0P zwpih{8+YJ=(M@<@WD?I#Ux}w2#f~%I9_(#l_ewWbSX01&zpk-qXf#BT9p(i9L0g_r z3(}siH?)0w9wJ6!BLPF!hLDyXyx8T_I%f?%AGACUh`|&BSqa${O-#zm@K6B-y z0qJkvv`4g^YeAj1XTSzJ#`8TdS+u-L8j;@VEIpL+P)Gz#D$P_Qi|-yDAkF$+gJXE_ z);rDj4ARe%X8oy~mvN3Y?A%5=U8CUoIMAlV3|z-JD-SL7G0hcBqusQ%OT6Ygunk55@+E%-@EgVY9CsVJv8*);%$JVB&+(QVIGM z#}QCkAGA(@y2Y%Ke$*ULw;9RI{Tl7Sx1+hHSsbu5Y3S&m%`)o1+Ux>mj-PT);N*IP zIIheI5D?mF?H7?)_LCQYYcpb?x_0&Z$c<*)mD$m-@mD;A8{d8w&Bdjd9s@7_R%*f% zT84D2hsHu#b1uJ67-M(O^q@izj*FAzI*)wzIfpT=FCYEsg>UzliTp- z?RVnklRM28A44g()Jzg50DFs=f)rLyYw*X=-j)%Wed zc10RG;}c8em)}+afB9WPXDUWmyKzLGKcW)JX{X6j zs4}6srJ1>|2IqNsk`(0R2~0@Y^!`>snpw)Okc(1aR9s#B^YH$5@(K(I+&Gd3p<*d^ zV<0%gXzEF%CULLneL<7%zZ;(YG(8MD`X&7WtU%{he>toepv zr%J7$d5M!-j&TO+|M0i}4QbQgLtuS#B~bsPuYCzSH*HFuu>|_-U;7%KedZZ_@2g*p z$;nOSqJ;*r_2C7w7RDQaX}ySPG>yjGl*yj6v!dgYAWf2w@F zm5*-gQ!HbRz+lhJqS*t(tfcfpi2|e=TGsIX|BrkNpS^qv|E~kDz=SY+w_3?` zVx}`E9asL^Z$SvaOXpvyWF0VZd_O9U96ZQ*l;>@(6_8u)Fj&8n{@ygX$2?<@er{m~ z$L0vQFV5mTX-%X9nko~+gv8MMQ}o@{27w@LkAZt3tZ{Md+aYNMNDSZ^wD&o%zNyCo zg??i28N_oMd`|rUzo)b&dj*gP^+{{aLIU24%3ywP=OCqBH)EpiwB!bid#!ohYSyO+ z)U1+#;`-{6(fo&c^4Z#o_l>r!X{t9v73~ z>cZa5TQNR3oKz{F-~RjXE82t|!2Q~P{4IR`sqZ9Z9@?`5kH6#l@%V$UG+Nc9Evi6= z;Ga(O2^=mkXxz4Irva8TGc!)J%;U>ZI~|4RRw%^n#8v)aV}Q zM<)rx7H>=$r>?fIElukjAHmv|@pQkr)0{In@4qySs~4{tP|0EX>UHa_d~^WPd7Spz zcBe(xh?>c~ef?f_LsP_lcCKMKocUIKVa9W{i*xlmBLH(NzAN9I=7@P&mV_`-=JW{$ml(=PnLz6bE$ zJ0A#|^un3cH%V~CY4r5>MJIB;Ztd!{PF>rA>vq6j0^R&sRXl%xJ7tjfRq@v=kN-B0 z*DjqrZbf}NslPY%|J88-cRQgYG?7l}dgwpchjI<@>yi%=P{prOA}_Vyk{l$afbL(} zZK{H?M4C%TUwo8Y`*)TBP!KYJG$>Ia`B;Fmb@zTle4ha<)u#MyLq!>Yw7u3VgGS2e zhqctFhb(`ks>>)qgS=1k5&|48``Puqn4F%`3E84T!>$oQz4rBlW6atGmDDRAX-Wtp z_&nhkuAIjUm(Cl;H=6BARxZs?O7|0ht(5Sl9Eo0TX`%hMexhlFJYqf5{B0V^L=5S3 zeWqwJf8p6@v9i2e?pP(-MW>bw*0+T9YTR;8>=ONXMISRT=fFUn<4Lo3s~tyWSRkqR zXa4k0vGt}S)eNn0T7I}i6++U;g- zm=hAks@z=PUTyCi!Z!MQ#l#Ntj?eyP(Ej4h>o~tOhYNIlvq8d_4l4jr4rF+Fi}K^h z(f>%tWqGtyCff}LEd02-3I{SZwedAEUwuD_HVWfJB>>s~%4x~(?Dwm}NLl&F3G+w4WE+0(lSgecpwjzup9% zqc@gUFn8jt1?IkmJY81l+8Q=!d7!SNu*1A+(7Zi#M(jCWb{4gR<$&5Z$9A5=HvRJ6=Kqu{9L;yj)1HDyl(~{&DX0c2#_(iv}pF2|L$i$ zX|BV20=e&f%iHncSHBfo`-al*@^nyL%ri4n=6ZT|7G2$4=8aGeCs*{4MZ0W=n1{*$kK4qrHT3~d9CNj{yTzX$a}5~dO@8|>^d zG_{Y$4+Fg(HjOW=`TUg|*KqdYCFH~KJ7-@sfB*73pT|FV)jRQ~JqHXxueAyzIAu+G zgPq9r^+u3h1vD%8ro6uP)5we7643hligZu~)McFY`XEo!KWo=tw-eTzqwT=HvVs)e zXU^vAFNHL8NU$LHpa~M4g1O|z#r7*HWF+4Q%B2_6u1YAEuBU*$93&iD54L&cOaj}G zgL?-8i-jx^5DV$haK%=vT9UI1R?#5L#NMkY9H$VXW zW~tnHy-XXj_I&eLX(vpnjjzsOx0hc>v^^HUQ9k z@`P#U($-14Ac-&Oc4-(pnkwVu?dt(o0EM0gyL5EcC*9Q6cMNaD9U~hZpjYPn0=F((~2kO zOpB~5X|e{SU?$K^Ey{tkKmAhb<0|o;%r&N^L)t}hp1KZcOW+UJ{$cOAw$7fE>j!x} zo0&Is(IoJ*aQ+e&uU^k;j-Oj+054r;!fl5+Pwp#qVfTYA&S#%Ce^--b(?Z)1VL3z#?v=j zH*LZ%{f)ncH@)U{kyld6SU8*`q1ibC=4-lTEEa5gArz#{!zgUoFos|R8ZMBKcrB&QGcFM%<85zdpo*iiLS@dH=Mo1FO*;~;gd)R}{z0+A2Jwwq5 zh}LO+Q(p5f&QRCEH$u5>N^N)>?YTHNkLCFVbar<+fZkzD+@HRB4u9h_AH$sUj*pzT z;Kt)N>F+h=QN3N}TiJfqE)8D54A?)9>4GVW1z~vj)@%*tPSXf?4 zlD#l>9e@3ge;Ysb@T>6;UiTha7I+Jw9ltH&N!X!Z2lcV?z8GYkbAp#Ym$;&$jS>s6 zs`gqBWVhqJ@-kF~saJLh3F=d#j$(rIqe&6nyOAOvY zT+(MqSd0MPR0gX&n2_sBnoZPB1pt_EQ1ZHh;ZV6VpG<_1F>%t=`p;|Tu98+kTM$|G zZTLpysoQl*yK9+jMZU7W;agvCh&IZ(^rX0Hn@A?l_jzQzvm$X z)Ia{sFIzLR+X&B-#{U!n^?M(omErs7~Orrtkm`OVQGQ1=Iqnr;V zEf`LxW#e{WU|zHB;OUC;VBJQ=S5~L+wytsP7$uNS3-dSuS(Px7KKOJH`bBLVu}p#( zo_T08SDuC$8olgY%pN`vSS31(HiB`3^(?|f-`5j z>(YEP ze>xxgB43kd+~;opx3r|21Rp!bCb4~d0#L+~#G0XPDPo`dd7XlL`F#oWyTOkLChxPPWrrF6d}@g2bLvJT#@k~02uQNe&!@h9|>b*9c%fS{i&9zoCghG&}@E+hZ zzhGV2VX3j2wpCTgftze!x}Tq(w&v_at2*7^m(Lu>-}uzO7l{b2j=vipCU8Dz{0Nvd zJ8Q=4IsrQ0xTryQYA3?IJ^Q%Z( z@Xkp)hVT9y?NCaz;y!A1bxhx9u6+L~q?J*SkGwbca1wxLG*D5`tj-}ST^zh}0pC3R zA`&>DkpKW+XUHn)sNqfCX)W^2y1>7Oe3s}8#cNe6Ly-Uv`)rMX`3VBGrwEwyA^}mp zz{pkPi>&|hMY8Glpo?co-EI-Sbm$r575DFc;78FrFo4Tb*V;OglY!0Nzx|DG;)Um) z$78Q}1tvC4nEKA>H3OW@E?Ymjl^hU$A<)JHyV*~xV>=XX|Cq83Y2pXXx!qTG-~#yvo^gc%zDGzb4;re{bk$1=KxV|jW1k1WB%T>X*b@r z^-k;_7zuN%k}kTk#-r6@l=MIj?Z%hCi^4S>x+q2ohj%>Lh0f_2x$~zesP&wZeMl) z=PvKCZooUpbf)h*#<2iMhk5VMLpKiPV7KtB&dy_=H0En_3wFr*F_W(yuI7Gwgclaa zx=4#J`aN?AID0isqjEh`VghLN@7;;+$uSGgC3uElc9RJy`X*UNRr;(Q45bMss{G%WL7@jzKD5>($oxAYi@Bbk@e$UGxIxzWGi}v}_ zsl)h#7oNcD_8hFTY~|>;%FL-W&AWPavI5sz*jf%u%Dp!@F6p z?O5&a@4-;tfScpAt`jg{8SX``VKw1w+TL=GA31dN^`bd3QgZO-&D84z&=qN&H+|1T zC`gMu-_v2;_yvu*Hv7({7j_2SovST0j-0|Qm8&_M9i!@pRpC9q3yr&(Up4~QIgGlj zt&))Q5-#o%Y0ga@I_d_PGmX2tFo%Eg#XmMqElYuflGr|BwdS5j;dPJJ(e9c(uCkYp zvSX0WPCogMKp+FbF3y8!XU)xPrtw{)V|c^MUx`Wjed4L7Qof49p>t>OFTe6>{KL1u z->d_s3rP^bN)mKt_08*F#c$=bMr*8#0V>)o`96Oq<79kZW!e07yYH*|Gf%hb`Fd@k zoC7$T1263}zrD~QyFo4se)4(Esb8G4B_sAt1TaqRC=q@#V5C2e=A($u_UZOHOvdgq5P9qN^$phX?@fc`3c5CFmo>9;0)=@Cy(IRwacZVmNeQ~^76gq zGgD1>EJ8K`eA_E)X6bnyQ8W{zBc;Ifd2pc-+KG=ld-BOAv9z=lm|WXdk{Ej;8}qT| z&~1Rbd)732&?^du+Z)Rqj$gZk-}&sP@Y;JHz%RV>2XOe}*;^jV=EEWZ^)G$ybJ)Fi z5Bl%F6E7;u=qPxtl`WIzy|Lc$o98plwFjqG8(lc_P|JV=J`vMo&S|Yn5=**?7uII+ zXnlx)k;48>ThV*$SuAG@b~KHyU|(dey}--~4rZEGqfyftsQ1DWdUSm5Chi#= zGak7$YtEc!Zhp^Fem40UAZG7H5_qtW1Ua`8I^dHQ>B~qOvBF=tdYZKAr*Pl!1YS>I z{SEYIiyg;nQ3BL{9Qq0A+CG4Rbew2i9zyX}$2idL*M_2kc`?co|<-Yi6ZtkbDa=eu)wQ(S?2_K{YlxbSkbsSEy zPt!IltY&&S zG3(wxdfx;1N5Ay{#?~#H!(8MT%dIsWpT0(#^F>!oRaqu46yY(>rxEPgv>B7b8%l94 ziP45GR#_?f^yX+S{PjQnm|1s3A{d&Oz~k@#03Ltnl^E*iPRmh|-l3}(@v#@a6Fjps zNdNMwqxismkKy$M*5&u7r!7EdVY%7XR@AKJk~@Zs9j@}xjB5nq`fJw7+2rJ;S>u*g zR*bblw>!?W+1-Ja!CtdhoX1V~id}OW1X>wTdu?jx@F1ETrxnfHrB>W@4*}PuzAm(Q z9Z9t1+Bt=Dz?{n3rRpfIPGR8%QdX?3L8Bu0sUWoV$_WSzP>1kTpE7A_d;9+K&$2kn zi;Jd@>+W20KDpw5fAVYTF?co+3(og!0lGWGI^^dK1N<&NmW{q=VP{X>fpu#CUhWIh zcC%#_Ytg+`44;}}j>e-@4(z)VgTq7k!e>64fpmp`_mw}x+xOp(@7aA8dI-ibiIHF^2$oWXuksw-chFGDQxP{GV;T|sRWL85B=W~$ zz{wNTbVWMI(#zu{<;f?Z$O|Zsh021j z6Fh?!>QT{GaU`T;A~o@ssa2H8#r5Fa*Cl`vq+>NOg8qpB1<>iJPBh*+FN>&$wj)>z zCRB@hb6oFQ4xYI#BEYzQ{W=aEIc$DQX&YNlxu?^nqasgbc}6X?KuKrY6qy;88g%)+ zFH>d61~czDygD43u%6qcVQg=?|51OuGg6sgor~O&pN-Rr_v}0JF~ z)5o#woVAr`8b8>zMR@4-``cA$JP$ew-yi-{*YEsueDNmk?Iq0!d#@ozkB{VG2HErJ z%%FDB$NNdcboAs&GzNOY8S1gG6!Jw{3R3}Hrk(Wj!t8atFmnyRfA%1LWY>MTduSsz zl0YCaC$H#sov*=yQaLJ-j22xncC;&lB%9tJm7g2hJ&S^>3R)<@UC^i|<#2#btOqjv zGmQmYZZ6|{`o{3;jtP8g@j4cYW+Hy#I7%ZPlD(ze0>1Yh_kx z+^o2ZVkfg9?!k^&mc{v2twMee&_SqbRmLG7=!Q!&QCe*KDad zx4(j3!fqRBIk4zyx)fMOTB=j}XWBCE_4c25dgPB$YdX;FP2~%B2D(YXXw6+8`?1?> zVV*$w!sTlgO!ze>Y!^_mF79Z#K=WK#RRD^<$%XaY^gN0qr`bbcdeoX6*i{`&5&Cy+ zL)YeUOn?1pM8QVr5KFJT1FjC|5KPjYFb*`@GU*|%iUd?8Jsfd=a;rL3h$J%h-twCeN(oU7;D)Y)D8evJu^p|MQ`g2(vMy^YqjSp zjzRPgD8Fk5f%7hxmd{U_amzO zEGZ`pWEwLuHjY=l@eTOepMD0-)du8XKSX7E<(~c4>VfhqDD5?BYt>yxB`6pCdT5jN z+Mz1stE3saPeNzE?oNwqSI$&3Dcimf)5C#t z8e~n<+C6{pAdbCw9HEj`9VG{q&VG~Y9LUG#>{+}ewJ|P^S&R&`SSwHV0FdL{WPb4I z)l2AHUBjufXYje6Wh}q>UUY7%q50ei=<`?2narU~B;CePKh*9`z@~AVToE;$MuSA5 zV6D!sO_o_8t+iTO2@44VgB>TQcIU$y5MDSTpWB~qOxz)aCiLCOVi_ma7I8MT~A!!ZRxP7}N$pP8|sb!nJ~@gg$N zs@h^pe#i5FnE?407p`Jo*C1X$Fp1-hIUHM=M+KRI2Z) z@HUcF2jJO!l^H@0+}CKXw@9mBxUtWg9cE$}^y{S5PDC6W2M91E+M!GmRAv1k;CrNv zs#N#j;4Sy|bAHHLlqbX&9V>w-aAY43A*%q}$fFdE~-X8THG zs2ANgX3;ghXu$gu7f#`wcRgUt-u?db1_|aYA=FMt{L0wXd^|z6J@L7!(c? zFsJlQ-Gc)*Z7eeCJib`K$7bI_@I!4g*q`h<&rRJ6U%w)1%#{PyT!uOeyJi+qEGz+A zCixsIQ-@vx>0RR+2&B_GckWWs&gQsA&YkCt*TTE}d9QfSK;~(?N*E%?VIJ27t}l-p zrg!{jx`>Y|Ty3V`JhI>Pb68Mho~(IS7NlFCUNb&9%${}z?Ccx1y0zR06k3i}w&RTxUP+ao$#1Jb zaDH7C@_6gzmA}uQt(T9CD?=46%F-PrP|lv&UjpX;*F)kYPHJTYf06{r3#KeV)xBEh zS^hq<79PULeds!4>}+{mIaEbi^Ep#>lW{aL-6%DWd#Pj8Dvk1b09mzpCihB~BSgkt z`FnMH0)fiBfJjt;61IbQ2T^|?l6)iQn*2&rNPjO`#TZ?DpACoVxh%Iztkce}Ef*Rq zc=G%ySMDN6OG~3#3y_Y;Sci6&{R`g)DFdicga~PKc)(u7d>o_Ybxu)3MPQkj_QS`H z;^^_?){HLAxN;nZW~?HhsGh^J-_1GbNq@QYO>&lr3n%X6$Lx4++{7VJL4fC3VrVwM z{WUnXcLP97oZ7t^wY^(WTU^~>4GTFo!PXtTa?4M zf(O6elBUSgWygy=NBLP(oNwN8pw`E;+GU-zum274{5t8!r0YAIzsxnRZQ5-&v2AGu zP|-r9{Cy$Xg5iS&>YirmWdFGQliQw=wXv*=7WA6Wik`o6&HS04v`L(|0ZkA&j4OHsMGbsrYjliw9te1?=vaQ9Xu8Xsh@am9tL{B4{8b! z%JYy9)89h0h+Ncz6FO9uAu`kcnOh4(x$Phv92mrhzW;sr(EHwx!Ty2d`=g&2kac5z(}72b@shn&%6dT39dcEK6CmFYY)-Pt#{vvU;Ck- z!ed+a#Q1U@6hMKmpSXAmzxCADNJDP7CNziou)Jv$1#c-}I`57kTkY+}8Ugp7nI$YP zuR62$BUtc9U^?@0o>w(pcl*#CDCV)`_NMc1tER&iMGMXmfi^$qW4s!+J=uG^b#e*^ z1Su`{u-mqghriI4b9*3sUMWBEA_!%r-vO=W=0^&n^cc7nvIyYfo4~8GU z8~yZb`a92}MdLf+AcBd!1?oNh{pjxRGp`u%zMizorT%II;=*tgak_{mwi;_@+!%mP zuU@|MPJH`_@F#HRcB?^GDz=#tgS!R?@Ew9s5%AZHQ@EM|->f<3<4hT(uQaW&v@XEC ziBBnPyL&&bow-Y zHX12_P=VoyQf{A0SgP_Q+f4>p1eHAtgWTU{4$_jbnWmjE%h_Zr*C#Coa*0_Ul<+h! z^K*<%&Y1gpNH7gu-IK}FiZ+vu#dMZ;tU#V-1*lKr$ri{SXT`Dk$-YR-RwAqtvNa52J{o3RtYkqO{ZJiPSx)x2W%nV(A<0CkA$419Y z7KU*ySVPF?yN;9QnKS}TVno0iLIUc_N#}K7LY%aI69N1?wp-?epE)K}Zy1ESGHo;x zzAxplEu1M{IG@Cc@%gO#d$Vv)4$g{_Dq{Fz+85+ltpr z?83{&wxna*eJ9h%-y@F?8K)ewSryXcKM&3W5cV`Ki~(13KgQk1x*~nuCcn#TOPFaj za98&bUe!N=qbsvGv$hz)KQfK5_!h2ADQHJ1gKw%>m#iKAH##k!@0+@FB3CmemI%N) z{>C!-_VIaX0j3e+3b3q2{Gv#-*Y>5OnzDYeiIeY>Uo8XxzTUomI!!PAM#sA8GN5M< z%JVm-uzYhC%d_)wZ8X!5=Z+J7`lIdqv2>p1+n1ru!$~3hec)GIIY|IeoVtkOe6}Z6 z9MczR43JC+zU%BH&G|kwNiex|>b#5V87+D35@~6OMLwnHkY(q0MLC{ys^*VOtNUxA zVmHaT>mt&|_q_S7`0xJu&tr%{y5-R=O*0PQNNYaB4&VT>)kZsdBHF;MvuswpZ_^gz z2rjgf6IjZv6%5H4DAnp^M*iZ|b$sMEejQI7IhZ^j+OZ9ff8YD?;fG!m0Y}aQiE!xZ z1$>M^w%=#o>L-vkK4d(no3`O?QwHcd;M}xp;!-c4l%UbO(hQ*9B+puE9*^GfTH$=X z)ZCT|1F~x-jSgM2W6b%ZNHpF2fb#8VSi#%s{9@RiWvrBV-sjHYit@E?>98lAe>-Vi zZJfJ|n`bYX`Qk&dX4hN}%|-(cj!fcwV*#_n1K9YMR~c>k;>q)|;mqE8S_^x6&`rW} zZtI>}2Ob;QgtzQIfbsrOEY8oPr?;1i+JW_gmcY+lcXW3VK(`L}yb$pR9{pbY*5^NE z&#&(zk>>6#jzARYLXS9);P+r?3weXz4*mD z9(uWRL>vE%6G`z+B`}uH!2s1-2{t>vr;G$EM>iIe`1W zHFU_qQl&x9`vyRX$?|;`-pA4FfO={+nFl3)!`MJvyKGs~iIsjYVweR)fPS52kY_9^ z*-kTVm{^^@liGDiW?g)nla$J5s*MKS=N)g-ctU`nGfq!_V}2^~&eO^#bN)`{`Bg-h z=Is)!JGLuktlAVvWn(EBPdcf0!nlYuF3ruDKVJs>y|kF~`m%g69P z^_pX9lm+&8Yy<8gtq5z|z1Z3?cl1Y0FSczo3<@7HW6bf0!BWO?i_1d`e%XzIFh2}n z6-9U4<>xSsyU~k{wN7Ii$EL>YHT!{WJMq-nQ`VdM<}teu&Sjz!&tK8%QD1K zc%}oxt6Wvs%$<*9+gR_hj9I-F89csb&`umue}8|{Y4b0hvD);94`5*5uF%(sm20-> zCeuxf_dt2V2J!~bIk!O<2xu+9)7=;KB@qPV$xlA0I z-Gj%_^eV0^%3I(>OPm-et($Xb9XDy;7Y#rc zNdxl8JWq$bX}G_ZeGw)ckTUU+=D=xQLqt>2BAUJjGbk1Fb#v&Ed+*0z|KN|}W%oar ztO0z6`RN6_&s;fXJ)iTzkDYtWdx2SFzIk*EC0@&qkyr|J5It8JK$%-!GJDKF`p175 z8;$-S+Bk;C-}!xb{1tBqj;6HuE-jc%lg-q5Ke>s<`@hEru{zvm0WJe`33&Zk-_CP6 zU-LQQYa~19p*(sVk(nR?!R*vc%#koX?m>6v;KszgX0`0Rb*X7Ppv67~g!4A#i3g?I z(X{2~Z5jrw`mF<@Y)){HXwfsxIqzd9pkBL4fcwyKD+CvzP5^uS!Mo8xT63O9oaVNP zJMfCZP59QrRUBNriGlrljOn{}d(e5iLCyOzy=939(t2aCu=7{!H}V zzCO~3chPSzdI_+1(lwt6U*appbvMQJbvE;#>auNg9DnB>@5BH1_kI(&{|>pJ#UCBV zl4&nTqj;-c;b>yZ{qefJMna0!`Wk8t*i&mLA9fJ8M)cl0$@SZAOm7h{Gkjn#roa0F ze(3{`V+W1r!U#^>I?w&=&W>LfxK@GK`~Z$XalcvBeE>ie-?#f#>v9=H8&wJf@^9Az z>N4FtEp44TPWBJJm+u*{^FZd$eDiH7Ewq;sTovmz3muNCk@+qn+KzSk% zkY&q+ElJ*5n7hP}OZjWT$+c(5ZK4{n-RxtPW}2XcEa>Xz8mJUN#J8dV?)UhO%Q3zy zSE=ts+vJ+Ba4neMtdnADVUB3zf*H#Y3qJ~i!7`V^B$dOyy?b%T-aUBn)G0jk!tyfVZr6#qD#|g4_jW)(H1Brl^X$Rj1jKVd${a zoEv737(C-Fj`!1bcVlgJCA4>wn4{}oeG;<=PhjFruRw>aNI<%X=kF7*@ov(@`Df|6 zoi^e-i?9#@ECT5AILSW~sV7(ReV`7?mWPjDyR^29SNCtkmd;*$nHX4hAQ$O~NKqkx z=FpDfc_~j>mH=YocOh<}qX{`AJue`&)6tqLuUixhrb0%YJZm{eYatIELAR+xApo_@ z$H&Rrv=DS8u1!e47w=p_P&&j+F#%$p-5*It`0+6Fq^gDz;62lJaZZN@+Rn}63hf|E665OaQM4i}c^ z3_uM{>DsTBw3^+*!RyeL(G3L5$ITWY+b%BEn$15_8@39mTdk|CJN>aK`|p14Q^res z2t@1O^wu}yBX52mwhj!V5{$F~6pQ|>ZhlpBzBE=F86c15;VJlS*)2@GpPa7wT=8yN z8~Duau}za0ZjRx~mCFX;g?;GWaa_%Mc(1#?8TaLAZ~sdU;QkASM1wAt8YH+vd{A|rEb@+R)^g!Wes%U6uT#jFnY6p;a-M3* zNeV{Tyq7@JK1k!HS=ohNN3B@tT?#qfE0r)&>w$FH8py6?nF}qLYcXL;6Rj$bY$E0@ zEi!^Ge_Wc>f4+V0)f3b+d$O(CM1*hpSOhmNWI0so&EDmJuU|NUU;6UL@&29nnqxLn z#?)TPQ45i5=yIQxB>=Ym_fS1+f8fA-aRycMt?VoiBVv~kY!4J+l>qWmsf2= z8S~yE?9XhG_PmSo3nQGE3CK_QM~VpTZl)#xX$5 z%GEhyuIko2Um1;yvVfh^>K`1$3_IELqs%bc#Ar-SBB;?hCiLVg!_9(@%EvuIJ7*CgUd4_ zUya->Gs z7Lq^`Ra6nwLc6;sr0Hy(yny2D~~&(Hr}Xf5ls6j znSgEotwL#%$Y+MWsF<(YLX?(4#|hKY(UD!2F7GY>nIHZTKKP#Z3Zb5jd%3~p?9*5+ zgawrN+>jl2U2|m|B+zxo#1?a;l~&^Dsbpel$I#b|wOCGz_#AUKXE#S|`F9lWv6nxF z55MaN@YtrEq7dR+t&{pMHXHcT$)kAU!bxM&{;sNYEP#f ze?dE~E%e)LuVCu1L+jd;NcFkyHt@hle<+a}(t~pv< z##RK}+8ujVcgNbT5;)h+iX|w8Lb-}iKI>F23KduwJwO`#3+C9qG<4HiIC9q>bkdrz zG(*Cc_)OhJw9Fzq)=TJfLXc0Ud-OyTRZ7csNeFy`#98QqGzxLu^EE3b zxX;+f4F9cg8vHH&@lkCbee9ih6A2|+PLNViT3-0Vj{g#%oPpNlT@|p+@AG%<($X5G zPzAp8a#ej-m4?KD{#t%3>qB9b~g z@!Z^2cd}~pEHWtwb5&!VVnAf6P?@;OClDgDi7bgS`17A-_wBQb@$>X^Mmn3wKL2)_ z{G?9Vuk6RtS8AO;5P6mI%M;31bPWN{Mb0|%A(g`hX`e*X`cwmQc5WI!_nH5QuhF8s zcW6Tf&SRpv_9_q~T`h>v8Cdt_Eis{5MLsBiXCzE{WNdUy2kAF)Jkz2((2rNW{Cn`g zJ@?{i(x$(7`Xq8o2`#;^XC0dOCUDH9>4VHso-y(A``(c5Yb**jb7vF8T2c>xWumzU zlf)#8EMejgYIhGrCmsB1AEVA-u=CVmX@r=yk8)A zk!ABVN{bhEUtiO7?D>5Aq|tz>>3PdwdUH~X7UP%_lfm=9vw97m>ThBT1Nl^8>@2)^1}m4ZWAM&h7HaAPv>w25Hv6cI_Pfoi%Uw4{gN5oXjI-TPOvb zDP8oxDWF9XCkjh}A|php);HbXoC9DV=h@|(xY=68Jw3x1HxA@(81Sxjnr04ML%K10 z^Uiu5PwRwGZd>0%&?QM3qR1w58c|8qHG~l7!)gU~8b673;%F;4{@@Xv1&0&=obuH;W|6e2&$Gy{r&d9cj^jisD zz>hcAQ!;HI*o!`aa((uS-HKx)zyqn!se$Pbb49LGB-9&t@GfJ{KL6}dS7B-P?$VHs z+<-uvL)vD~c*d9qhm+!&+>O-XtrKIaq%>M>#47r{o?)Zyzx&SdoH}!u06Vz*U-5_z>LuIDvu?h zIhT96JqT8Af4VlpWv0P`<0rbTsqdm55 zY!{el;;sUy_s6Cw2iUE9m303;3y6lDGdMmHZtnUGW5E_ZvfHtJbAAR?YILhw0MPdn z4d2-}g!3y4xNBgPz^Zl7ru;hM=9m3d0iY}S-21LkzDsLv9kT06gHN;|;gtdITwf|% z^Xhf<(U|P->%#yEquD%uOJ6^pe9KSc4=xuC=U|o&C!!HTkb$-u>wId+>u>?nFP;oB1Nc?sytXF0IW%O&HM`1p#r6?yXh^0Alpng3S{9iG=i zwst59O2WguUvm;JL;k!?JRSaVF(P9k_mYR`E}6vXS5_JuF9p|a5VCuhO)~eZ;1~Gv z$G4+hR#g)WqMX@;kmXN{sn-YQZSnzPt5>y=TzHeT(^A~7a1yVd64LLe1Vh- zW^--RlHDUNINjOC3jX!eU&XI}`!gZ_Xjdou=<1tupZ@YM6qVPSD0X&9+7Dru28I8ClD zbI_=~hdnl)9DpU9+?vwt6_)$4jkA&v)jmcT7|K|_l;M*S{bx)u5p3DyE%$`bR3&1984A9Lvs&xmlY!XIsG&*EcoSsK&Tp8=A zGtyu|x_x-LTj6q#J@O|2acEcGnk5x}JG(|SV>i5W5jVqoP+=~Mu(+H290PY1D;S-+ zfo_sW-!VRkGdHgjZYZpZs%G22&w0;HCSf*rn)8d8e(DhBkDtLPd*>yv4#zmFKra#c zzDn@u%Ld5=%f41_Uip2yVcq~o=WgKWtQ8`Rb@$@H$VNOs+L!Glc-Y`Q!i#wq=Y*TD z`8;tGqHy_X*;}FiWO_RL)|95?u2y&y6#y3%EwwoCkY1;0=SDaByV? zCs*c@c(N}3IgQf};fORrx943mR%QPZ=OxiorGB^zQ_r+hJgX954CC32 zNx2;Qv5%OH5fm~oFHRFUXGdCoEayRq3ZaU5F3>qXirzbSV&U0i=3MC{)I=xx^0{#v z9J%>?lDH7>R5tI0;2m+~VoA~x&67>Fq5C3g!o|sHdRa+sDa&+M)0rO?M9{ZMZ z{{-O`V$r*ZG2gLa91}w$rSkbp<-X(iouqMQM~71JaPh`9v&a08zVRi|i3IuyR6b6c z{l_1A6^1&y)3)=~&%g+qvH#PreF}%JS|@M~YM+_Lq7er4>ss(-YGfTO?B$a{NI=}P zR)7!hVz2bMJZQ^6X|D}*qib%Nz+^XO=VqzS&M|5c&qVjo zxurC`H*|Oq&!EwbxuqL7EdckzTr;#Zawl;r+jq_uX}mqG=A5*#Is3qlErw<;PS0YE zt*%Pu`?d@V3yYX*H89!HANq!WN4t6qz4rIMA9*q=i23J!Wxp!#2vE6r;d`%vx8uq_ zwt&6P=ZOyvV3}yc8iDu4#YLlG?=dw<^8$z`A@^^{(A@Re4yLM0MXZF#HlQ z|F0Z6Jc#szC5$hzrlkFBDQX;C>Nc{G3!I?H$@i(Q#xD>mU2DQ`4jGd*RtdtP8-^|N7al;nxV5&#WxNah7=X_B-(YJ@;Xi zHMMetc*Zkf#6`mXQtstTD;5~oLey0!<<#Ptg0wV9#;Pkz94E0ffw7e(bVFd@?mg!3 z`6GvM@Yqqz3!tv$+?BvRt3%e1=9W#pT8;qgimLOO>^mO@TS!x(!hAO1Tnk5cUbx_%>^OBD_Z_~5JBGJl<7yAS)J6Tv znt0xyr<|F)S=cR;)1bU~Uz?>J@-&VAL_W*pP5l6K9lc4~5tLsBCyrWYz` zE4-;Gr;$;Cq=eS-eebKMVjA#Fi=!F*?jlyPze!q{VhQK2Ts97t7S>j9b9vD?iDRaR zH48Q)U79v|V4!~eE8oTR(}xXEA22{<a5QD#k<5O37y~JuyRE(|H472x6uoC&W}^ z8DG147GI}7PL4I|+h}orXlx6%()EV!-arBJ>d0frLLBdO4?1{N2jbj{@esb0X(xbK z$N6Z#vDf~{Gg&U5<^nNtUt73Jn)FdT);Eq}V&tA*B9N(LE+w$8b&_`yN6>);#Ot(9 zp%p?I;k4o9Hh^q12dYCn_ZuAK3G?;%EAzKncAs?w;5wmgzP~~#v`*@_Odx;(IseYh z%s9`;@ms%McpPrYfuJv8O^P#8Ihq+rS%~ewRY}^>99fG$Va`Dgx=2I&9=OXK&ouw!A$NYXE32n35-b-YZC^~U84n8!(#5YRlKMleH>IGI zFsHZgAG+s0{Iw7KFah-Y#L{lJMqMBw)0ri)XE{_;IL;Gvpo19n%|oL^pEii&lOlf< z_ykAe7-V%Za1_D=;{fh+pQgW0T2I}+8Y2Vv@OwUhAAjW=FzA7~><7u42(ZCs?Emzu zpEhTj^EhM^_a#1(vtBcRd6mlCPyO51azL?<=ne^d*_x|H>q?E{F(_-W&$w0VfKYkI zY^-Uf-!1}=JtMI+9t47=Fow^wZan3r0!FSE^J9df`smVH=y1xS}p;+m?3&98pcwh7gBWogOycBumO-deyob0p0*3m!N13?$$g z0+a`UU2$(?N3-@2r{I+T`nuZlm;Lq1l;e9W(P z{w(9c1tnNFo#dHJL&j@Yw)|a|E(^N&EOS27{H5?;KXk~B@zc&QkU&`2y-BT~co9gf z7bJ_PO3%}$F|H0J&|(2biD66zuSPUI4ez6d-9Aup?W{^ri#SS}Q+q-b)A7uTk{Oyb z;zU#OSuO+-zeznp7Anar*?XK3iFR6i?aSfJ%Bu3I{4Pr`Zzi?dzLiFww%o1@uQ;u*o5CoP!8TFYwES%~0zs^%D2BcUJcw`IFF9mbwz{1E`N7Ie=x zxpsSpwVR)Iju*O!B*L37dUJI-#+~LP?8oNx{4qO->H9EE%!pzqSj4QYwN`Qd+7)af zCV1!Y7%t4-B>RfBS-`$;c-%OCV$g1JN*T=h(gtwuNJtpg zvfBQ=E2~UFg7ff=j%nNa+Gpwd7E?L6GL4g~b9mFxW^5bo$7km*V!pMO0NX0M;`5I6 z((yUcGU+hq6(Y!Xd&hMV)7H_EoNSW-YMobOPeaUq!RcFw?3=)y+@%AZfanjMw1N$V ztcT{46I)m!AkG@F`RQpa6HsUhK<=6I5<}{r3p(oeuzDKk%~%psc4sYZ#dV?kQ3Cip z_t=#E^hLBzT?(g4$b1v(n=q=$KnUfp5uoV5cOO$ zPSPm8_r0xRxy)l>THqHEK~pwOOyH+~;D_;!*S*03%ecs#S)L_uzF?h$x=(6K(kDL; zV)+=DZzHWagY@*Ya-Gyi5pZWv-s!!wi!^@sv!B98|Ktxax40OayQd3}eeWyrYv2E) zLlv4?nciT2_t%~z@SO%(CXMA)(A*?L3pc+fZs}iJ`xfy z5}4Ft|GC?pjaID|tg4g)$Hwcr6Yw@jpxRn(Bs?FtX^YQCcSq!TlXkQ2xV`8eY%deY zHvPwS;w@w?Z_X~A%GqCI;?9DDt}eXczK3xC;Kl?f*St4a0mw_BT!3xA9m*!55P-U> ziMSB=2+w0&mlp3!A00%K1Ske5t90s_4rRZB4UM~chOxhI6pwE`5RU0vrgmB;f%X#p zlDNk7DW7w`si)tRZ6jN8CA^oVa|VcO9Z#Sz{sx@poB?`g-EL8+8k4e8@_m@ADvcxO ze7p2Sx;jt4>UzC2s>;{y`>d?S0UUwymmI+TKN~tCc!Ge=`5NXJUagZpt`35&GK9an zbCvkle<#<y#flbx*EzQjZ)0Wn_B2-K` zX)XxW-n2f}Q&np3kySUTx)36SdzYuG5EQfxCHfWHDztY+T;=Q;gK4~I1OAjv*?;xh zpM@{+NOuo@;i1=5367rqbEK{B_#pC&1jq4(@wXAiQg z#-xqC0_!3&!MuOlj8_KfTxSO9W?ne0nE(!PUfM;G^+R3`5VVX%+<&bkBVCIvr(Itu zEkKr$>iBQ{_(9ma&if#*7OR*jI_+7yMGaS{rVNw2d*cKyFU+7{O0N}4<05pP3~oNJ zIiNoM|XS2nIL9VnlQR<^=GDwZ_SK!43u`-Mrr=+g%@(JC?yAx2kt37XTZ*X z-@azghvxHBxAqSipuU#`6U-PG&@N_Fnd&$PaE`e&{iNVPe0y+i=f5t^$sz&P9>_)j z?VFqTpnrVkJRa#A!+S?|;N-y>R7OY?m{UIX=WHj*(E!n$O}X;`XMin&RE*EGQVqBUjLNhUGLgmHEeymme8CASH) zIXmAjJblDDg3H%?g~ZY&>;V_gd;zQWq=&pjdZDbo@P(*HY)=;C{op&^jURgFyUq3} zHw4UkKGN~n;xtxtGjS#w)(%k*KktWWUEMi88TNE1OS_Jfoxf#3)YaJ)1u^cI_m_Y4 z<9~=JjvkEbKhgHb9(kE{0Jm)qP?c^5@UJb+n~x{@2hGuGe1sZ@xrKSm6F4W7W{#>< zsM{p!agMWX!yNTPuhXWp=Dfaf6a|C9>m*q2>p`u*7k#6{Cz_wq}*@2bG zVXXD{g#GCz13re01Pr_-tA}`zMfRa!_LjRd&>bi>%KKp29<}HlPGGV;GaKiaQo(A1 z%TR0Ctxh^KQMY@^?RBeFIA>->IM3ajjUF`TI)HQj9UdRU+YUT{$3`{>L8{6CS`NT# zo~M)`93Fg&v81B5JPZR5#^HE8h43w;0Dy0)t-FQ?O}X{rveRfI`Kz-gn*~MMj?i09LiRPrk?|U=pWI`Vv6L^2P5IDqFkUpE=P{!W0&v z^Q2vU6EDT`T6C8V=9DfS9otH_qc2}bDm>IB9!~pmgdu0|2MDOY^);`@u~R4T>Hi7t>8WV+|dCbDcq~swk{_e=-_&f#a&5~7G-exEUA0VmFA;oZ zVr;Wo2Xwkf=Z#-H`PaYsnXxYVKGTXPRpI-_0YJ zkM@ENKW6CR3^C1{in`IFvvk0i*3sXIi|4Uv!x(mtk>G;`5X~lTlL#YUbdBlzm%c-q zp)U09+G@PM^YLtU%=q!;4A^xF)Xiw}?_7Urq%>BJtzbn1S=<;w36Ip=F3ZgZPRvf> z#hDxU^2HOTEPcfAZy}JrrEdtMy?xkB0DcQ;+q>%>;sk{VpmkPn|E~rTfEzr6(|-jd zu(h-)aUSQ=F!R6A2~Bxo@j9l~R`396awa-^@r{LRW>2RC^?nv=i20tiNNSOzg9(2T z=06h40Urv8E>^A~9c{4I=|u+K#h+-aP=B44*Eie>dsipg!pA7hv&s7GteMwwZFfbe z`LPl^Uc0q6074aOt|M^`1T<{X@oS%s*M9x=LNmy9T#Izs3hU+!*3Mrw^WA`GTR__| zwS<@=F@N!ZS<7_oSne33vA260R!^Nr^Tv#5ci#YR%u7>tB^%Qd8dnalS@)gj9lP;2 z48ZDjAtaD>)rgEzZ2up8+q>|S@BaV>``wX?E;>7aJGVNAS(;Y+nMh1ju(9 z2XOtUX6$WwScu9_(mLwpbD?>k`A2@^U*n16hvFyn&(?i=@!{|P0N%U*Ayk0_WIW8$ zS_7Xr_AEYi^f_b7zI${Wr>AciI(z-b4K$i-ri~a&n0e#6l&@<#(gCIGSfMx#(t7t2 zo$Y1dM!;N?@VGU*V2zDIUBviq@INhC!^3S}Uxuk6VE3X1C=x*hk3J)D0tC z_4~^`xM|jqo@MUeEBo!}=myMmT|jfiozfxtm3L0GEPurKDNAc&CI}yNnxz0CpTF`SY61 zyZ{U3*|KbH%{YYRwY{TYA$;A?%zB-@*w-z;z|K_pSkgkd?}=Wk!YTmVRPkE;XeD6l zB0INWk`z z1GxXTLPr7`0`nrqlYi%a%QQ8xWNmoIY!H>eqi2odwUTJUr5eVyk0(8uu9IDoSJi|N zOitNk-LVqkyO2=&Eh=v-%K)A3S+6XvU|MpZo`7S|K(Qo; zo_`RGdCc4QnB0#p8eKB^?v-T1e(o~_&W}84Ua_Y9e|^gj<6V32D={b>M(KT`+0KYl zq7ieAl7n+db~?~PjfC+}m|Vw~6*@^G>}?hMJyifaI)HbdpKlJImMLB+cPw)R_V((o zJ$K-)-FxxeQQn_^#4ypxFb=$(-$x$b`F*$PVu2*Ewv5faIyyVTJ1v<`4+@AO_HEH3 z%|i3WywR37&5>tu?y=cfy;e(`+$HLPpDe1Lm&t;e$Q z0`2zwK`y?oS_JS}`mV3sw2Ygtu9)Ki5QM5ZZKE^wS7Fb6y142vO^a~cjw;m;R%_mh zjE>Lds#?Q=wYmZCHEn;H(PK@I*QWP8v>$6{FQa*N3YDQf0Zsy&Q!X4kjfJD9&__Uh z_<{ZCC9QfYs!?IA`uSf|sk11Q`pU4xkW}41wxcf45#{1~he85mSs-jRHp?__P7!E7 zL7+XfC4J8zy@!Mq4BlDN&%m9X*L1OipD;)L9JY-T^~nfxNK&Ezro((kNXO6J^c#D1 zvN4a#%_V&Az$D%}yamsYHvPD9Ag9Hwj&s=eQ4|UwP0=~FAS5Ka0%RbpC=3A*a;%!x?VJ(oaeu;1X0|&H6+472%6@+C`O+n*_tT@ zvU0tHx6IJV)(h|8$&4~pbHX@p2C7YR&C63pV_w{xjh(GRV*pj_q;_TDXSug@22(`$ zy7z1|kCvV}Y5;@iwpgJ;r+h_1M-_q~K<+D-&FY(xrw{|)miM>e$}Nvsi<#rW{deQT zkN*Vr?cD9wSew-{3H{Em%;Rdq`e}(ghnBMw9z^kXto7Y8I%Z7TA!)Rr&Bl{?SEr## z9rcbjtXV(P(a|E)sk;)|y6;%X`r~gzo`UI_V^iHRzr8{cr!o z-@uteroy{8+myz5@P zZR=iivxd1AHQ>_un*`-4_=b3g@EihYN5GzH$HVsy;HPQDHMEwb=f*`BwJo2=&U1#n zz4K`>ttL8+wwup4FAOiifS%WOv&F%ArRVetm!4K}8;4C_2Ofq+^(_{Tb&=qg){tb{ z3cjd{?+({_sCt&a&lj5tp1%r2x6Pv>Pd{f%dHUee?WDHbM1pVz=r4u;wn8`uJ?HN- z=&(+R%x3!_7HBaAVdwBD{`3ca0Xv2^pj};!#2?X8!jE4qvXFUc0f6(-uOQic6|Q3?b&0={ z^_Rb<)+#c_F$s+byluR~Yk;Ane<3HIUlr*Fg42cO6Blc!1@@4=Cl_4g8H zNysbaON$ppJ`-Js=7FCHPVWLt&7nqsyr2cOv3U$@r>C&Q4r>|>B-R~S2v6I*f24mL zKQyu%ADcdH&q^n)nDOev%&Xxct!A4S?~5}x?GVx$QFagqnC|U?+CBkw>4x3+?cUV9 z(>LY5t$7#ic2Q>mg>5@#W(xSd+YF?Y_S-U4JYFZ5u3E-gd308DbD@a-D|lR7QB8B) zj0#7rDZ_LaW{Vb2)~44Eorc=H0bLtM(d09mbC1!tqH`5t>C8p**EKnYvA2FNI(cgW zYw8jAKq4l$QJ$@1ewdzvk#u9*8)NWzw?Dool$h~=&?Em+4^e2%Z2 zOAOjay8Da}Z@9bHXx%3UhDhVyY3{p8E1;nh5+ohNNfUR6&BDF^jRL-SJx_DvPiHRR z!M+W6_24FK=;*;yi#M>!Oub_SO?+PsK{uqyyBSYP1+5Q4*+iK=IJb=fB{UaTVxr+gSB9D-fQb1-0voGAp?l9()W=4! zeDDM`3kH?=#?4a*XOZzpG|?G;54hKu2LXnPjS}XJ@&hBrlP4x1cFG+)ci|U4_&6TE z`(DVvTEO#-C0trvu#PPiQrfE_`pyR4BD9It*GU5A9kp7Rr?H8Yp$px_z;|_Z#dm@? z;l1U*`}t4N->2<9bDzObAAbDp--lm(-8(Sktf+1k4qd%~j~#p>cxGoaa~9CncnxAp zEuxh}!^1{*6!s3eSQ6C%vgt{Oy4`4^RcFmAUc3^*Jis$#?jx| zjURdFm3ZU6d-2b|^htc?6Q4BNbk3Lkh}1_1?HXg8);R&NcluVO$H7UhxgRg1n%d}*Cb5CPWb#}VE zl`(ziIW57qnPlMUiv{h9OIqjQiAqTRRhXS&XEJ z>J|kQlTDhi>#BHd^3DO063=x-{=Obc5)$!^WOCaDA5T@*JhGHNxnzjY@9&Y;-0nFi za6aV0`Dn_lWTBe~`2=ayt}M)kHuAtgYL8_9CUns~(6T66jI;0~wO4X5xO?$UiGdcm z<!`4 zI6QR~y`;(LX3y>!X)ttypV1<%HjhKIxjSi-O$4{N=WWZs1>!MP=BPBEs4};4Y`KOo z`+o}yHZ9X0=a>p!mVFx5{CACh7mFqaSo`DWWZjy~o3sgm^s^(P6X&7W<1J~tl{+e` zeM749LNEKxFg~O`+PxJu0wOavXN(_#PGW!u_Uu3(J=bDRO2MSjPC&S-l52=cx5ea| z_&C{J{|N_Ra()?Yi)(frb`0pC2}#VHDXh?VyV9J+mBm?jb6sCv4hFyzGtxzjGi%<5 z3}`3N(a~jq10NsNO=Dvqc$nA8_mSpmNOIukx7VJ(V`Lk4bob*EH_u_7&rF9-z`qX> zoa-blE_R6Tsuw`MmUPH!z8gHxR+dQvPSdkesbLUKA<``&t4&o`3)Mg)8D{n>fAfy5) z@b9&SIehB)vsheR#g|Uov*dX^u62`UvzxTq$~s7^jSZ0?xB<1WV$RIwKL+VDvv$o7 zXPdF=oUM`STG;i__-WD~o95GfA4TaXlB%GXnWKLOj269PZrL2CCe7MS9zs*SMgmk$ ztGA~I)7qL!_c)V(bF#TOR?UO-dd;@w?hPA58?ZC3pFQw$Y^inQlTUpcmyVyn%G5MQ zc5XJp=cpNvX6(wDwR@*=(!{+^>%>EM+=cJkdKY?XQfsc_ymEc)fBm`UNIN!G==x+838~oPD7QZVZN+M=W^xpg>l){89=_FEp9yu$2IyXSdVAC!)e+T z^8AIy-j7Rp@2@5JmDl-LmfyA+>oS;mDATNnlYSuH<@0I1yu@?$`tqhNG&c@0dG(Ex5|?1cd?63v1%PcA(V z$=rssIQcEs4<%5ogvmLw{B8x#9iuOsCVbZ>&$e3nB!Tu}J*mA;;>jByU0ZPOBL17V z{)hqR>$S0OlRaY-xVkt8sjb!`j=i^eg`|`Y9**r|1aa>`mjDlz*7@_(V>+z;@+MUX zAR;owG7bMCFzJF(mF?q)Wz>M{zyn$UDeB~yZr+dm&Nsdp$4{NaGY4P5!qQ@Fd(juB zzde5~8O1DcJY(kG(UC0HzSu!Zec=Z5-tAbO95ujTabeLMmBM?@+0$xBJ^^e>l&yL^ z50V!2S2jO@pFi^r3A3kN&&-Dg zw0KXH2iB3EHM3R5LonE@FPqr21fCx=Q^ek?TlUQ9a3qdjz?JK2=keV$ivcjXzPMH+ zfWFGiqh*u~(^az?bao!cz>(Qs-YdCw@;o{wHlXv-d(k>~$?UbQ=rptqTvyTzc0O>Y zagfAE4>g`Rgo401A5yq@>{G)8xp^0a3t9_0>=`fZ z#=gpy0PSP>f|@^L;h?>xLL4Ke_e+2#ZjsU=ZwByJ7wq}vn9=DN(|%q>;V%Pt_DIdZ zo}&#AsOJmr2VXklvASLl$?)}=>Fap)&}L)u{x8ow=+%ttr){3o~oi;0cJ*)*LnpH?=(?ZDxVS{srUM%N%RwD^G=4J#!IjSEtZTfToki zD}(ID*-Kcvc-<3A+vkO7Bk7%)tyFq@&`knH)}pRFe>{x$V3wXR=2`$}K|B*Wn#~e9 zCp{669^PAm{)IKc-m?R^_rCdU2AG>u9o#(RHlAy(;oRyx=KMZ${@MYRApFXT$>VwJ z@CFR`^#^*)fd!Skr@Ws8vi$2oQxzzA;`kAK^iP}vIG=Mz4UaweFh2awAHZYV_r&_< zJW2*iKFRyck3AP&FH^Z2buNC(IQmj8HfgrysZ$<`wA=NiRVV`M!y_Z;9~i(ipB}Ip z&Efs@_xsXK{rT8!)rmf*ZD&pD>Oc>AXBO?TWjE*Saa{z^yXKd$*4vqC&WqGsy;ZYq z&3;-;O?ZE_+nP}5_>C4C_|8rX;2+z%#|W}7U$}_N7cUww><{;k~Esp>dMQ8Gf|wV`1m=Zj4KEMfY}4VBkentejt`}}z@d$E9@DNrS#8YK06IJ4lrv7%;M)%Ag5u_b z&Vc(P0~_%(8~5XXyGGg-J}QSz5qVE~-7%2H1G-&27v|THkMjlV?+gi4z3+c$}Q-Ao(oes z2iSe1+@$jFWlMTKOSkU&qEO+C^3K30+n?q&njZv6I<`%q##Rg5_Dd^h8(hIvA8Fw4 z-h=w&xJj&e{H*nIZO(Cyp*g>R`J?=I$~cqj-@Og}`*)*zd^FNiJ7@ya6abZD2n%-& z5872m<6Pe==^Z`{5=k2+U3QJ`KyL#4v<|e#CKG9=RywQ`${%|dXe=CQ@^ z?FN5jw5JamS-x=&eue}}fAFRc;yBH@Q*%?5{ou!am^<3injnM_%v?f$Gn!}Xp_BH~ zv7(53{G(L}Lf7fQz*>Q0++JfxE8;v=N}Nsr(O2nWtg>E7mslOzAD(83wb!pfAr-o2 z4fz^r!<*->Sch*3jZ_Hxv?&xMyjnhb2I|Zu^wL=EA>mT@uB})(ebH#!(>>gg31A0| z9a}dVGuo9Sr|nT}Dp2KUwk{d&gm}la!i&%m@|#RuE7~oQHq~63&UEC>uY5IW&3^!! z#>bQKzDmNwlcX^>z+8zW;Yp4Y!06Z`tSR3;Hi;n?I63;upstgi^Qjv>Y{KqolpKem z?yM>QHPV=W{+TC3-*MQwYbQSZo*%?}?|T%L07KRnX?wnY@7)PNXRuimB!n`++-g5- zbt~vC+_P417wSEosLzv7mj2tgaick1V0M1a2$WTrt9D;{o%(S@XHW33-Wck|%3!bA z?qmYfe?DG|#vFTjXAkWx1ZZ_ShkY7BTOG{Ssr*e!x03)eJ8orXt=`(8rHil^+-!4l z-+CV`8%VSL+>=k4Z6O9S)|2LC)x6In;~#=*?s;S-+AeKl)O95mRR@{;%9Qb&I=eW1;ZEL>9Tj( z#DrE#c!<9{ryymE=aatbntxnC$L$%sXz^Z)ZL`J!ac}nfTx%|~7=CE9z z*j`!s$ecvwbx?y%yzgRTXv7ArG0q)Q&xH z_r2J&V;7!1bPy-co(>5W5TGe5TOOE+V9k74qk_P2*G^tE&-mDv71E+FEiD_zQ|yHm zy}hE%RgzBt>PoPmauIUl?`J0N!cz;^@$@1w+r&WOGHJ(_8Zn+TcV{p*KQn7IEA@`x zW+V83Qm!OsT(ddi#Hgipd>*ZjU_ANONyD)M4_2hvylHhwZ@N3ppY9)ln!RDo4V9eB zh;y=rv<>Oy6=FX57&%?HN3EGQb)KAAi$2h1|7Mf76_8fHwq?R-_gg2<8{tJVXI)#? zcW*&$-*(gph%gh{dj7@rLA|Rhdw`#P?il?YHDJAu!TO%CZ-6}JRr<01qdo6ZATPZ|B(lmbS3U9*7Kt#2xsFABkaUeI7k(|np`rVPTncq9(y zl>`1uE$c{ZfSqAgGIctnA?qsXZN$;lYlJUjq)wv0mA_9YTsVF)7i`3$DOV*|#)`6s zbtHg$Up5uSKkyHEdE%5UHdL%%uvUq!#8xIt&mKi5 z38A`nZ$;06J!ownx8U24cg~OIK__Y4xtu`&5R*wAze#!|`Z0iTZ!RB*gHG2v`3(di zl5WR+cR5q)`}g02AN{`f;=%m~Ap4QsZeCcK$CZY&j8duKUV!|B7pN0LOdGfA@2rqU+^Q*P*ddJpQ)#;PF?yk$`_jrz+@p6j;RH z{*Es3i9I%}hX~~&2}_np0KpD3Rg1lLx9oMzg7kvw(jt)EK?2em{qY*rO@anKd0^gH zj#gB24&a0Mtui9}3UcO-qdg>kU0=G)=tI8m?9M`{7ZSSGVU_ZIe(}W5ZEAiMi zl=J7m3T-zk()VeV-n;ElRrOW*ILzmD9xCZ~n~;y|{M~w?YOSd%gDk5oa}L^FV5=?$ z=4=u6QuvD(+{U5^ho+Dj%0J$iqA;wYJ6ZabyZzaZGWFmB64WgN1>78Jz0*F^C z-l>X$C1ry62I<165Wu%25IG|k%G2(Q_jZ&079lXRCIiPzNk&%-(_rz;-B_k06fgy# zf1c9FH2(bGs$`r9BSpcWls*dKYZp%9?3~q}zk1tV>>S$Q>Zni>3S{lf#F~N3-mwXs zym<{*=4UhTnQ4)r1Ak@svBEl=@!GaTGp5ol1`>$NI%=7dM$!jKU3FTj$iHa^=B2HH z=m#xksw=z}<d}`ISic9~eDVogxpoa&%REp~SW!lb%O<%!o&n4H z!gxxZSwM@Om-TdGb#er&C#Nt^+HBqfTj+tX$cvJ>e^vHGg?3Jwjh_9=wukZar@xNN zq*X8={n8EVsn;;;@e3BT%uY{ZWONiY2I(OuzgSBu<;D>)I}((!d3*HojuKVUNgujj zW-hjjkxG04nk^OtGvKx7N;;O}KO(g^>)IUi-qlp**cg+J+ZW8gT|!e>>Kg*&t&``W zsNL(NP3O|IUc5j+-DzgVhEYGT0D(Y$zsqb5(0n%ZbH~wQulurnWRHy$Kus-+v zG4t0k*oU4ilNi{)hrs$IAV7j=j*|K3P2eRnW*E2Fh+%YxI6_z}8a?DgZPQJHxna^; zUu!HUG%{-DRq|afU@gsQLF?n((52^o-Hd5*6u(j-EoT77u}%rCR)TgW?Fa+nB-!vZ zNkep8h_2#H${maap+yZP!0qGnHc_|UX`OA05^W&M;Ae$x@1neRhI@$Ekyf=61>tn{4BypUdXaG3AMeP)=Vy*u113om{C9uiui=6H zcSG`JP0IUfa|xH%mW%_qpg+-KoQ1N7;HDOS6`@By7!@e({V! z`A0wThnQo}>AsR(o%r#0zZbvwhId&9aO?3r?Er;)-tRSgbYCnip=Kjt2Ak`{fXuhY^8d8C<+XNfIP!vyXc91EuG?Ety&OC{#J zbmA5PH!9)_kn3m^Ts;2{;yVe>0~ik931IDxQ&sNTe;a6$ss{c3y;7NWov$6R_rd`A zZAf2^J$Wzbdw%ajvPhyFN6w-Aeg00CqblTi=X0_ufAQQqfO~15`CshN5zMFE1Ihd+ z%)7W2Y`ORn8DrZ&Pt}as$4X*8>6K;=qHy7?}9d66g+AQ z%9>=|X5z^rIr zmMxqK;}0aD*fT>(abPvMo@dme&T-GF`*qqEx79g`E62JA7O@NwyhMS3hGb}i7GBvp z@tLh}e$DG};_PW$I!7SR2r*oH)YQ6zW0kZfy}iAm4dcExUKri+CZ{d|_w2^%CIaY3 zE@5GQ0V~TZVjSs;EOhBUpA-IfOLrgs*^bBX^C!QK1p>9$L}1t*@n)G}4?b$v0cg5(tmUq9 z$02D!=Ts;_LN5^MxjE9yRbGicWNpb^#Js*|e77-Oy}q_$oU>hCUBXOr1veWjSRp~n z6x}a1*Ib|FtD5Lc$Xk`ikj)+I=iA?bytocU<}ji68CVfZP3VKrDUjpM^+{1plVw_V zJ4Y;2sFO#894{ZZs#L>JZetaZk?v%94Q*u+ek$xKuT!n7(`a0}iq`ZzTD)Duw};LO zQnk+LB;)v9U?RjiL;v&9gsqu5bl#Ak5o=e?b(RDD{RFzc?+tIkd*A$4BT$t*)O2$d=SX9|s++FbsgT9cW6>sj&t~lV zH*Levz<|+`cba`h`u3Q77oIqN7$5nq{{Bi*UY1LbuHT3ckT+PCa z*`dL~kp1e|AXY~D6JW0W-frJ@Yb{4-nrM=+sAG5#T}vb!=17mn#a2%)vt$qy#a$qhs&GMTh zjqshA$IhW!J5zRlUrO72gm{fuHZ95}ze_;3e+H3;itqBW`!yq}uTrIYw;hM;g(|S! z4rt5g+1M`Kw<|*i{&OY8x@q}mbUjKMbJm)_1kC^9hYnBNRR-$p0-mmgVc7~}P(dz+ zT1!SC9~{Z5+9sAx@`+og`Dr0e_yc873A2V;U)Dd#!a`K>s{h=|E&W5Ga)+ zyU&?0pN{-Z-e-$t?4dtLv$SDO*9YeAQ>4y;0<-TKldRulc>cyE^UIp^aHJY!8~B=J zWp#@ko49v7DfL_LyceH2{zBM~4%zC;n}DZ-+*U7Y+BJNi3H}i%eN*Zi<)0~W-=_VQ zFO{wYC~<9&ASw2y*11+ao446mVbgXtdB6XTyYS6tpT@D1FB+%p`S z!}d?@1A1_D$Lz?Z;E zW)w_EOCB{cV2eGw01q63nJ<6u%v(K!F-4m5O)d9*;g}4gRnr*g=*&A6?SeTHiUb4L zx7F;gH3Fd*PC{SsUj0?&_bK(w=DJ4m)_b1S3(F|3+%Uf^3R^yY#>DFw8$s`#yD;$J zespf1L?_)#b9-cfO#%%-M*spj@08$!VjVaxJH6qvhqreQ88gy{hHK~~?HfO+l= ze(jq(5Gm!bs-!KRA@+5a4WLCK1&wxvX-&@=puBeJngQin(i_>HC4!JK$bJohigk(& zft<#h9|C!uo5xKV8mBIxW5;It+l2b~2CUJ2^YXRO4xx=a@D3+0h;i%7;*UQ^IRFD=&Y9qi-gj2NT1i*t=;f z#s&tVsu+1+#yp5JS_g1{fG3U}PNF>a$Rnu(xO{wP^E*6~AgL;jxHg(y7xgm7gVu=uAww1b=CW9Es^!$edaGYfcwi8tdqFxVM;ip z^xF@b7}qnlmWi!S0hOdkuB>VBteVQ08kwUx0|>o z?Ko+q#@m~BCRwjDLEDCWu?LWAWs>NKZB^9_B9YLSNnU@B5Csq!NcW7NLVTywQ83$B zb^u-BzKvTFdKAW9Lgiv50x_d7);EaDbJG~;>LHMRAHHznFm9UtXp!NUiMXeZzq132 zvyplc>0hW2&VID3bG(mvXg@ntWOM_VSH9djw43>L+LwwdI)HG&%_sP!qVcRIod{vaU2#(UW6}C8B^^RX(}2d{$r;- ziwg_p*r$$QYL29&Rrfg!viEL0Q9kb;+iA}F{f#R}N&P}v{RZhUNQ)a5TxNh#Cq)lh838b61 z4AQg77xo0Z%k&v@VY8R6W=!6@^X)Wk=DKm9w|^(}h9T?t?dVy1cKnu& z1lYF&SLk|v5xGV)#Z+Z$ky1{iq0$7*i|f;ZK#JE>GnoI@vzY((v*x~g+a{w~?|(^y%izm=kiCF=hb(pcL2mib;4c@O9PkZOwL;uCH)h!_P*VaiHj5AM>MO~c3OzM;+pHnm0ZfBVU3{KF29aQUz;XiP6C`pGa*fFL+D~xmSph~4I9$Y)z6t04GoZRXadFjGS*ICvSpwGHx8Xd z^U`&6-LnTBBuuRD-HNqCr%+7KB4Nzit<7O!f(3iBjH)~yO3!^AX)N7$=Uw>QkN-3# z$H!ya({F>u^92$ZvF6-^6%X1yo#7x2F1XtdCD<5HTsn>w*$Ii8y&EM_xzlU8uxrX4tMg}wj7 z9bLvE!1pzTuuksJE^D<_3DlNAX?9du!b4}zYK}H}@GAkY&o`DQ1H%TC%QEs`mN#Ph zCEzTtlla!LrKg*Q#`wMKzHTO36eP%e(zd&eaalFTgAa_}sT&xjZ@)O2voPbGzgA?jPS$rdPU6hYQA&12fVi zz5SS`1)M?p_uc(44qdy5!&fd~zOe!%AVB#Ni9Dhix~&Kx!8Esg;kjbhFNI_1(h;3* zX_V<{txJ4mY#ZB%aX&7C$90H*mAEflCb6m9wh=jZT^(c`4t>3Sc=Vx%ara&O@%8U~ z3)8c+Xct(EuEatafs|Wh382?VgIn};VQ$|REZ$ha%$>tUw9rDmgimC zswCiQu=d>T^GxQyGcUI0dOBO=g-}nnJ8RN8@R4plE#;XrggJAWc(zuvX|*RDudv|U zXosjAe1`WLF?y9&Go5wlJzIgDo9sJ2UW&oF_s-7F<2Y<3puTGpim5q@b1m%z*(X|z zHl0Q&2z(V+ro!IM*g+L^)rD0yDW*RL?Hm}OGidL*YbUz5PNHYuPMe=we?oive>E!T zwr#wlOEdV#=bpxvu0G?X`OZEIx(A72nd~7zUhi?(dEjw16`P>uz5b)ml{0>D#q#<7;sl;50-+bBGJ z$TY?tpnCf2K{bLWc@bZ(Q+^Go0(%^l&7L=n8LeuIgkq})PsR7O$X^sx&Xun}i#lo6 zJMP$q&PNWQxO~GLVQ5Z=P@)Vn1oL%Y2JMqXvIvRgBLx1&)#si&_Thu?d=CNi{YgF8 zg5z3q8Q0cUtdlX{R&G6I>#!ag*JeMtIoit#q$Wl-n7@vEU%AX1Ks%r@M}pMf{o<#M z6F9ri+{PQ)Fp9_D`ffb_y>G&hG()fAA7r5Si_HcOUA=&%#;U0VpSga|#FkRLqrG!l;CwLdHFtj< z1l)V(S5S8}Y>kAwtL&7Jk1rG%uUx!}{{4H<%>cbq1?_n_m)vX6c{>z4X!HHZ8vf*X zwE}gw&weA(sJK46`6j@ny+;}FW&l)+ck2RS`1Hj0!OMCL3Gf!zkY%Wq(=F{CS7lHx z?~^#{OW`km=#b2@2M+Q+-!|-nUN2R-P*nn|$b{&GWsnd==H=tqf>h!@ zE$iTZ{VVZKWea+G6S$!;5duR}#Q-G;6IjpuI}qfUQ&I#8JfPCO-vM}KV0_&^5J6?v zp0HPauLJ=J*g>Uc_5m2^s=Rz#W%;V8HAh}37iJqP;oVMS-kxrokmDv5;n^E_pd18$v;MGtnhW29tX`f`PDGr;aM z)#3xmk|Y2r#&r2*LILH+smK&M%!4?l{p)DDUx+c)M*#ZGuYCi~oI8ty-}$c5eCMID zx@!K+xvyaXS8;B!H(dhh1%bfT$zd#?pTX?RES8Ds8yp%enYqglK*Bojlf|9F&k#uG z{l=f3Is*(3*a}{s5oe>*Z_AF!>0%B4=-iWd*Ysul%6+fI#cf^q%?k$!sL!QsM`^DR zb7`7Rnz%}>xhao|Uf*4N>d+^iIdb)4O*%7)bBl3qE3q!q?;8Eh*EI&3Eu&GdF*s+h z^fl}KyoT%w^*_(tjQ55g1Fc_R4i2 zd)A1AvUxu2e6Mj63ehNY)&z#0Z$QM4;$=8D&c&$y(8w&{r%&jK?*n+l_q;Ma zo0`Ja<_a!17TG0*m1$62`ntN%L*tXjB!lvrQb|?Dheoh{E7I|Ku`v8g=a1vN(^s(ENy0GRlg)ik<7E7B z4X@d}8z0#JNNJq)kpRGgc?91dN#FB&MH=m5aoK`&>TeIu8_dWLQKi2PLm?HtMl{&3*;_EEm_61D|krL0Ow(i#T&4e0x&Z=TG$wrawEgl43O?wvCYg=8pSYlm| z`3?72hTMa6_VE6i-TU#-mR6U5>|$vP9<< zP2lq?GA3X0DXup}{wf0nQOm`q(}e&L-UomkYdWzK@EU!7zH`S8?4rMe&peCchmV*u z&}HEJo}dl4nmWl;bdctDjR5+>zO7jK#xcyy%wTb8(U|?U1L(TqIxsL5&-^_vP`|SC z5%c8JQ|HXt=7u4^Z8!5>x|QcL@o;+^g#qZyq?-Ny zg=K0skfeNJEi#sVzUw_(%&~Clu3aJ8P7$~=TLK8w7o>0UPUG2y8+c~!dPQ|~RyRMhR9b&kL9`&+PSEJM zZ(JpgyW?ke{>dZiY!)iUbqOcSnxKiw2^nmDcF$~qu+au3=1@Vrvu)yM9Pbe?*tB?N z-Z;x@5#VJoUM#I38C6=eL-1@4*QCZ*IH+TR>ygq+*8TBNLxNWYUei1`PSv@ zR?{l#7d7&(9ElUS*a6Lx7YtCZGf>~T88sSX&BLdmuib>`j0y_?Wci6-QIWo$9=!Vv zZ^FAs0Ll*FWZ-?~=h-P5X@H0N2GC7Fyq7>N?I_AGXfZp0h6+<|}hss9}JnHvE_9gjWu5Ptc6AHx1z+nuMM<-^+Y-#_{^ z4lYfZ6D3qv&45cb@8-G4H0nmT!XMo{gRgw)6#o0i-inF7=)`S+G|UE|8&K=S6k=@) zvyFt5YyrXV>LY^&oG))5M+C$6x8QN%z`EHswAMo3^ddSMR>=9T{SV-a#0M-9UhbfD z)^1FPygY4Ti>8%xm(WKby|Z9|uH!l6T1PE8k3U7}dVpTEq7MV7ZLd3F!USp1%`9{N z3p@=AzuBLkr};f9z-yJJIS0P}S^{Ga&P#D*S^{9lvLe)>(!4(814!DLYrIn@cYp-% zNt=jtO8Lk!Dt~?OaU>9~;+gX`t}1O+xv;IDZl0#&722lZ%mw@?SjnEMo zmTb+HOlI&|gEE60vF698q2UGgBO;c3>N#)+_lk9SBb&1 zm%NWma-VT8EMg?Vs3~98@I;&Sur8wV`H#E@b#`Uh>_z6l+VyuHc+hClpSo}cbKcXt zc0wRGth|Ow`||sN?(3(sabQ1+U{Wv_D7>RN*S19=7~?Fi=m+WGNsIbwQD?a~S39sB z7<0*tdM#F=vv1Qn`s=$Nd;mM|*n>mQK992}PKIhO%+F(JbR;%~U-xQuv8K!NE}z!~AdWcn$v3;zfM^$|>W$-2(+(ll*q<3*`!!PXi-_FL>E^j6BlUZ?sO+SrBq2gETekG6Ty^tqQi7sz`%#^nhfa}@>jLV#wm>roRC2M?s1zbd zW?QbIP8jgI$6kS-dhh$uPhi@Tp&EMndd;5Z83N};M;CU~2a}@telD{0znehp&WTL~ z<>>6vYng#2|+dAD(_0;jpuJwATof_urUDjjnmm{@;JiJB(Fbe@8bycJK-G zZr_B)^_yvPy5AKN{4hZ8qPex|&D)(YKnoDgf@|K!RWMj*s}c{+xjc0~2Cj~7cH-CB z=V(C$xVe{g^VILN2HZYuC4e=`@iP$Xfwz3`)9^IIrd1XM z(@Ea z-OxZ21c?oTD0x6pL!a;;k5TB}y^_-UGT=2jHa_u@R)L!Li>hA{qH)x0Oq z>}~Ei!nN>y5gNE=ltQu`)%`6RQ~I4>xcPoH@GpHl;TAvoU&L{pw$Dsfu=E&nWF8}E z*rP1``@(A;>P$)paD&ZW`zc_QX3G}Bo` zRMK`}1iqeWdm2B8UEs%owAb|t*G;B$c5pp9dJ1p4SR_1;+XpEagUE^xvv;_aJIhe< z=?E9{?mn}$fcbLJKwda7tpDymaTrgHo>4%3!RId{M{qJXtT;ILZEMG?si+L!y0X?R zCaDGUb9ek%jn_vwZM*dKhOav!kQF|YxwdtdA|8ZOeoQc+_fx|%fu6~BWj9Xe+B!P$ zv}fIbORl^eU;pQ?Vqt#HXqcCl(M%@QQurgt979k#JBds&{<5hNliLR|J3gzm=*!E? z<{Wjw=Xriw7ERNlIMKH%&HuXnI&_p8@aZ%A6-e(kFI8EL&8W}}bGBSX`%@>BMt=#D zs!Tmf&@i(&k0%~`9M9|N#SItUfZsoOKPDFEO}aJd9r^h2@4VRS0?62_y)@byPa_4} zyGy^sU|5ju0U+DUK0^D@6+>6b?6G1d5p#rK))@ubAxC zCe#!V)?VLd=9tz{tV-MR3F2D_IqGDB>tr`(sIH3lEgv|B^8RDcwlO4lX)GCVpny-K z(tgq&_fmH!8oE2sK<3ZQjrzAifjePQs}OE}nLZGDehN01E!C3Yc~mfeKAL1@4Gf@9 z$5&A>I&L00jkzqScEnt=Ipb)m&(D4>cv}&wIi0L@q8^^X?VfIx~xOB=I7oj3`1z2r~%;V5t*OD1 zjcYEu49!X?+}PZV*0whN{lvlyX6NU%R<(ru$oD*jy(fCGzIOnFy?uxTx8Q9akC#1X zPvTRLeN&s9yW&-paBI1x0S&5Onine=*tP}T!-Lq~-H$(f&D&5Hj2HCVf&%-WeC%HQ z!=Xn}X*B1m3kCFR!T@s()JZeSerq**94F1$wibMG>NxJZ<0Rg7*)v1Z*5f%43-hDl z;{dtq1P9Kowp_ZS)M_y}r+x6h@xmX*V7n2By!@i8G(c}&KM2vdUxWUX$0{q$I{h^b z^g@aMuv~R{NLvnjugb76J>vxf&U3re=IurRvAMa!1Md`Eb5NVvap?dnPyl!tFgC+7 z^Zast&v*dr0VLZvSpxFG*YjA*?VeL86e{p~*f>Q(1z&EF=56YlSz%=+@+R}{yP`24 zg?j5%(KpeYPQKyKBN`>+YyPg@+$_qwXwKQ^jinNXI=a^!z(eO z%|AP@G87cuYx05M0rWicl$>!yAabsEQzF63677XNk1So^2MsMVwaiD>f%ct~S^BgB z%137=5}N7S*rZmRqttsabAlluG=tOE)}hlU@U^rO%(k!Hv>m(Gtye((5S|=4jXXFv zgCJJ^#AWbIwnY4v(O&nSry21aEUj>F8r&aig?OC#w7EclwYg6x6(+#oi&~PGw<171 zPFvgC@#5F~5RM&GZKO2nv_HLBX$y>CEgnzw)9}y%RG)q^7B;WL{PF!5AD_Ui0tn;* zp1j$jsNVMX>n9K5&mOrOufOzK+_d%LsLXu$@$$LF|L3<{jSdB5X>U1tlqOwiRB`OA z26oxP3cB_jMYgn}}@4T(idl89qKR&rX9I1WUH$N^l{1J0ZzYn?N}6N`*O9kol5Co6-vDW0I}I92$G= z)Nb|W-g(Z;{AURhOrY+`BapKTCQSmq3K;1qD~jP=TFxipJ0u~n$!(f*T;_8W-nM0q zzSA%DN4ci=xYp(tl_qRbX?hz`B`~4{HnK6(dr`H}L20r|y_=4-y?kyt?RzLbXm0Tot4#ZXDh0!69S-4I|y?e zY7UgU_bI4#mFCY=5PYuSF@!)N;I9|I9>C^l@p$~(I{8wcl4thbmNu+~@88hihlyoE z%(a73L6T?yD*N(VVhA~_QjgW^+Yd z@NXfAI{W$)!d(1m!q}ODSjOKMEccx(NJA#|$bTdwz{MHM^R06Lc_P=_wlujXba$l! z%D105h&xUkv}G>%rjU6E49`*}(W1NZt6ySr1{4D_uNYl2ZRck1&%fXj4c5Q7_c4qr z4M)&yStd`0;ZbAP@u-vmnAx&wZGZ^zq-Bn|rpmXG40Vaq3k<#c#uNnLrujudvb~ZV zG|Grc44QtCl+6wX&8%+Tu^pSYZ^ynpdoVjWWf?Nl&)$KNir@IuaV%YbF=loSqx`kU zF)==lnVD$}46gG4J+xmOKDGa0?4O^&e|zFioK}qEPwsk}AM^a1sFfI?wV@Hvh?SP>f%n!8E0bZkIpwG6r z+ni~yfqYA&f2D!G&C}7khK|-ySW2sw9L1$BYReT3wxjAOZFhCqI47(T7+*1GUssh- zBzQ%K@!Ia47bXot&pe2QpSOW#a@r|Ef_e;9HAojNO@7^V!pguD!?r_%)>3j|CNDBh1enM%RYvfRId=izBTKEpUUTh-DqAeJ1F8V{>!?%%;Eh!g; z%=)bGLOGjW)tW5Ra}Uf-+9Z7Z$+!PFUjN$H=7#Fb(xTFu&nX#KybUG43IAS`aaEe`+bXHCKGWNVCh};$P{!8|J*oc) zq`&U+>+=A_ULTnn$AA9vr*U@K9M?#vO=Q*&iDUrXY0;_u-E$2-64Q6RhLB~(x;}FR zq!P%~Cc;S~j@JoAaqAaz{^vk>trmG~ZyRUeTxrhR8=H{=s@Gk1J$~iv&DxB8@z`n9 z7MG%apg{Z6X+D-38@1!6722ZYJh@A2PG589s#_ELTG*rB&}ua3Tbery_FoI60Njm1 zVf(ib6tiyw;D-m?1_V36E-a2UM?M5#Zqw)2t}Og4_VnKMWxeu8WfFz?X%NoS7Kr%H zLajNU2fQD_<^r1&QAoz|InznTN$z`|7f*{ns~0aVmknzU;MM{U4M;$kbdo{y)Q`)e zvHqN-yJ0a_F>_yC+SS=0{Mz9x-Y4#FDpI+Pmo>4Y8x%WDI zDGL{5q(C{jPtE8Bfb-SbuR6tr$TyuMjx1d+IPmo>tO)#b5jR%cPd-PMh)wd@n|5k| zPJ700KYkE5ZrT9^Gz3JV8<|T@Ya0~@vZBu|E}6wM`q~8Yj@z!7Kx6h^W>WLgdsY7AU{10;bb7+^z2;iVxSpo2761#T^$vwJ42HDdu<~(3^+=O$^9x^?7u-BH zfNJ<(=^B=eZI?$4LF@0vmNT`F1X3yZSMM&o^b)L;1jDc`wY3iR5W9D8{ zpl)V;kKThmF)@Kn&I3GHo3cR9cl8Y5!L!G8BmddKhj2~*23*s(KKdr@&vcBwL21F; zOHKH|;rq0+v6iKZb`Io(c_nLdNnv4b7VTYKIyLgvO-$qX>@+4HpT?yfUHHr;FUBt% z{igQf&D+2*DXxDk^|DNTzz!DVFi>E|jcV`yZs5cy(t`?4w+ZZ+s-dJ_l&pmUE!%s0MkIl5u=Q?9iPj#oUrjMC?ykb zNg&-a$lfZ%W@Bie(Asv8)?ItHPRfF$%+dH`)g-Xi)@ronB)FJe)S7Zxlf`wk`R68r z>to65CxS4;2`}h9KbgVq?e%9`*Q^(O>xaT=NAtps5IQ?M@Y)~!QM^w5btzEIhvbqH z6ip~?Gg)l8)r(s@iGmL8Y-`8*o_=(+)zO~uwp7=&ul!Sw-iuqFGM>n37A`8fu~f2s zYETjURw=-*LDauxC2*=XH=ucb8I6ige){oyaoeFMG*Ey3_KU#5$zcV~fAy=MQ8J(k zl;Rsp>X!p;=^Tb;#$ln^dtSA_2ABsEczRaTf-%@4@IgJWA{nOibWK?J0 zT9Ju+ICorcOR6#et->!q=OzQrdHdO4+KawTtvN@g#<1>1&%*q{V^}zNT>D~RIp}8{ zbmW@Sx>v~3h>sa(mhZIZ^a1TrSMO=xeb(5Z_n!|pw-tLa_n;OCrs_o?mt9BqmNOEu z7TSYim!BRA8B(oxdYwK)!Lt;Ydmvp$v(B$Gw_b8{f7T}45gpjKrZXgRJa+n?KP%v` zgJ3&OBR3naa84`AD1RA^f5HCheJj$;ZD|-mj2a;Mo@?qH2#vsboscV% zlriy2^e2#_i)Lba|T2Orq)Zgk=sg zrIl*LB}1F^yQ%{2E#1bfbForZZQ?8*gl4QypE`xNZn_jVY}tvYlxAO>z4O^5edC~~ zsTos>VfJN{v6*r-2Fcu%n7hStn4>EGiW;(;XB6WrYf=AGoTnD2qWtPso`;mFaKB_K z{nSy^=sa{9-wv$tIJrXG8)8lglrLE=I$0{TH=AG0oL0|4Xv8WDNp7}=_vZ>p6mqgx za@sr3O9>WNxTyxqDk7u$kRO1G`dd+gKGjYQs;-TypBfjIQCeEj!k!ZS(N+Zd^Nt1C zHYJf1OeB;zEO}wVxfhbXOPSG7;DWvp$-Ru@$)rpUK=b~fXH+NJ5`cJFbte42{Mv)M z3<>k}{B<1kUiaQEb1WK-7dm^I&Q70OG~V8AUCd@@PN`$eJNB~bzjkz z7aG4xdt4$b7F7oA*o=T(B?fmvARyD4;F_zh#_#=)58*k_z7frIoS5yys`~!y%(OOV zUvV^6rqe3kUP8j8O#?&fk7>>8F!PT2|C$2QzxlP#d9C?^tYSt1;uT?p#~whzu6f>T zpkO?d@gq`EK%M?*Y%eH5>b)lpX$NzpeI~Q;!xN+U6$Q>oAfO%HWerNhlWA)e@=HR3 zNa+QXQn$UC5=L0wWfRO;c~$eWf;MkJ^#_K6~3P#S1j*c5LeFM$7s^y-kF+i?~3i8PL&> zq(v{21I=2MnX@x+&VHM^+R?sYNVoCE<_=uZwJrw1B&e|0<~MeN=Hoa4hsgYqkizzF z=Ht0**!~s%8}A1|u(S&DiNHDipFFGghgIHXOdybn;1Dm%M6^deb8X>;JWP;~f@~pj zeOUM|{jOjR<(@@?Q&Bix>OM>JNS_syk@(N`o!wedzIvpJigQZ^fKF9 zdHv=KaFG%keB;F4C5V%91#+llHwzzg#Zs>qfX%Vd(P z(rkYGFqW^p0MnOj#^Sw)Fg`Jksp)C-^!1{tsX6ok5M5uhZX?#WcjNTZ9C59ft(1K| zc-$IqCMhFFUR>*EzO9*IV(;lFqs(d=$>L&WY6_i7i@vC|uG&G3br?n_>!p&wg9rBN z_WI3Tm*Kv3i}-`X55OJg#TQ=Be6lg=XMi-Ca~EGf@9Tn?oQ0L|=p-xZ{mHfrn`bMk zY*kc=c~Re&)E{|rU#VwSTraq?aQZ{X5RFVi5J*x?q^OgWNWhjG<}RMYfS%rv7Oh96 z+0hQEv|$8P9u<^cxzL0Y%(a);@DoYuo zNHUWL0hAJfB-U=mx=w1vv@QxYhMGs(cY!tc^?S^P87%t9EgEA@1|ZQn)^cjbRKb~* zTPVjbsz&lFRm?=90(4xxB`l|uVA!@ELo*WOM--%FX*enC>aAg1+@W9ibb}fG9 zCw@`^^sA!wsi-y_nVwQw^SPu;%r=&y^@qmgV0SOpDFJ3FWza=3FKoHy%S-t1H*VD+ znnGED#hGf^ocpcYG1E>D{pkpf9@Yr>SQ-V}+yrR#_*244C2*Nr*M;V}Wi%;`I+?uR zd*ZMLxVP?q46_UKHm(uWv?#FJqG;)iD$guAVAWvKY~yj-^DGN{(S_ZMUUPs=-RpBb znv@p5wFS!^&8Rjtm{U*s+R-@NgY1c;sL=6?$+)jsYaYQ&+Vx*0IEV{`NfgW*>EDrd z{k4~0j~g~@!w3H5pD@ygTsw zNXMCbmV`#tCd2JrYo6KHLfz2duaW`f%v#-A&)0l|LGql0lme)Wz9P0xGyc*ZJq0+E zP($5z|1P(GpD{i7LOycjS2KOfQ0e);SF zgnQonUSG8QXhbr0;}Jq2owVd7D|vQ|Nm02=CEB0ffkg#8&Hi-540g4&;zbu;hG%TO z0H1y0A+1pl;FhJurbG^aOK!UEertY2OaCGs7rZ|W|1J^sH6tEJ0twh=8hsg?)H?@p z5hEIk5P)W)LU+?an-Qv?#l7)L<7T zCnqsJHiq@xwKYRtvK^8D8abI;Sk&jOi}|G_eRd;xdD(oI zNx6JE1uI|DF1GAtm0m|nYe9B0pN?cyS;{DTM;FuinWjZoB^l_DU!GXj&OxL0aptyJ z3*qNEUDT{eytHGG63|iYx#}{rf*%?qCE*AX+?dtk1a}s6QHU5)5j8jcRuwHQDd1!n zY|{wlGzhj<)6rRK?$k&yM_`Z4<2%|=YHKwn$_hMIRiDenN%(%M)qMBrbhK$Xe4h)~ z*VV9pUU*{0itoOmIB6ZTc>Rn`m)8;63knCw}A=uS6P7OpfEs z)P!C)a{Q8?9P9)2VqJF+hLwi82_czjd)*f?5XseNLEd>l_l?Y!LCf=X)>+Qw$IFo3+l>tp%6rHJ`9vpyTTl!m>% zp$XG!-CVGq&>a|;R$!3%X9lb{WhFGUD?mQji;AKXuv<@@sXCRXtlpQd+<``=(O)?= z>f2BYiEMxCVsj^P%vqeG6fgniWarCnSV9)N;baQ+1v#f9) z6oTUdUO5Wsx|hb~*M;~&=Hsmf*g3E-`W9iIIhnJsg|)C+XfTsa#>|<$O4uV9HbDed z$d5t39Hl%9H%;aPRn(m)5KOBsTpF%S++;KWlYAmRn_TexrijZJIdrlB$TPzH8bG1u zIAV6>s>MhFd%@CNSQTDYpKlZ?qeUh74F%hihB*VB|`e(Z>3tZ@GWZA#ll ze+1GO6!YVOy3(+}`RZrliL)nh_pt-o@eePM&d;594jHwr<2P*!z>MXHb@OwK@l4=? z42k?W)aHI3T!()5*aqNeBD{k=D_saHC}43yq^Ny7EwEZIYAbDRAVOQy&6XY8F=&DM zqSB%-KJ7w`?Ou=J`;X%6*|XTPZJR#6EfXb<1^h7BH$7(BwAolc?Ck7gr7M_ASS9!) zkTQcMOu@A|DG6fcR3B4S%S&a3yUv(tvXU z>ICS>`Q0>@^K_^l5h1VKn}(oNbdYogJ(&@o8iTORE}3<1?;B9EkOswAmoYLkr3Dr) z2=gpM5fvMTk0@bs6R$AIqLLBwZxf_hy)wytBkQ zLRM-Cz_SgM53&n%x&bmQ>YSuB-{Eqz>lxdj;si#GPJ)0Q2D%sk^mcz=F= z-=p}82X5EbbfoB%8q?IT8VJ_{=oU4-qaVhn1jRPs-1!&s0uX6OSFIKR5}9U`23^fv zkvQg)Y%2#Pq)!^KXPfJjQWWTDR%_hw!2mB+_p0(e5f)9aaK_3O0SPsbz>w6r%#;3@uNyhubncQv@dJz z#pb4V+&z01dln}3-t*Qi8!-RWA%EmrW3$rumuJ0^LUT`-{?nEPir=`lYY5kO52Hna z_7s@wtJDdc1HM9d#fcCp$hGky{C5=iIq5zhgs~KB#fA5%@7G?-`r%$wM1hqtEiLV} zCdW37N}Jwjop9uY4YniPa{}aKel)wZq|bbH;gX?^`tR-|`*HWt{pM5p&N*?j%!?G^ z+EuI;G5wMimi4nt?F)igeJG!ChL>wf&(y{b1K%18xg7B3%wtq6o=O5_$i%*?7@xAz zthjStwPsJXL$lJl?YiV*bhZlIqax32AHwW64`OV53=capQBrL)?bmF7chD`|hB0*^+M zF8$Q*=U#kh*A+Ovy#ar4_(8LOoq5gIed^b1x;LKOjV8b8#1GY?-HRD?)A%S|{>>S8=;Fq75I%~?ezYEyCRXfS^>GpI$^eQlQ!3JD( zYzEhKt~1`{DRJ^-uHx-Ebqsq>9A$0l$U{uyiYbEdb$3H4iZ?wEF+Jz3pCQQvv|E*D z{eX{1@3F9?K>M7pt6Y`Z=V?LQF6Nrra6!%GR>fG0qW1H&Gkjxi1tZcj`qnY@I%1LL zGy;Gj)S@`a`rzWrD5d-dl6_G?4*eqm?Q>z7wu)GRo?x^F5(R(~p?>PWxw$zdTj@|h zyi@;Gwdt;UujYPfVis)&PC+cK=&?OLKd%K@Y0-;<KHHshoU%DYMi{$o!A zWxj7@K|XFt{BZ@!-}6)NEClAKl-7Kj32M2JG*=J_Eg;&U1Y+8nBW3LC0Rj8|(@Kj? z$EQ&sAsRU_g+If(uGPd*izm(?1%96ZgF7M--)E#tF> zPE?KMgWme4)5{!T)Igm=yP99^O7JkPK>Cy_qdx;(m9vY^gWs|qD@tpwiPOO|A^X)5{&zKNj`fKgPjrT;tS4~e(u&ii8I@Tz3m74I9p3Qh> z=MX+Ueh|}b>evN(axin@8xsX`k%y4D=Pt%YwXxYK74fO#@O_5ixfKx4?zx zUXzT#_@7Iw%v*xz#{+*ac^RX!g*>D5T^Uwc4TMNHFI}SkMIfm zPe6*S8)Pm@k78gx`h&0crtTm3#+UT(`)_y=Yg0-5Lj=_8njk2DxXCY z9Q+YD>S=33XRQU}3vKYe$!zOv>zw!R8JZ zJh(UsBN+Ce%jo-X43gKg!|ip?*i#K$S=u5hk7VnUG(}>Xm1PCeTgfxKaQoK7NM66B zv@unQa&rU5b`4|QnQ4rUj$!?V4N3#iTsU6#O^=%krRAPeAp7*vEY`PlL_O{|2c%<8 zgt}{hZpU=P+=87>nchQ8!kU`p^ht|YRdE+)W>M;F(;6O~x~i9KYS72()P!pfGybWq zG^SO55A5HonbrTg`%*mFCh?&s?v5v1qRA_pt4>zycz4e`|$Or z9#abL3LAnVQ0|mD@;QsbJKh|&*-ib)0ThApQ)51+q=}QwQ>EpXBV#Y0iTEuB1wU-fL{30}v@Zc(J zKgyfzr@`bS(?~WcSDt3YB$mk`Tdf+WDf}=AbynkpEFI*4`lJ29>iL)g;~gtCG@hP7 z%iL0wm$PysTa(V#Hf@bkvmVi-?M=dM$%*@|sEWG!GSGU->g4aG#OjQ_40K&28N@A^ z>mk}_{$Jnwi@4;%-BDb!Mj&(cvDumM?W#P0(0(k2dit@hs|OJ<_m!5#&u#eM58PqA zw5vFiGt=7IMbBfcB-aF4eP+zpYubFBo@HjwJ_+8W*)|Zg?9yo2GesL$s%R}&(NGE2 z8|AJxtaP^O?Ma#H_m*+Hj*(WJ19Mwm$2&T$FumzknnH3eH%?ycRh#wnDiC|gcI`D@ zI~J@m=V~5YS0m7F&KWnh0HuyrR8Lj?Sgp(%!AQ1}>_M+tXLYIX1se#MpEz<<`yB!5 zOex=U2B`n!;Kg`weoXz1V!GRyuhTyB&J9B-u`|wZ?A>V3I*);9k=ER6pY46nhG+3N z0wF!*HPRfEbD-?&Ac_S7?m7q1J*Y=ycAo>{E=|GnFy(hsmxJ;=XlG@$d~2aXDCOm% z`3=+s>Y)KpSMZg_7YQOMN-HUoEXvFNk^?w0WnbH8z81cnU_^eL>0pewpAv4wMkCh* zw)7Uv0sdkBU80ZoU$7+_4yeaW4nnbc8DyNLl&lyi_>dRTMsNVbG5&%vNg`pGiokh2 zz@9RyMejMo;j1E<#>T!+^N>Y^Cdp5v`Gk*S;>U<6*AO{^TGy=K-Ggao850)r3I!9n z0TvgBoW=dgi(Zd+eCh9Wln;FK%XoC`B!2t3ufo=@UgYZP`^YR5vY;*8Z@Fsr$+eUk zMplbTr0GcVB^LZrzE$d-xk#Yv8ofPLo<=jBj4U;`$YK z%*J!VnUhDr+_tmDah?aIe~n)t`woslNljSIlWNG;fOEd+NM{c37DH*7{k)cFQE8%k zy1QBXI!4!&^Ma8jJhi?RQ(Mx<~}_=gD&+AnO3LMRPR40#-L=FKynf{jC?Q* zd%+?JLpkOu?hGx7QdN3}o#J+?e)D5RJy}sq=*s+p(SVnXu%bqdOHShm%+=iHP8GXK zp25wx+M93&t+hJ1e?PYOufsoF_7eQ{^kMwtvAw3ye+JEwnLH<^osT@PRFjbW(_!JXl{huy%pBe9a2YjuAeENZz#Obw_HPS)tI^JHdjCW&8>Le zuFLS1Cm;4>DAKwD`FpUfER1FqkzdGqg%?JHPFi*c+9@5<*wXpPioy3WNq8{3XuLiX zfG@9jCeZ_2PA6?zWD-o8^ftvc6TxGLg>(fE`h50tt8r$N^kew*u*Vb+A1Fxqxo{~;xmSZ*qzeBmZk8%r@br;HL%&VL05 z-AaH-ChmTY@a-VATJ$Ge8mwh2Bv?_voJ`tzak=s%6^ij8^R$k2*xA~n_ohESKVkY; z(Gap~dUnqSZO)zp;_f;D)gI)gz!eS{+h-mG+i&=>WDcC`Z?iDZELe7C>|j7S2htPJ zoCDGPy5L(^zOW@;urk0~3#92mB;eflsm$}EoREo#8sy_~0A2K)zbnXJzjn{-#gWD9 zDG%fTZlJA0`({`RYvJ1srC*_A)(|ri9;CTzKAsDc37{HCPoDr#;2?B%V%a6ct<-Se1SHX*59$c|*Bd!?SfUg{R3d;&q@0%FGM;^XI#~}dE zKJ`peouVZV%~R5*8=fL#uu`B{E2{{vVzN4#%|2H5Erk}v_+Gndy9Vs!M2?s!+80(s zTqkd3!WOg?Ab9ZYnS?OrQq30!6P%Ux+Q7_7q=Sx>_q^H{Se>!^*M;B)F7i~|SK-0W2Wu($?Robjp>s6YVvPGpmuPQK3Oe#6W z@=PFUuSv6Q&%dr|W;g5+G$1?H5*|5r6jzFNyy)U9@YLL-J(kTkSZ<#APpz3bR5=|z zU2HC)XKqD1M51V3#nZ&MK)7)?m#H3EpFd``Cr?3k&bWR?3Gs6*=Z zX;kfhW_Bvhrho=Gp+ddhiRg}u=}}=wCbB*Pi!|+{lj?8E{Eg4(WB7Ett?yF;j($km zYrn2n{VD+-?Qd;rFm1U~vH4j#4g*pf8XBVdM0?1YAj6j_p_82UM0I7~`LV+va=0Rf$z$u0n}6*)yTAX^Pt{RP}{H$g3hy_oP{_&hV0k~#MudC zBp4xZPJ586F_NV^l+`?G^7GaRCo;Q!>bW4z(RMU;NMjEMTKT$=R?SbTK2~2}h7IbMyjW zPUh(%c}`_rK6u=;bIt61Z{4<0Z_hDW`C!^k<5t>l$nVHH*{D6RPftzzk-N4Mq)*TP!(*91k!Y7P&0V#Em* z*WTvFBT}Nq8INM#Atw>9LmDFxle-rzHJpQMhhMxTr^L~3c;J%~F*xM_oil6_br__2 z4FD7f9)Q)^EaaG+BH%3I=w72yl;?Q`nQxCM2w3JU^Pn>RTqgY@;*xlR2&^Xjq;PC@ z0x$jhkKpKx^#rW`-`drO_ucR!dsce!7>??j5Z4sYHK=+tYApxrR~gsUl*@ceXRXgE zP2A#2IX49FJhBgW9^MDZMu-F=jhF$8(zG)bDHGPSdM%XPzH?^9PGec~X-MDJt_)5a z9%}(YI>B+1umIheD8k=I4VXBhi|e77Jkpr8cc7uIm31cZO2NU^ecQWmx~mDDM@Def zH;>?w%Pz$=N<+1N!?0rJOS%4|@WsPV;D7Ib5HH5_1U)>0a(>HbTpCP zyJwxp3G30CyseYR|Aoax!w9K_uikkP?kLSGVZi|$nVX6i{VZV=V8#HBE1VhY>zuoBua(AKD3C52 zl%}t@!#vkO-9}JCj7B9aYNURpbFoPPK<6;8R0_%>{ILs&U|y`o}=`6p7MYZoqF zC+&o=aM6t0QmU~il_ru*&ubkmdK(5B8$vlLU3x~-aBfl8sWdTT&aGcwv3W|feL>2C z<_-^nfK`U2Qe8JFI@#6M5!b=x@9IqszG_4x7p>5zPWn=^wNOUwlW&WH+jOiI^iQ-9 zuyIF$fce{A|3-X{~47K63y?7ILl}^h?$QgS~xLkPGA({(6C>d++`y z@i+I~j+4S{(;=&)WqXX7^N#M$i*}y0;m*3Yf<6y|SbXxF-cif^QGu1NHY~31)eiph z=Xlq=!?pJDSu`xIU}=0xd-iw0O?ziAF%VY@&-JeY=W`ctF+MK%UURCeJzYRLO*z{? z4t9K*v>Nf|vU+~xG^QWgt6RL()`Ec-+@Q-o+ua&8@$9IzRwXrVQk_qG$uDg;LSEs< zCzUr^Bje%1X(faMx8`l$!JB=~K(~IvJd$TQFYUtersjJo;yu|hh|j#xMcDKukHga~ z_+Hq2P!z`$E(j8&3}edGA2AT;_vZ8P47P59w;liG>xUcnMbG)Xs5ghs_0D(bdCa}E zJm-qXpQ%7c^^!UJngh7C@NX4FMX7`W;B$+Qs0W*Y6kZEH57PJb?&AeL>|b+F01mbM z9K`CyF^jzgH|H@&BF0dlsCq794jlhmg@m<<8IGcf&cG!Rf|AIPf^(j>+b>&nU=Qai zP|SSudIR{D>+Uqt?wY;GwRudZxlqIv0{Fz9`|yE#zNCQqgingh-M{do@4^=KTRd)& zUj%F&9dqaxV?aCdDA`4($n=$u0s5EBqRC;q zGk^)E9LN_D>9BK+fV2YEK>%pJmF>+Y6??Wb0c=W#%AlEvL~rI614x&$PJfw}NS?`p zdZ2Ke*$2hf@ZH1iC4S_lSK>WC^$vY}8V|J3{P5^m4b0QlthWX>sxh%?Uh*BV@tr|49|%8Ry| zd6PJRa~kpzo${c@b5*sSw!k7`um$eIAG>DCLZ?`GBqi9pks|MbFYE__JLt z7L?bF^Ei9mrYLihTAyChvjvy7^>{ky+C5To06BgqfLuTz0h;_73iggO^KK7}?HCXo z;0g}#^Z7X5w;p)cBt9!XPdrXXtR9D_TkvfT)br=UJ!+AnnoHw=f=S(2KY z8zRqGcu-fAEek1AANG2K0Y(|hC-Zy1qne6y19x5=xyt*9KrliOG@sDgzj~m?Cu7T{ zfjpo!z7?j)DE=tq#R=>A0CNY%i+*hczo<#UIW5=!jHwqQC-|XQZ&KRw8#nF12fp!T z{EY(Z6iBOn>7W0mdUq|}f5S^OP!FlvcJ+fuB-M@UOL4U$IY6JEEu8Lbn#WVar=e%>j&@F`_tVJbM^7-jNKP#08|D)8mJH4VCG}n zb|WzbEfF|RTHd?PU`*61hUEVuuwp`)M>^ewJT|iYT9%X zo-v`91m20stkSJE?K7T^Cl=-~d0;;-q4U!(e<^9xC+!~x8PQ!&my@~UddnBh(wRhdX720pSpJoa=p{K> zi?(OTx=EO^v$BHP_%te0b7*d2j(j-%g|A2q4t0KNjI_6e2d-4ou43+k8Z({efyNDO zy^sZ~Lj*PvRfcPwO%F z!R+*OII~;}{hU7xtB}x+;;chBS`W09k3H`^GnPHwSreM9x61OmZY&MDWQ*W5eF_Mr>ihnaH{M~TZ)SevvTi;~`NzqDh$5_)A`Qyxs$B^pSuMcVM=r_jG) zJz6^3G27R!LA8iHxjPNI49;9^yVq8XFiQ^~2Kt5mMq0limhd6AZt=p!1f09|#MG6Y zBX0SYdVm!|)Q#uj4#ezMc+8XPqbDTg1RT zq-k2vZxdv3c{!qx&X4nXm+){dufn+BjsU=~Lu_sqrEA|2Fkf>3w-)|AL+MxD9&`~* z(?yxi<{9+vzZOhsToU;r*yNH|OWfakK5mypU zSWyImIiYj+5tu(maX2f+e+x2-hyxK|lF~IW^P}?(!hT<}QeA%Xb&k2o6+@g?eu9mV z(0A^tPN(@XAfv|d>ndj(?Lyn#tbp$Ym*dTsU8m1meQflke*f6$DSYIiuj}t_+`O~6 zkO{%%drmp?s@Ht`>)gG#c1W2tR|8^7+LU&du2&c%N_&y6-@HQu_Oo*{SSGC%YZ=Wv zScXfmy81cD7GvIQdM~t;%#1F8{w!J8!ghlI%7rV142zvvplV*IAy~6T{J;IywGS}? zWI^Dy;nJpU3q-Blu0Z^;-X_c|My!ADNxh%Cr>7g8U0qrOe?GLOUhw^A@AuD7;AV|e=Fz(Er1#ptsjClt3UHb>l~|aXMvcsU-FT(F=u{r^tS*cOkUaL1hDg`f_C?K& zT_JpX(fR623h=n|%pEMEbm*MqQ>RbihX%IeIXf=0`_mU)W3rmSfa9(K7$3$|#s9K$e2d}y#tPHU%iJp;sE>)MZ)hk*Onb6uM0vU~ zM;buk{+#xlKrdN9tW@>0%>IUWEB-#DCQ#fEywjv#1LJ;_QOSHt;UE8zvsm-=!g}=% zNlBlIK%nDpIpS*p2?t5=OZ&}(Ktg!H@9C?0M%#yoE~diXfX`(pxd?3@fD_{HP$x*4 zXFY7OXm9yPf9H4ca|)dEedf*q+~JYa-kiOtCqy!q2YY(4WpF(@+uDmFqYz*up#9@t z`x4&$(ci-zdmd3kW<>$>W(CmA`ZaRqtOCTrD$tW0OLtQ3q$!*9@taiN_BOR@T1#5< zh3&&A_jh`MQ_z-6w(p!hBQU7d`Y(U6YZ*P8&dgnC!X33 z&~6@|#oU8WVC2wIl$Vw{$TBqUbLRR*qTp z91H;4_22F%*Zb1#{_Z$kR~B=hiGsc3StyJ1ec(WPls{niiL-s^i1UL$9N%LCTW`Cq zBFnO<4ZODBj`70rU-VlvcT#~DPb*C?oqt8&r{B7`d|vYVBp>1FK4Kk&SUu73J}?#OwOL5GmNge6#|*irQCV^^ffNC!`^KZ9tyUNQ zJpzHM2gXzIwpy~Hes(UgsNC9&_}c*H`8+_vi1P{g<(TBv+CIn6 z8$k@1=RG`f@-i>7*g8cD~2~~5O7RsX&jo;ZIiaIC{Av-q*imtlSX0KPeKij_PDQyPeqSu<(UtH$rX?h0~l zMy8|Iv$CkyYFyLG`5z}cK>%YmI)>(xJ=p61wo)TTmgceGJmVJzHg<~`sH99$l?A`y zNs86)!RO*RXp>0kS6p|!U4;6ml*QRO5F&jt?0s0fxCuW8xkV$D3!F`i0CD2X;RhVI z5cAK|^0*h$56PrSNgpEFy1IE^g|Do%TA zD|QTT)MvwIMa+CrS&}%k&-^XF|6BON{r4E5tst3PJGSoL1)=ET-1Lmrpa(K45C9Xv z88!Fb(O&wWEWr9&+I72E6dhSm8tp1MQ`4I90$?sJh*q?@X`zgk=>@dTEumRmyCc{L z0GFp{&9xT*`hRq2P(XWm+pwXFY!5koTT+6l_OpsE9zTN>MI*KOJIs7(9~#hG@q{&L z7l{{l4bT_M8lc;1Yk*E+na;Z;U|%)g2rCev#|^3-2%K+j?(pBa68qWdc^-IxX~1tC z=V^#RZUmqcM{Vw`aTCuuO>@rzD5vWz=M*kupxS|V?zBxH1;wdTHxJG&nDpf;atxO| z%Uu2di=V}9AYuR?IYe}0-;IADe$Ic>XGI$H)zV7;IN&yeEVmBj@~u}c;&aGSQfbUL ztT}*N3;&lwu@`sP@r{_6r6K+%zYo;Rk>W)kj2$Tg=iDsXm4(L-xFK(j=XhHmCz^aH zzK#kJPh=-abk9Wk?pqApoH}_Cied?!V`~1b9ZyhdCB0YxeBW|iVzydBEX4iB+6f{b zUl!ljqrskAPRd0f{AaFt4z_mpX)QYK{hlr_;qwO`*L&6p#P>L}W}j365e9M#mK(or zAHQCxY$0X*VN^Z(md+g}jVc}K=ITOyxwU5iSFGQxz0#9Ly{IgLl~H8Qy2Ze4E~!N!H|i<);6hZhh4FOQlhrnL~Me5@J$mhZNu^EmKu# zhiJcMMS<%jbw8(o`J4jn#3(hZv`&wYVQ^XEr!RRr+B!P%FadQh072*%rO8LT+1Dzp zHr+ACzRam$P%?^{Fwj@3`EquC2L7o|$Go3y6u4lzqP@oZ!C--Bdt(!h%uPnwh}@kl zTr3;|XPb_-a+uaW4Z#y$g{kr>6y)bAj_~)nsDi!o$yR$Yt>#za>koH4dX!HEXU;jq@$^ZCWJokoYqPaPk-Y8~0MZV*G=y?Tt~*2vYshYbYG-}l#lf)9WGGx{hx%7}pZ+h6=j{J*b% zE53Sizm9kI9Kn~CVWR3s!qRhZS+g?ADG{Mq!+Wf324;Lp} zr5ptS+&%-#&zPz^@DH!hadEu1oOyWX*%J5oA{kKDHxfIO!{MKDG=36Cv`#4PSc4EH&#k*2l8h$RvT?J+L0T z*A3(FqHZwSBl}L)gFc~lwJ;eB zldhf6DL~xbZpMVjqJF3H5oF(AM3hgf~^3 zK63_JRI)#I$@O?}ehgFPrF^AG6+{Ap?slV9S}OZ~2w>j-lK>a#f1bBxC+_)Azlg6N z+pp(@DHHsDRN6mJj_CRpgDJ9`#~o_S?wg%J&H-Es*ov5{D1QC>=()Q}XspQHtxh4m z5I4J3EA^d9R z%SHgJ#=vU29-Fcx8QZkY+2F=MN<>3lnQSF8I|m&|_iH(d`Zl>uyRWd3&38|~=34yS zAO9&{@q;gpfH@ugc4X|V_RhX+_c7ao#Hlda-lRa@aNhvdt3T3A=Kw(bVZL1I895UD zwNL$pb^>=&3FlmWuHStLKK8S}j2B*YE$%*X5D%X@rUiL()6;H2@V{YANXjf+G;`K{ zf2VQCxxBs)OXQzI$~xejfO!S23Y52v&Y*#|WvIBcRcrjvQOq1VhWWE z&O1RQi%8?TKBYY$VrK1Czdt-v^s#kv9#c;p(jW9HaNT?14m7E6YU{d9lU4JM=+bK5Nf<-`eN%HMwluf_({(pKQxG1T zt7p-1Wfc7JX_@fr$?9no0ct~o+>vGJd$4qnmWRj5*imcj8X^Vmh>u{4gb0Y7(dqiJ zcJXwd&xd-zJ2!p{z7fedFM3ul-J&=-jSuqaC*QOxK)$}c6TL0<9oephweas18r*RW z-uN}gn7akm+$C!y;$_vD0TvD994iyjW7<0pjB{KY+n)vo3Yje&XGgtxxJZhbZ~;1= zWRC=6)mVAF#=$ZjGBJeQt+N4_-E_FO) zLIju-u2o3RS{r9@ay1T|HAKYg*l{FTyZgKg8DA6V=(Uf&_zl`w+%MmDv(}>12cLN2 zK79V*<9OHAH{yNIe2K50)9^qBVL(xTx-!t6TKW~Rs_4b4Bt;lAMN(p06#{N_cJM-Z z8H>xyK1~9O?|Q}y@aUQ2`1+BjFr}EdaP*5A-@;yN$2Nwm_KoDqTGIPSYvv><>%T`> zL4w;$ACI&!k33+vMK*sX->MhVn0QjeeW0DD$*|vaacK$dN@LvE+!Vahi(rCJ;lj~H zj4F-M#HL=HJ#ZF1r%z#Ucu1R}cBsGA7Q%uloC~wn6&%o-UMj3&(x&Fk*i>Vj1S3?Z z1qGr>D!#n1Xz5-M%!R%boR7Zl-Hs*NmrhI{@sz~dRMdB) z3J4RUGt}8_ggfd#(ng)!e-J(E2K3*A(o9V}_z*s{>na?-poBj-aDOvZ}k1vF5}4R%E3+e`q6!+iKx!jdpmu7s#R&x$BpAb z+Fxv#M3cycoqgCdJ&G^_T_#qrVduX{-ib)~hgIjWnfLKHbr_|1`sdTu^P=Wfwlwv; zz)-TeMDLd?Mqp+gx5LiHI2}eOB$9IZ@$S|LM00?zlREE+rsio6d@jBc3GFp)Tnm|n z2(qBu2&Y%1+0cIS6}vZ{&owWkmi0Stv0T<4k>yZsO_cGrm{uh$U0hyb=V}A-!m#>U zS{PK?+5y~8z5~xtzdVotVPs}X{Y`6OY2Y;w2p?zUEnOuCaP(Ia1;WD=Fcg4*nT2_L z{9pbVAHVINF*85!pI?9BC3yGCUxVv+UF7q-?a-6@cW!1@ZynInmdpnT-mVf?S# z?zZ0AweDIaUp3=YJBTyob>l6AX}?-;XMuw^^Or3fVMt4F4?3>gfSS^>&owEHJ3E5S zgYszqxkQtqPmPMsEssq_!?~(xARV{H!MXDQFWu2?p_fD7h!}Y5$3=Uv_47nHm43^? zseR_O(ZbSQ4~QKh0|(mteUhfXNBoZE!}HBp9eljV@mpNK_<7j>9@2DW%R+jr9RAyL z1vl>7GT;%-99IM8Gf%G6e*M?HEJDZOTe2xR>(g7hmswMk& zQ3)9cpa-BRJySyW{c{!c&sQ+k-he|_Zo%N~2XOq@arE}}>OJXA+SEPnleE_hmR%q2 zoIDAK=I9uo5&_xF_^j7Qp{PpDD}YW1snIc0DOCcy_X0fiO?&}8* z?A7V~*9)(}eZ%wk=z#|iG5)R-siQlUMyRXZcuA(?xk%ej8nB9SR_3(gUeKe``Rr2< z;kL`4fq&ZjP?)duqnUiuUt_H$j5G@jC@s3SXfOx#YLE82+Iu}f_wAMd)f};vGNyz4 zhIg=J^-EvKI(=@(6)c@jX;ezDA{rEpVK6*vE}V9iv~Wdh3>Uyb#uiqb5*i^`O4m&z za5MM9;MX14aRA>QJj1AbRxrUT9l(P6bd3s9g4AH|See#9$ZDp&C(63RG#dQ|K&r>?X{J*^Cz4(!r-;~Rb zK>2AUDC4H}MM78}dOA9_#yr|ymd?Xa*l^9B-+a&Q`0y?NppQ>;VPpR~1hyPxqw47Qtd;0d7XS}?x&dc8GF0uX#8atX%?duF@ zkvsEt2bAq1(y?nRmRcK8X24vqUun-mCy?D(m8c#$h1o+Vyw~=kU^MLk;70-pz_0Th zTnalc>H+Rxp_beWk&DOE56=;O<9M0?vg3RGKFg=Y!My{_f`M4Xk$@`=Mf0TSz3)txCeOpYOVe}DTwFe{#VXX53*=b+$um1?%-6zN z_>T^eR~HSn1#?bT$aKUZzW@hTnlIi#YA;y4lr838=dU@zG)zwn*d2owCFv7MCbeU9 zL=3`p6mtl*PZJI>8cwH>`HL78MkH3Ntt)?~LGi!dwD;dC5RqzDnnHIhTP+}DtfoB) zz}(i97;_Q#S7G0<2M(OO5D9)9gX$a8wR3owwlN*cMt>i9@ayW&n7SWRVEw1R^mq89 zhi=CwU-{$MY>#ErIfWLv!}--lUV_u6)d>Ux$Nu`Zrk30`JPtn-#LIbrA6=U??qdC z2m1T_wW+PhmAtSubI*VItT({NiB z-wrZu2@bzyt`q`wz`0Jh=7E1&1D1hrgrF!)+RI0#;c7h8i;^va4DH~eXEy%~#$UsV z6889c?|5LawLB?aW{W_YbozCg^r+orpd*wX0r_Q$&r9n+5V<iV7badctuYCi4 z{B>{8Ae{#~+U>~Lh&?%@IHWY69KfwpYeIWVi;EC{zp zENmSNpxC8lP1;Fd*?xLT1KD*-OZc-2m|r@yF$b~?KsRe(Rx%oQ?PyFIK<-X@(6mp{ zg{-jwE98WZgK`GesSe8um@hY$Ou1`8%kJXJn)$Xt)rqv1OUI96(Sq}IJ&2SU+)BWm zgWBqudV6i|dyq`9;@7Ai4nf=H`qjAkIb4>ddb?ObiM`y>hVnP#p=B z_o=^sYkyB`VJ&>Sp&@@#;~M}8*;?L@95)H)~i{$kF= z#9A{MfpfmlxM${8#Qh}$Q*IUtnb3L8c#DW3wGl#G%j`yDkX`@GHP6LOJ1@rvls5en z_WX3(r+(?5{if2SU#B(d4ye2Ff3CoftRi;0EUWq~>_)eb+SN_)vWJq9`DAxnhh`p~ z7U@iF8Nd1Hzu?2y--OGDx8So%(>b{~ug6JX_qjC*&j*O?U(~Zo)o9P_X^Uj^FB8bw zXhs<)qh@zq7`~$1jFeq7S@3QV@s+{s@g&%V{N>pwF_e_OEI0} zOjE)PrSp-`8{dP{Q*{6gWb#SO(hTYHQ5eUM*A4!Sn}(sx9NcY6X*Lqt52I{5X;G2gI4JQ;-XeBBeaXrhs< zOez`-8q1u}w-PaJ_ibWNl31~zm_Rx&gJJMpLm4`#eK#nOE_m6C+6QcyslOk7*-iMb z@A-usFee9aoBzKf-PuXKZv=#hp>3;R6GYbEvQx#({^iCpYanJ0)Q4G zIU8IsjWys+#Xawu#qnKSVc+?Cb74Tf08)WKmN6j<^M)P+abC{AOPOaGih#H;yAY9y zdgghA1oVS}Z}uKG51)4QZO9`EMc|vYWiFn(PUEL>V`ea)W*Vp7yK|JQTPaKER2OYs zw-(mIcTgz(nhHfPIB{b9)VCZ2=fFhDCJ7WV+3tA?=<@Mg9w{L}%;2R#vhqk)49Dz< zkS+(Lu1sluqKLp0Jo8xzW6u8X?EvoIA_%+KlB%SD(G`BGD z{&K>ZLiW0#ZAD?(2uw&Bd&v;veX zVS!8*r?jlXqR7RSvduQY8Oi5`15xbPLmtNfrg7i|Cy4T|T82&sVzfHjbw;jz+%alX z-OYG%XFsO;J5gCuAW$)W<9+QI*mp`ZJMA6q3Z%E?j%`AcUktv?0Q$_C19)U+#8nPG zj~^wkP};#zi((>@x-ZP44j&NcC?9p$yr>_Vg)Ugn%`pK)U<7? z9gi%{1c8SA-nA48KoBd9tyj3_rqZk{3%&yK3}Ui|$lNM|x`AUZ`yU@uTu|p6rk?OYH*Pa) zYV84u2%elBkjWBFU}l|Lk!1)arLDx+kx$aawE}AAP|L;lb0uQx!{yVjz6QVZUw&0< z&A9_OqCuypCUAUWR6B!nZ6#OfDJs`cZ$CB-3~Fa^zOjma`8A`#GfE)$u`hoX@BWkD z!^w#;KmTZ-`5*oCFW}YBc#alA)CCpl_sa(!$I00#Or0Ik=Y#V#QjeM}q472kYNy`b z9=$b)Jhrd&I&bKX9vHydFu#oUGv*YAiM?}8bxX02#cK)Tl59<T=fGW9ukO+wrAA8l#+52o4xhlpBYRO@ zR03YLevnq4{#w?r(|gTVlzouZGHvm3ntSs2uKKjCrwdJO#;RybOD8U9 z?e-qZA!4m_7}xA<@ho^%2N1>g-)F(&xnW~W8fNZfxw%5mOS$9Q;3xJ%JMFuBQCdt)Vv!Xzxyuq5YpKptPrvxf*$pQ(PBbsruo(19f_T#kvhhL2p*D5P0ig z_KZSR|CthSlXD6+D;(mVEh;AJp~W$!g|FcjrA@wL;v_b=b>Xtk0qscc;&tow+3H8N z!!coV-b5Ccmm$ep4H%t&Bc&0uut_F*TYvbiC0TLnzBOK^=f)oePU7J-E zbKFLrghgl<%InDHZtUGTp!a~6kDh{@nMbvwRhvdGsS0$Q7}xF9(cXz>rO7g2*b2%R zvmiM*w6x;P@;v_d*rS-QTFu?)G+GryDi!c(80yz&`?|6$&dy_bY6jh8YRdTnA(#Lo zXdfKiYE~L<0?}G=&A`3;mb7r>_-g#jh*4-u&V<1;V+3v&6oBvV>GgRN(?%e@sk=*S z;st}4l-H@TF*Hpr;HHfiqOZRX508iAhVo3bOaPwk2$jxmHI|;l62#o1*}KUEPtiD4 z*S)jlki6xGNTWVit6-+!Omt^0BI1XBUZkX%f5V={^(tcGq9CKR;+X)gKvKUYlT>56 z<$!rn5bTeF^X1^dsC(zNh-tP0DUEV;(9=gC7W@?z0!WG-D5I@JLAw+XQx(MtgeW)x zF&7-^uChST3zwXSYL}D{5%XSRUY{G-xyd>ntX5s+*-#1^s*?NOwnvEpc%mcDQEEgx z)q_pz*W;tV{UN;PCw@v@Z}90k2XIHnM=-a%6m^4$S}g?1Z=kCOn-wr`Yi&g#fba>) z_&pPfeE17DlH0;@u1uTaSP6!mRvCH zx^@7k(yJUgftknlVR_PM_vwsuy6)J#K3F((cWOXRp*%LJOVPSzJu)TGr1m1mo3uB+ zg)FcX4b{h4s&VwZo{e~R_jIeo9sTW8l!z>wMF;COuScHsb;&yRH8r1ON2`}CTJBwbN2 z&7$vC3(0#D%5PLW)j(T^TCdl%=4;_QH=GloQ-~LF8|(rVkNuP}5(Q~-jzh-JBZf%= z3G?J6f^Ln-F;^VGC%NXb<@fg2??p$X`^af{QsR9+AprC9i~pS(j5b{a58XESHj<%_ zUE7^A6!4Zp1n?+?>%Vj%NInyBa&=qUE*f-x`a7?Fj*fX;JCR$`f43ZXOoR1jZ@xf- z_0HX>3 zdIoUS@Mf(^KdUsbt^{tTNH@-HdB|Cl(Q3mBnBr!zAl{hNjjQ>v&3NV19Ca7nYBoM)}xj)U;!>6@5gR=xWDwZ#zm#L((!ahejog zpyS#|I~D{4t1tyBeCF%{+%t9B6j?QV$I-KBoLNNqf{mED@iJYPmXRr)&V`F=t-PF|?n58U~fiQ5BVwR^TC$X$FYNhm8933xcFKB;rhpnUU zH>9vR^W$2ZuEWBz z?|0w4>TkJGX%?r;{`3G>2I|iyMbAzw&&O%o`R^IaDBl=FN2`M@D7;A0W#95Lo^KGS zw{1m6G(M%z(JH0KxH0qg8W+iERQe&CKfD}1KO04a%ITPyn|pEQ1?i($IFQ75VmF$j zWZ{uzLD-mC!Ci>~7F|lFWed*%t_g$);2a4!2w)%!^v;5Gm#%9ab568r7Xno!LC3V! zytb397rHt+@k{S~H~!!^J`e$O3S^B!z|twuPeoCGS%VRRPL7P= zmc5UlsjnM@O0)Pcci*W5tt$bT^D)Opmac|+)~vvEwXNA}vITeAX3sWv*RrBC`n0`g zau(C~KaQo5aiej!p}DUI{a0M1b6A*|!d3kn)H>LT(Um1M4fHBtzCn9%Z&0AUqyRZt zE79o#?TR)xG&SlpuI(JeYt~(Wt*u?T^TD0vNOUfE0#4nrY5be~QD^WP=*(%x?R^Bg zvpD^Xtpo0S^Jt!BF$mW^&Ev2#fzLI+)|j;^ox@qGowsd}vGd;}jXNLkY^+AlqA`&? zOXp6cER*%$)yuD5x;%dR%;rP&>T7Ms#?GEKV7?ZE zj%9I-Kg$VxU$Hbz4~z>O5HZFeZozr1#5Q){Ns+6#l$;~kq?qf{mu1_3W&eLou58x&QCu!JBg1x zaJzOWcg5hQ98ix&vtZ8EWWHREzp*Ev1#oWPA6lBhNd|Uq89Sl@`m&0A+tew%xPJ?} z_5NvcB-en8lm>KLZ@<>2(>`j5RKa6fP9I@}WD)cwim$wrIL3S#-rvqk!Qy*Cm|%0q zoZ280tx9WWhrT_yy%(nj+OW9qFjgj}4bU5(M&-b9$TJEoZWGw!*LXaNQRph=as-k;7GX~#_KRuk5wSA9cfxP3~C z=k_b=8me!rD-|UO=+Nm@>F6rjFYmN5>O0!6d-}|2yr^#rZrpy60^AeD=cj|s9ouxf zy85th#+-o;#ZU{??wDN&&B-h&n#)zRE}1>p5<#fbk~QGi@|*`hp#iP9NQlVqhhIBv z8lo;aw7?-^viNJ6JkARs>IHGG2r`Mwtuzp(kh2uw`&(Db(WUL^8Q>@F1hdm0dS<&aY zNA_aw;4$yW&4um@wxaK{3$<2!VR8ym(ZYewZoG0}J2p3U;Ka%t%BtKgYRwSj-+%QQk2#q<{OBww$!)?Q6%Tt%dKR(133v(6FZknFBPLi(#2O&uqmE zT%O>I=$(w1G|TiAK1b2IkuY@yacp*xcANte#?VBXT$@&16L%>YvHR{bWk26QHEdFg z-%_=5&bR@1OZNZ)^)ZIqwy$GeB^yIY=CB8HSse3r5Vh(D~^vc_ZF;!;A6G zfBq!yIB`J7A(QtzjvvIEF1t>@<7qmOZ;ps|s5P!rXan=FaR5&H&`ka#))MHJSBY)w9>C8%{e^h=>~VbU@Lo(VE!eTE?W(a-scOxW z2UXx8+?0jSB3C{tzB}?LJe9$MTGp={cQQxu=!|hq z8>W#Y7xKZq5YN5})y5JYxoRVtA3BM~Lx(gt&q`a+-ri;z`Go0J;q>Ahe&gUbLr0VL z{Gif8DaLQ{!VTKkBic&B>WmPg@7;R68W#uurHR1uR)|M3WPH8kKX4LS00#6(}0&xuZot;!d*ME?vqi=&g5rS{ifqs=->lf0?99fy}@L)(Qx63lG*7~z1I zEd$*8CeVLI+xd{Q=@!p26W{Ga=T6_k7|ws`1x+IHGm!-Dd7*5N=kt5gW&C|u7iC21 zV#j(RVpK)csTe~0$<@{%*7gzIlh76z4%BCYi3_bJUJG8y`b8JQ9KGhrruu7CECW%M z8uK^de)Z8?Zmj96uq{lti#1=@W0*AMzWlgj&;7WGp;avGyg9m=&iY0Z0f?@;--VqR&@R}^h@X&k0gCx&(^@T~+P zgBRnj*|T_XehkZv;d>HdzqD^FwzdQdC&Bu|0p`qtb2l%&##zLIDK12E@$~mjXu`j- z_aaRr6OoWb0098g9Axj(GN<0teH;|Wch5MG7p(4Sx?uN`r<;ydQI8_Bc;CuO z^PmW%r{A+Uh8~K-G-#jshK{Z^2XJfQyE1%B8uT=9Epx$JAIAj7Ax6Kl;F;$qk{ALJ zj<-L#>}B$hUq{kAcmn2`3de~!W?>a@9xVViWFqr>wa-DezUzalf9)JVIRfXQ(SZc8 z%fO@MSm^NU`Y(==j-q1b-E)9BM#{w}O*wgW->LvRIgcZ)I{n>wob68^+!&RN{;Gnr z&vk;$*I-DAMWrR#qcoqDr2TH0J&Ab&hGcrE{*G4`F(+zx@xWGHGj0kvO#9OZ*6TFM z!H}!Tiqbj|BkTDm5q6uKGCQYeG6p5mSpzsgBUv}e7+6W>m%-jUz~$mo)Pq-V#nRL? zmQRi9Z&&sogPd82855;ZAx#N}`moZF;p{*=I*yGZTVB!H5%LI6Ouf_5lQ(QSFZiM9 z5&YJX`!TY-;5($WSVrfbqnIu!LB;qKvbGkKls1|)H>Ba2RnIHm+@qs6C{1u*XJ2EhwIw2(lAIe2<}9D~aeKYiH^XzS=G-k)x-cXamQAZgIuJfzo3D5Q^& zn+@$q&&!P&1}2v@Aa?+lZ*MrevdHoX<&S1TGQNdCOmi8%HS?Q)i#QZY>*Dw^y-D)r zU#%9m;MWU??Y#k;_Gm>*N8+M0NN+lhRWmPS@GZ#Jr=sBI*{XptuW2By9L*^YR;2Xi zyaB{yfw|APmM`p8b#vcn`NCKf?t2ky_8f2^`a*41@_YG3|v#t34R;InP0AMG25(6wU= zx_50u+weMVwbRhrtj*qqqPNaTTZ5vZZ9@a-*f@;Y6Q_-U(p19#v~1s~k9Z?fcAq~3 z=cG;VP~d)Q??F_Tga3+5(XyUh+tGW;PJK@OqFO7ewEc;JeDd>hm0EXOvXahmi_(%` z)YglGO1Ql&tJ<9XwSzm++t`w`+~8x|2&EE@b9C=ayt!jHuyGx0sxv<~>^2R7U>2VR z9+>9`wial4dQ#r>8zjH!pP7?v7@+1bI!*rpAUS@7QKrHeLgk8WLp-i>q8xcJ=(hxe+YYsz}2v3rbT#0$(!0Y&3uOFHL9-!S!83+MKaQ z-`QrR{pq{A&Y0xl+JA={cEyW6!U{Y+mg+K-H&wy><3 zb2{#+tN=S{oyX^9@$r#;_}G~zjZ-*B7*tuh9zKLQ)mH|Y{}l#kE_fQnn7T3{XWe!7Idk+VW^KLa@DtLIlvR)6I0f z+B(|w(L!zzeN!l+{N7LMk+U)%dHZ2MK_@z%*<7*64 z<;AG4C~WWS#i7}W=$Q`92DFKVr5q4$Untw7@6 z$b~>51L)uTf7m0zJ{NU9<8&qeU3kVRkI22N>iH{}clQEHC5u+Qu;3W^S?bk2ZhvE; zlgz!ethTQFcuLbeZ1CD2(!56d#}NyWD0rX(x5fhw)a(DU%%|yrO%{%)asVcn47)U~ z@FEL)(&?KG!^8ObpL_@2`%~}KT60co&MIJjY2u9R z0y;#!kCJub<6rv{-uo0;p(7scb&Dur{=^v%(8-*g_L`RzeOpk1%`*8BVCSg&W7rgp+&nmpb&a7f zTT2ZXY;IFp^-gSR=|E3ov!@p#ar$ORGz08xtc!Fzh8^pC42SzH_Ok9x$L;%ckGS)O z?oHL>Yr#CmM<*k=d6byAN8Z?@Un6)(cb=LaaJ}Qrc6PR#Dz* zJf2UzXMo;YKGfcc!M0Aby_dlES)7A4kj~~G0 zANy~3<7L<5{m*=fK0m!gNeEVO{wtAV$__717lLygn(aQmq*DLi#2!4Wa~PL4ccWQp zO)3gtR+g7NXd>7uUv3W zw47#stg{gdV+xcjU@glPR24usK*W|OnK#*;k`u1m{GqehO3RAN4!_CCF|tTaCrEeOzzTV|akWmbbk0tyQbs@A)E zrpi-QzY9w9PW$bp@ehwhGNrfWm>nHceZ7M2LB)(K&4AN8N(IQ9TACH}Y`kE@ZfT(N z)2AMK1i!QE8XWH^;ScxUiz9PWzNyH&zc;Ngop0-?F336E+!CY`4hY-Xqg_oH9G};H zo9SUm%%U;d?^8_iOf~W5Zs(g!nKB@>XA#!a&(oJh5!2T$?L-?ci!yYSNapg3fh`vn zhp)I`*3k(vcMp5AldR#UKVQ$7pv>3Zw2{cwjaN|=Evo)|E^L&@RoSmyBoL}v0S`TK zLgkta+q?9&t%Tk-Ls*=~nFwpK_Rx-CQ9(hQ$Cr!;c4vXZ>8Wo4bzM>PkH#%IfP3vt zuf*$K@k8ir?}(~T9@$T@Bh#p6HAhO5rf_ZArUa+!6>uh}VL{Rux3zT7IHS+TDeW`= z;Aj2@Po6mHzacI8Tb}n~y!)lEMj?>#1(5}7KurI0>yZjqt>20}9=%_;d-vcv%#2TH zuqt%h*Nh;L+OQV-gPd^cc5iD*&OLX(Rh|4|PaB$Nm(*vCy5IFTDm}7RBI~aTj8&Hh z*3!_3ng!b=gs+lQOVze5{XJ;z?Z(o?l-|~$z4TL6NptRwRI75waMFYBiV`Apkb}3y zWwdP=)JIA#DO$Iz{@mH+;p}v4Ek=2({vTPM$7St(7W|s|PUh?!5Su0!5$Mz$COg|3 zDS&1D#zLM|4fGu8Pud*_uBQM!og3`$VvnTztE!}-u)BZD0doC6gXH_k%5X)1A)8%;u&}UFD{LJhgjMU039UD~HHaE24 zABS$hmQt%>{0VRpP%oFGoC$Y)qARX;9jwp_X>T*tn*)pGEif zw{7zV^j&qaW`2V+rW#hrGGJm_Yxt!$?W`Ja%vKvzvrH<17^)AMbL#7wX@^Z0Y}3p| zd3FJFBcle;BYUbG+IGKGz;E#^onY4!l?I@E%veTuW=f*p8FEaDIM-K!IN z?)vTc`k5o4QhaZ}+fRS^lsOK`X1?{*3_2Do{&*~H6=3Q0qYLBaqyow2IiEwxd=e4z zL^5I7I|1WCo(~Sb4ghS8k+~#_@{aCh8aXLDTMNQ3TWj1}WyeV~2~QkN@!tzO&aJNs zpHG-yo>HW3CfK?slk%q|ilycMsatoYeNZ~RaCGy}nkPTtH#pxo8w^OO#7S85)7ldQ5$eXj*Hc&uFP-TlW7YCwM9(F3?&{muF#2l+Tm=p`F} zF6zY!q+h>%7p~uSA+FnTA-bAd&pp<8T19cwPz3Ia-q#E4eWRKK+q(MK!1-GEo(lhN z0G)#7p>xScEK2;8aYEc&FT!K|0~S}V#%O_MnSSqX6UL8&H)C1`I6z^T)bH(mEg#!= zKR)#6?U+#7I9g~)<5IJKB>jHZwa>%vJm-ge{AGGhrmnuN0B!|Q2+Ua@q>LK{!P4PD zBp{l1Wts~gGx7L|Gvsj`NEda$`rj%|I%(9&i@FP2l)&Pbp7j#E`m$?b*?2EJ;(_*? z-#2{*tA(E_@56{R4&mZPGM7Kqx&^=4wL8KW^h4ghiJ9Z=HLkS7|FVA%{^h{qo~d;o zxVafSdt&b`=j$|Pj^XBvw=R-NI0)r@p9wsEk_(A zAiegCOHplY4C&dlb*KfVpF-*2DU=SKvY<&s^>U%**;i^$=(8tJqyOX-`bUg|Gt%O1 zY3jhzEu~7C=L3O(xe-3Zud_*V9w6}l@^?n zP@}2=yF^!CFS`2sRQb+gMa?@zb0M5=(Av|D&UFLYYjH!VLGMx5T71&VOXIjn>w&8F zTzXZuUIoT>?b?OU&7HylC1|=+3Eu2rcNtjca`s-$aU35puM|_+y10U^r)MoNH&LCb z^J%5o8d+KL12ps}6LvM@E91(EG*!U{`qG7)z`l0BItjw z-+2)}_~xH7N2aBKf_-1u7xlD^s{t5we`;(5Z~v9|;pFI9$es>J+TP;8SQd`BYiV!A zrd>NxZEnKcRw^HJ%;vx@rO)qc_8HM289=ZH#I;y+tu@&aJ`SBB-L7cK^3*I&eC_UF z9!O$=y* zM%w{**G4%I3+8Hm z{s9HbzqW4=KKtms7LeOHn=!rtY;lbzBqEa(Sv+U1-@Y5Ke%dqef=jN_+V*c5RHtL! zwp-o!=Lnvb+i%x*bfK%M1#4j~d@qH6^&a%q0!B~BuZELj;KBCzI&vDQWjp|w@t%Tl@%O0)a=JO3VkfB11M)hfQ(Y9o`V z+`9*O^Z@YGG2D0my_izkjTc>Yo&L@NH?K9f%w)0FoGlc9ZGZbFtgQ&zv9M?r1K zx3167HK9#u^)4OWgd4VAhy?}UPbi@R=kskd*bo*xe|(lm9A7zxUow2dnC4o9F$6}} zDd5}Rfre9Kru>UbegZlfm&l#Xs{WK#B|AF_In;+*gFD{IZfS4}GySgsf8(B`$WD(( zX-Hc!ZpzrOWmtoYmH7p|3eFDi-oS;+q9!L z%eiSSP-SXHo7GkvfDwkF(%0UBP|U#bJ%cOD%V_KE!NS=Q>(x4dT~8EMSWyCi<=Ht6 zin7L%cKD?I6|nyE?PJSFNA8_Ea~hkM1#Y_R8hms7q?*4=h--#0d8o~aNhYAK-jQi) z>*+~j0$a-1+>sjOO;(o?QF@-7jK`2b?5{+ml$%dt4=CgNqTqysBEJ$ay3P|%;)t03 zyK?(4Wd7@v=Ouv)o#V}Sqz56Tdm^Luam=P`D^}U)C#g_W2r2Up=un4_IeJ8%&B<9@ zaB7uBX0_albl$lko3kphx4h;@@#fdP9=|`eA1$L(Xq;b)j)rm|X7~0T_`rLA89(*9 zH=)0`&zFld@S}4I^i5A7#Upp) znLr}iBVV_*H(rUX8<&ejN+2lIAQ z7U7)6IW2kBZUc&hohAKx8u;*8Kq(0 zvN})~B7e_;dC}NU=~caXToA}1oH$3EwXhbx+rq!A20ab#ST?u7#wg7NnY$CwwLs)e z+6E2;9kW!psunzt_s9vi;<$wMty>(vm&21Y<9OpepV7eEY0FneCb000M^V)P$k9;g zf$#tLnV-kIfB5y<#FZ8Zt~K{D(>7=K#Q>gzbN2z@I>u|ZIin+EbLsO62{V_1^_&)+ zFETce+4~3Y{t`a1=U&ZRGz}}wW?wg6z4;=%>%!}?v9&Yz-LHS+i}>^VZpZUB?7(ZU zzY&9q$>ibZO2;uKDpBn#4?TtEhxY3&1|NO(k72mID*%7i2E+um)}WomEh$DfwP1Pp z)KPru;csAUeg;PCZNHZ;Cx^Lf3?E2~i%mW?XId`JJ7M|3sx^hzMX8y0dp37s=F-h* zd~m;J>?+63cpNDUHKSm)R&C$f1liY#Djfx;wCXwz?em_QM>aVJF}r}+T0sQBBSh%p z=0>zU>nbdcj$v5=i2gmt&?g%7C)7qHFu7#?W}~%HV0&(20!uS^uz|rycOq;#y{prHi&NFiM_sD5Am{#<= zuCW>Po|sTfV@W%l@(%FS+oMZ!n6FjbpeX3qNOF^>q>{&v&B)_;@qXqYY;v@Jb@O`U z%8?xZ=*xiI{O6+E0H}TBi2rO>;5YNYJP0kL;mtDS;5uh5QV>6G<4}ID1!t2-K`_p5i_><3k3jg@UTTpFN^slc=J98wk*4o*DjXQUsqG;s&hCZe# zuLh9LOvWSdOKn{Zd(Js%M&Wbme|!3kU}fK@J|BcSiWc-;aS_DMO&*~8{p4x6JTR|D z8hockr##7%{Bw3+h8GTQ3)YGv5#j>yf>4O~It#*#oHiR)Sem})>Ckg$j_w_LCh0g+ zcN8Cq7ShQIpM~d>XVU`BhDiH9d;|ghS`rgwg)cqKqRKI&qAIHq%0vq%GAAkMor#%zze!)$8 z54#_~Ht%RW*8|wA%%j4&%hP7h=}6NZs%~pxEqt$q|EmDqg_@KFFr3quh^6pgI|Xxm zkw0fhi5Mql{2KAI?-vJfCl!PJs(b!Hf%64aEnt{`=t<1q^{{^L+uAasuE)BSw14j> z{!uYtN>f^0NdzU_;S!McRvu$p!A;6j7hJ{6Q!tfIyA?Q(al*zU2j=Eg!J<=;HhFq~ z<3Ig5_RLM7b(=YcjllVvcU+0LY`Y?=!R*R1Ui#&~!U;O+>f{(+d*$_b?R7W$IKS9( zMgw#Twc}^8{H1#^P-{THYU2;R>!Kdb((!~5{L zCmv8heb$21aBPmNa=LfazKsBo-*>73I)84Qo$cuV1*I-UkrEkVqHg<2Qs87pBtOd z4vd_aZCCE@-P_T7^(7W~D{y^s6t(g)n#fB$IqtKXqFRNSwKKDKY0W-mwZ{Icc;fgG z?CKxPd7$NEGODBbADultD3OG44Z897jV{`t#TuT3ht>teX4eTfw(`0odjDQJfTP zu6ADtxLIE_?C9uK5bDNTZqo6k1xJXtU2wsli^PQoo_?|72ut&ttP?gaG>9yWSUH7z z&*q$8^SA)bWhfo3vnk3TGzI@KZgky^{xa@0xp`SM>0owXc-j2?!5wCse%kjFGzCvk++oM!zpZTbwKlBTMs7+Q@ z!WJK1mDc8Fr8VzCYYQ`HO_}+WiBInDJ+K!a{^HG%#=NVo4R3kwi}98pc!@bOEd~1N zwYz8Gb5A~mKY#Fcj4sX_a7VyQ@6XPR2W@-bA~U>v>qU6|rPpJyy|cLN$@MMYxC4Ls z>A%B~qeoHeQ5yZ;E~PxIOC( zAY0mZ1jinH%!Bl!GawbF;JKgi+&|LZ5))-T@aD=C; z2kj~Ul`nh>r>7^Aac$-ajoTOOzFhrXf$O&KLU(I>eSlnVp0MZlDjwlhrKSI>(wq}; z4{@V}6i~lG0rgjC0YKe(STCP~eCw6RKev770`jg)sVwO$h zfRu30J!2dQrTZ#MH_eFiZVB-6A8_uAU2Ai8dq(E>L->&gK8tZWlF2f$^Y=c6`FkGG z*FB1fc<<}qf=~YXf5*Q(Z39LW|1TBLqOhdE$WY$^o_@(?YK?UamWY1SYxwuD3mq)) zDZ*>}r`b=Dc9ct|cq~M*(mpR*IMT&ZoC655BD(7?C6)e(E1!kUtzCF>enRiNo>SoX z?$KlT$K!j^(b$BGy4UH*%}V0?jKMAV#Mynox*qHqJBzP9bRXK4){V^Gzt*yZ#rb(G zeerIzKd>KLl(xLRsTps&`k8pau1gVDPilv724Vs&Fd9ue1ao(v827I%bGX**N)w~cJDCxkw%|Pdg&M^SZ{br?ORCT#CpmoIlx{tJ~* z?F}z}IT}0K@W?|CqcS)jSQM^b=_hGE$_VK8Cc%D4yTSB)pVey1i>BKHyeN+?J=+U-10r|7iukh|Aqe$fKGv?Ru;G&M4B|l z?Rf16`z}&nrXeCgh(NlNa;EPjW7N!^(Cqul8{|iiKZdW3ANS2VtAIKEQR7^3!7hC9 zLw|%Pqxr*LApOCKdA70{t(8}94FP0xHTs>V$iV{%h*3ujA_Fy1J{;Bmy& z`$Q(mI0n-(*XhD$9!qf3GxsdYeU$}==S6|`r@v|E)%xhH2PRJIXLFS@zIOHqo>ENN zVnSN|S#NwyXIdjm&XlSQWVWgPyn53%JZt-IEGmuFv8izjEM!5OI^ZQ) z^<`);Y04uZ*%(kvdy~Kdl~{q;N`IF=KfFW?nF4&?tdP+Gc(cOL)Y4supgHmV!tMjn z5SF>i3^>2~Ld=bfp*E+O8xkxj#;{!jbi?F%vs&w{jZ6UDkwzM$Dsa)gS0vc@ z^Ia>w0LFn%a;Vizp0Gs#6jCsak7Gd9w>{lm7@wFxS%LSt>1mufbsC2cAHfw39k}VL zr|WS#wY(VGl#cx)v)82+th6?wX=+|E)D>SKI-{7*l&3tGtZLN@yKH&WxhSIQFre%i zNt|Cu57f`c!D@s7u-Q6AS&82_1$*$87I`H7$z2gmU#*e)CWj?JL#kl|V z>Ah&8Ef8w#u5`C+%c|?zjmDKMdd|+z;Ka<7K3iK9G3mkxew-kXXjX4$H`Xg~u7P=- zy4Iubx?D;S^|7zsqQLo|;>i=o{PXK~UaTF!z4iGo(H{7D84AHcK|Tb||MM-M&>%`X z&*M>TThRhZ4dTSOp}Y>6%>^;gvt|gP5u=W8XdZqh+0<6H16! zX=*^t9kp1?dtrA#UHbjzl4;5%GiP_;yyii?*RUJOr50L}Gt%a<8QiqvQVd_bOFN33 zI(`}*8;7)4d0`~N7MNG34P<4yZZ!!i%rO z%dWgm@hz>^G=2e&4${$W_a54Zn;*PK^EI?bJxZ%U^IbP2We}^C*#_#jHaCaY!dmzq z3IAaL-31~i4ybdD3I}-+Qzd~EPyw8O5i!t7U!_5YXCP6Y{oYi#&EK>tW@7h;yu)dBTY4Q>^XuBZJmnAH4JHc zb2C14)0?#xo#&;2x(J16R5wk|6cBkvpDaR1Lqtq~J{HNoGC+Mh29z-1m|A!jl_oi2 z45f^OS(25(fpcmOe~z*YrCe5XiaCZ^r};^dP%112|tc(*Hiu2zy#{)K&}SWeuoW6eyRhzLbB1>WAj` zHuX$trmiPHjzX9)i$Gf8R<`m%k)Y*NC29-2O zZ&rVgEKFc$a~C=k<~}}a9Kf+AVBh|C~P0TzhBd-|@5y?&lS4baeUfBw_lY{(VmO?hV-6V*g2i6pZe_}Q|7C#!;VWY!qMqTEGzj>M*d=2 z8jFuevzl+Z&uTPJYiLqU+^Qb6D#>As>aS+9{viQwwHkoC)xrmjdd;62QC76z);qtB zyY@eUm+!g)&${puVDlhO96ExAo-Qr4OZS(jnscgAcYhzAzx#5$V#9^#R|1OEsanPX zF5-0}OV$hyh%@%<$8H4DMdbZG^V)Ad))Vm0oYQf~rUCS4F)&Y^FDCU#>)^EV^UF*4 zi*MY9_kHdYc;MKfcnqjN1I*uqcRlwd*wj0aTQ~jKNyo2Cr^xyw6$puhCfny^DR}*k z-CCng``GV2{8SX5=>M&I9#TO4Zd|;6vz}|`m0wZW`17K`%1Sla;~zipdl5LN@bN>B;Mn4<)~d;`Kd7G(K!3T?u-Bx_F{pwuqx^Xka7&h*P^F`-&9sdLH6;aJo!|f> ztqTGu=S-c@+&uOkK8U&bd0n?fy9c+=d5~svAm_Zywhzs}mafjA0TRN3^Gs0F@IXc+;+Hux-m$JUo8NWKNFy zl%S*9+<=n$73B&fr_LHbN-Qlc83v#B#3xLbVC9X*B^x(6487Ww0x%8?{aPv0#8{ewm(9*ARjxL&zF+_b zA^;6HR!g@08;6JSTR;Dc`90;;75u}KzI@OU#0?58-FxydmKK*ZNUvF|8}5v)X{Cn7N==_y(W2;aV~vE!0N=o8 zG|dL2mx=C2`_O|ada1EdJEL5lQ?%p6X)VzF%Ht0yS?dB`vf~o$*tQ)LwH19Ij!$D+ zbM3!E^+j*50_K50*C9>6Xzu0a%K6QIS_|iu0L~~O8n71D!uLY>4{;KAuAqTK#EBSg#VvTScl_Fa z=%Uw4nDIhRDDeFpUj5{kv__XC=coVSOPG1^2~G6A@H>Bm8?U%J7ypH~ejInsokgYq zO6`MxrJud|MK8xke(}FVdz?L}V@yEQPXbs~m;%1O#N!}@^qzfNFGf8{vOEt*adq+$ zHjV*^=AKM_Cq*rad&`NZ@Mi}f#A(&uI(|nR*7x;exu+cm7pHw$F8szpG%gu!PquLY zmtAuWu5IbVwXJ>F(9l}9{{6L*2Iy|Kq{1z(1$Tkw*^<(BtW;x1a^yVj=10GYI}Yyk zpSpeN`nnbzWJ#-Oacvd)*#O{^?V0*3xya^kNC|$(tJ>`6dT&*4d)_h4$_Iz-nuD(T5|NL0RNehvzSr8HGAgz z1sgZxsbfdXb_c8BHD|KdZ1)&be{%Y_d1x4Kc-0T%&&wxqL}@@lxCPae~b zkhD zx)yn_@(w(^s<{4XYG@3gGo?iyTqC)T%gY6rJ`D*V0lGP=CF6C{3n^8nZM|Sepm#9s zN>(Evc;$59PG)NaBZYP&KnVI)05Z&6) z6!J@f#yQq&a{Q(pz#Z7@Gw*6`$6KEFV!T!Tb-Lr&>V*XC)4%_6=t+F~v2W@<^E}MW z&0%_a+Gw3i)->6fX0u<|{nI9nIWt_(S)y6|8=FRhq9Lyw-ig;;_H+!kbwuSU0#>BS z|CL)mqrox-ZR!rAEl)$Oyn^M)DJ-2hi zbatSnyAxYw6Yjm^PK?dX0Ifh$zlP3KKR@)e8*upryRf%1kNeLYLq!SeC@liEEAPde>L*fcj0xJZY-meEoCqpPv3at-((7apj4|a5`R6T30ll(y>*KS4x9= zcXDz9@B2T0iF*(2MG;G;J-+|oD}u-OqOumG`>oD@3f;{u7-;QS)26S5@A~i^DQeQp zo|=f-d{RnquezM=NRRO1yo8N_oK^x>ax4VSJ=MRr!$b4q7^#(!DNWb%N7WWn%NVe#l~limOf52z^S%Qjtz8#nC0&mO%82U;X53yU~L4ARjFWb3+6Q`&Q0 zMml@?Ok0M7$DYJzm9|!@`{#EK<4rx=@w8U5Y^eDXG+7?0Cg625LKd`yjLS+(Ses(X z%4#uNs;v0BlNSB$H@pz9y8P+*#Qk^R{^JKd)9=7Z4Tv!T*bz~-$tm)N`6?!9FM0!O z%QNFj3NWp~^xSYiaUp10w+q>F86`3qR{&cmjk{b?jN(#R{Vf^hOO;hpyOO55u>rMa z)h;cKsFawmfsqN7zWSS(VSQ%G(x*Odtr2ahh4`-glG_|e?GR^Sc?pBvy;_@Y**6Wq zPfd?we0CNWPmE!Bcmo!c#$bueyJ@esF#FL-bEW~g`z0own0a9Zg9OZ}XtfOE;}bb> zPT|1V8C|BG>xMO8)^tmn*_HMsJz7!i)ZEdIMK!iY&zw~nc_Vx)c`c6lZ73P-w%#|Z z#%fI%?T`WLFxoSf(MbjFfBR4V2ygtsmt*78pN+pbxd%t+wF2qO>-*4jVpO$TW*igJ zkXL1DQ(Dt`t+iBRMD-_>@Pf?m$@Jaw$EyUfI*3k!aamV)!UcskOxgLjtAPU*_MAO|Pd$FG z`aP-d$&7ZfR?)Peqtu9wjt=c$k3jnT!U7vTHSef1v<3Be*j5xQhAfy?@0w^Hk+U~7 zSH8G#24DL6-{aMrcH<{5e`bFDVr9QaXZ13EShYmn{2iV}D(jZI+T)F{eo zjb9oWH@-5Q2{!#HT1xTg_)5^XH5r7iGCpQapAezKRRbIFqp$x-+_~=w{L_Q?D4;&0 zEBa3legk*y+oOQ`Gx5#~ufxd7qHlyPO&#cNY(}uq$W{fjIq*)`40Ji|GdwT{1I~!k z6%1%YL`SEE@7a4hHVZ*@a<2ua!Z>GjrZCCa*Fd-5g5e4G8w5z_C6Bh$_|JhC*Y7ieuBwAb!+Qf>AA|MU}eztJ9|71OQFS1t2fD`cY2 zqn*C{=5jA$$T+Cxpj*V+zSW8^3hv!6oku>GIp$m9&hjMw*UUj2 zn;l2x(1g)K&&;DVK8w!9Wz7HR^Ay-@^5vQy9fMHX_EK-V);LS0iM(xL8DE<@jaT+; z!B6$?QW|w*I@Xk!k5Z0R%N9l@69tSt%vqx3l&q}4+%f~|>pD8|bI*GjZa?@W{^8+o zV3f`)Mrh{%8YBa_!sb)+5igjoU|(+|mOa8Du3dq*ai#Sco6@HJFF zdG*fhHJwpTpjf?|O0;u498@5kOx|}YO?s=!!<0T8*X7!qUd&BQVpiopGcm2tRWDga zQ9HmB!fr#!G(abMSY@D`v}Mkmw5G;w%{hlsZT6|Jei7BL+=4f}?3Jiocm@7KKB7Sv z)VOUJo6(x{OaTm{x!NJ20y6&CJv!@sNeNvP?Z{f1JR=+p*7FTQ6wJvOZ0Cba?l_~F zJWJ9^Pcn##?baklc=)x9fO{DCB#0p&40#%b1f0UxSGxC>Fy@*-AZnM@sAIex#n6O# z1<-o>`(frpyw%KEnO(j661@JUH=$mjcI(t$(1n{_{yAOIIq8RMi`bqSD*6SWlui*C0Q5fdzj*LY zeC?1qhK=UTw4wovimvE&PXXE`HMhEzrk}P7^{aO4?CiwY_?Q(`I~r35FC$pZ zJdkfwAW5w2!%9y(DpXzPT&=wYpPD|3k-KN{zGuB8*DfSL`0da99iIBey(kmFR^N== z^AJjHEjo?L;u5NAerl(CH6v`wXx^)NN^%gsVotDja1DzVEKl?&+@`yiIoB?iAl@uI@KFVpB~ro z$aPzG;+J3iYJHZu@Kq8~`VmNW_XPSYW&GpF0o*!%9QVzfHLqA-yX$EcZ%VBz1lB2u zTPKd`cLdl8sK0yjmDt$Q5skURJlQ)sa*h6OR)U%jf9130cs9$7pPZUd;Qe#B`GLC> zNPi_>eeJXA@?*I?>&$=uyg*nvy}XD8MQcf;-rw4ewXhbx+roFnW(}=v5Y9rCX$Tw_ z!RI)fCu#pcKl^^&W4>B=WNAt-*wj{2_dl*_+?!tXa%>%1U;m?u1?aTUGo674wH^Qp z|E_Kj@r`sQl|@0a&i0I|$pMTVbYB>jK~4G=2btNNm+mNCJC1mDZ;Y!Mvb^)$p*Y^hvU z+Vo`|r^d$lo;{D_iYu?w-;f}Y_RZ4y<8ulmS7|Rr=}+`xiE(d28Pk6w2iGw`@zdUIX{n+D$bd?X*^@+MG82m`Ki?N1Q;{`7iNED zRh2;>A*uJ*XPWV@S*EWhOkAosjk%g-F15M47Dy9H>(*wi!Z?F1c9NgW2zpUH> zp@c?K^(gsU6Pk(zd-*D^SOmEk^5GrhNStFb0?^{+3U#S4xf(kAwY zr$(^7XApNh{t)iI^DZonj_Y}qkxvm40;=2$C^y12vW}BvF=S2F=@bg-)@~YGB|`^W zdb+gcovbP7xHPbG2&+-2V;qTI4K}sveewo(3GHO`RoC2rXYaZc|ERR-cRjU757;ra zM*R70|BQ!@9Kai&^#fSf+3kUFI8QtPR}XlRoFafcI1RrU1JYr9J0P5b@ZelD(VE+j zv!Z->8IrO_f&*uzzLLx1>aaRc8k(S!DN1;G6c4EAU5WaO*$XS5AA> z>3YEeb9d&htzQaY8r1D@t%OdQ1t4I@kPyN8!vpVvHk8S|8-Mg$QOxY{`eAf*bm@AN zLm_(CteDnPslfw9&&Xxmd7RmES}w2PzHfXJ=L!|YY>X%XepI#lgaYft;1MW)^6V)b zpBmRp2-RU-caH+_&9Rqb1SYRjjWN=)^Jj*6W#{xFW{&+ep!c)idH;QwJ8%^L_|OCRja&ao3l^^!-mI-kT%a?}@0~fLg)`0CL0n0D^M2@&M{viUN3e7E zZv6PRE79B4rOny(d}KwW;}?zQvJUfa|L)zt7w`MqKf}kr{$+C}vwBYs;NJeioAALm z{vbM_VfzF5ZU9K)11IknX(kS`aqcznY>E-`MvA9zAgwN0dhLj(hIH{`(%% zM?J=CnxiZ5;4^?zMq;h63Ew6v4BmL1-iJ=d?yZoui*^ntjZjedoFyD;o9X#~#GEqFvk84Jlzy*hifnS4JUn4fZSr(C!R!tm@U>IG_^|txn6x&odXMu(9WP3(mtg!NDBuFaOmq{=L$i z-=#I^o=6DebnR`=`~m#okNgBSS;xGb9t$@9bK_Y3edPEf_{IHq;I64tSk{6XczVtI zo|6zM!aGiW_?$esJdaPF-KTyRaB+K|63j5cYFdtjKx&Wg3Vf5MoVK87M{-Q6Nymwj zNjz!UwIey_AL1PIKYAxpqFGGOX|~Bh3|s!T?5cva13L{2)ZVY z2oh1}b@Th}$h2=Ye01uNZqT_yCoukvhc#_`WJ4Q%|I}mn>WTe$WaOk~2o5Pg@P+-4 z;qM;(2Bwr${EU+16R0dbaaaSED|TL}&p~$~a_(h=;IKJGweFP+i$#uSPw1{^YFS9I zeeyv8As2#YT{qN~T%#_}>;o;8@0;0+(Wefe;q*9K9@uYyjB_AFd#SBPRdX4q#;`a& zYtEV03xDPTIxL{e6p+tGCLxtZeNN@jp+^6++XjovW*0S$fo(ljt52x2`JRr|q4`9P z#9W}`-Uy^WPiag@<3!qBZau-B#(B*&OS?^}pSqW8deJOrB4{z)um5A^^yLMFI0$CP%Sw>2eaChP_FRK61$dg8 z&7RAe6)vSrCS4yrd;}+s9L2X1mXwBM(f*}^1rkaQ_w+{jlt+PAfWa>(_6?t9D$Dn_l=L965FjQ*(1>TQ&^%tjJp3I*=LemVrV1 z+FRd&FFoJ{6e;a+Rsz0b{2RgI6kVpV!jtn{72JH`Ny&VQe)vW_qsYbKziu>YM{MNZ zK(0oC+()a`GbO9_>L}yaA^`Vg1^D`hhwQk_@ce79$FIEO-MH%FOVLQaA5^rf*KNWr z^QUm@i39kD2kzGUJPD{%Se8{hJUeF2L05nK4j;tl@46kQdz!J~g6;UdOMd|Cl@_(6 z{U5L**_#G3FiEv!?E2)y7~cJ7e}E7F^FQiqR~K^r_Tis=4{p5p3Ui)0XXaO9_zUtG zU6|FT?QZVV9`p9LHm!js@G1o&A&AlVN-Wb~MkhoV3#2wBfM`&(y{tgG?qiz;&CO1w zt;Xd$QKjunRR+kbPIE4S=%_R$08sgh`6W~pt!bsxFIro$vS1vrg*gjwt*~EMT@wg< z51YOnF@0BGg05ZLwDx>qW|o{1_%qQ>*^B_9ZOAzIdVco?w73&45U<6QHv#Y)cVC8q zu5KI~8^wY>4dj^OBOW+%P@fKR$)>HI#$^1wai)dlAlU25ld=Ei>z<>4`P*>g&P$@> z;dq(Sx!}h2z0+s#w#UDWuZKD2)teLynyD))|pL$XQ_KOIp zH#7ytjQ_g8?n(IY!b`8#K%GE3d3Fy>p487ie&<)UW<3JxX`PEgkyd?``LB;KRcjxu zODzqCw^<8o;X5^a*8y}M97CCE{C=I0SUF?US2)AINf#_oAo)3K#p8zIOOw$gl5 zl@^n{Q&;WZ)Qke>w7{$w6S&fW6FMEzpx^YY=lXQLHr69rqK4_0b@4k0J#R|=2vlU z=DupYe9OoSu&|Q)0I0}|w&+v~Syu-hP@0v-xh0G(E$B8U>2R|(w#0 zI!kjbn`Npw7$l(ntR1_tPHB&iPL5)ZG)*?`v|XevhyGPD`xBQv11)57y|`pGUd&|KmM3F9$Ylax<&+3O-Mu5tn0{Uh zz;SV50T=GR$N~@AhaH1R+D6i7frCD7%QFt9t@b|p7$(QY@oyDqkNmFnn~YY;F}w)c zB{IXGUtDC&WneG?uAO4ct<}Oguue6kefVTT_>j_Qe*NBi@XF`@0It||5gy*Z*Ox`R z###k@{@iQch+lgBTX6I3x8m&dB)pc9w;%`Q>6rD*r^v-|RETq^W*kU%FCYrQw0U15A7R=bh*jFo2Ta@jD>*D&D_pxyL*zU(jR!*W%`c-f%B>=r*?En&0)(% zdAIiWYVG-@ZN0d(y+4e3)~1o5KxMXVU;Cx>cP?R5$$DuL1{6cRDkS`ryfIl za|^Z)uJ`550pCPh?EtS46e2kXJ!5n#)L*b8C*Aj6c_Zw)DuU*`NCWTvBNUV|ORq%` z7|f~q{?)y=;n%+W_jv5|@p$YBv(NkwfAnp5^%d9au~H{V@a>*EjUBigRN;G$JQe=hx^cB=QCFz2RE5>Qv`7g+&)zn|u`7QQpX zcQrs?3;(+CiTUGtAy^ul#K>I_Xad}P(RRp|W(~**oYyJ}oR=&5kGwH^Dz5Hp?XB9| z_7&T9L>hDo&NFf$XDX6=9fV(x?5&AX$lNocoP-D%reD+}!d6>S1Xw+@Ux&eq_;m(| z-TfU4WBB5T0?;SN&~SXzfN=)U!L;YbiCADlBtJ8|WPh{RbFJJmgqg`nT-4BsvE>CE znVZ7inK2xjpT?wOhP39Jm_>V}4VTDspnfh;L3akIH|Rd$ple(IAYOjyHTrsFcG>`S zr?GYzTgD#XU7%y(T2>@_l}3GFN$PXcjV8^!t7IrZJ^_rksbSfrH03>NX!ci)*8I?! zQ)cmp5h(CaIaqhct9gAG1Mbnd%e9e=_<;&}NmmT_@bG$FaK1O(?UAGpXn${GQxmFi zD`fjd<0wpNHT6#9v@ijbq(r zUKoWu!+8n8QW+n)0fnM?3<%MA=LFETl9^7Aplvn; zk`*{_QUJA6jcWp}(5ohu3M(?IdKUdtuGKUB7b8x}8vjS~b`n zy#7iIDD63gMFsN9L^C5HfhnUFP+0BxtNOO1g=rmKZ;Ar2412w8-7v0If?NXUN5@9= z7X;StKfDiL-}@LY+q@mUs$DtAjX=2t&RipIJf|0Et}_m{^Ct9MD;@xL4D3aL7TZ5h z%dd`cLaJxHB&(;|!zhUnQjQ{S@ z2l1;1zll@jc_)v7o7F+w7}Ej{!lNN80*&)Sq>vN_#*D!Ey)$QV^T)*T4VL>9|QSbZI+kiKRDd`kcw zTVjBEEduJ7uHTI7x9-HeV!jTKkJz-O&%-o#xR1PC(*^BwRN9;Nl^S}=3bd=gfh7gp zm6n|Vc&~m|!(dsUPhEE@FyC1-K)q2~*ul!f&!+dvGj;V( zdvhxm6~M@xH)Di%no0CWKnbMUq-mvfm|9$@oA1=zEN*-F0ko((zT*`?f+J^7VR~+^ zm>#I`@PVgr^vr4X&Hd&9ffg;SaE!E#7@^?R0|(COJa)znB8i`tEzK4z`~RQ4_W-o3 zD$|Cab#6Jgx7^(HPI?1E5_%DkCIVtXRM45RBmOfwwsGbgbrhZXEc1!t=s2TTu!GW+ z4xxsc1V~8l$xXSv-*e9X*Sp$&*V_Bsgb^K;yWnQ;v-j$ImG^nuic+{)YQ98vwg|5* z%LDS(rLZ260qL2HUyEY2t|PpCMiLUglOaq)Qr?gJ^?UG@PyG`nwv5xB#LeqYIhP5T z_Y4mjVX=O*xTXpB4z^*qETbJdP8*Na2X|p-K?2vwEo2*`;!UWi#&vT~Q!4L~or^}= z9JzJ~l#v=WtJZ2cF0T5<7o|k)8A(KIN16o2Kl1uFJ9Ih+RQvW|ns`6P`;(i)?rvR!0nPHOKtqhSUSuM?D0xm;W8GG&TsH#oT&!{zc` zwjsCp7zxgW2EKW7LPDU*Zh^XeZXL(bvvN{#C&!Ye+BwNQ80zg+M^82!GYw@`l^B-N zbp`P4B5v1jb5G9+Y)^UZX{equL9Ji?66_E4^=YO6rL`YNUF8}(Wun@X7Afhu24jNv zEm!|`pf=+H+#1O&TrQ=eXUt!!!1<1Yttt~b#`13YjSou3?u?f4DqakJnN&RE19r`* zX7=QQF(b@-4w|jmV+g`s8Y@W`F$0|2n`Z)b_9TGUYWL6V-X3CB#_Ptc#-nVhPC^5p0bEofVwH9wV*7MQfG|(Bu$!p-XW9g^gIg1oXCy9FO z9bbC_clO-Zb{yI0K*T$d{Gt#$c~eme@qme(gpMVTm_!>sb09K=DEHVNf^+WK z-;Rkw3^N??C&HPeWK*V0Vf`97Gj!donK0#&q4M~H52@qyUg^*)f%Ked(>XDo9F+Kh-CMbh8Vg#B%Y73jX?lrypPn7QhhDgy6mp`oS*+a*Zn zK}ia8TJj_^BijiUo2TLkzy0wXe4_xiYj6NJCjEJD0J*RLJPG!N@R@mfnng)MII0B!k!mbC zfcx^*A5hYBT~W44gd0Zqj9rO&SWf0Y;YMQuy>|eePKbz1g^6(_nbq_stlE2Jr7}$z zBHMI2Bb@d|2{vk^R53TM34OCBB1dLO<&v2>aV!ezG>XUo?x?70yRau#Sg>py$5jj) z^QS`0IwiYtQh6Wf=u*;j`Xhi&Ks^SmQ0s14btT51dIH8QnTL#I8sz2t>h0)|Kz~S; z6ETNy);7MPsUBrDRq9B&^BblhD@sh4nKAFwyWxOuCIp?CcrPUYPTz3hl<9bJ&kl7m z1~p{61njpzw^{-ADPzWJpq_~(+!;Go%zhZ>Yy1smxYS-G?uFSn)9PiJ)goXt0I{<- zBp47r!(d-`2mbkYzrfcY{#|Tm?iL7bXa4VRc$eB6!?c;eNnSX9$+exum>e;3 zrk&tdH@mcUfj9jHjC3B3Nyz78ye7G2=*YojLO`9Q>13a-9LY%mfvcb@pa`uc|A=X! z9m$=nRNFUrrT~$8M$&ZcNN%dunBjT9%=sTTIywKzp(#0%BW7fcT(d^i-bcg#b{GZd zufp)%_AR)nZL1{IHQ_XPs2#1Vv;?bcwgl)EWmaSY;n>;pu!bZhL6|yj0(P|SOHHqQGAKuCnV2ynLO30H z7aQ!j6)7`Sk8c44>yVrO>YwCz=g;ga7SO@*m=bJhWA#W|;tV|s>+j%*&Wl75B3ihA zk_EATJLlw6asB`KoVsSs+13<>^EvG6?8Jfm0EP=p6T~#eu9F+Y01lN2Y%LkWAW4Pk z3~gdOcE#EijdJ`IswlNf%!R$ zjzdLRd7?eZ_6z@B_UG!|Tk*ZE&*2-Jp2QDZU&4=ftiy(#+p(xw&Q)R_Fk+oc&=sdVHJu&=92 zwCQ%rP66NVn8C;^N~^l-CAqli_7D*qvN-HWB>_UN<( z)4`-AJM-^7^${A-HK0c zdIZ<+T8nObWMx1$#VeRy*MtY)0Wjc?n8C=NInXYUSkKZ|tMss;uU#GSwr8M60`)Q4 zk(^(VBv>~=JCZv#b|lws)&S!uRymS;=q}?(Zfb1W5{8udpZcEdB_UX+qeXn56x|LWX^?;P5QVaYY8<8DY2(79m?)?W85wQ*NndL{}rmFoSlSzihWsK}bUNE2qc4LneIQZ@x{st?i%v5DY#rUyBSh=36d&JP+-8_2^$X4ZV90$en9D89^&&am|uIeB+`OxM22TB{iordhH)c z(CPT2az=<0YYXoe4lct^n%k)|6>jJZG*Z<_YMMcV6|%3ddE3?M$^(6C(vCRX)1`77tdtVgl8jfs zUO5)-&2-`JLc3By&2{ylAi-+?q$Wh97D}2{#&IYCkjk2B1x|$o@8ha#aco%wuCAYs zaU~UwAwhun=l9-)AKiPW#P+>z4I(M)HRoRzOUy?KyrbE(+a%Eb!GVqFsLLW>l}#io zKiq|@nvTI0bB+UN;9h>y_3Ab!=VGD^_M46f6p}GY%+f-(6#di3Yl(T>W%2gLRN5nZ zFXww%SHBt?xr#Eq-k5jJqq~{AB;l?Z$fLxVnUUnYzoQd5lHTXEwn-tc|ETY4#*IaV z+!M%wTv=_bdORfe^MSrT6;Fe4eN3gun`6}sbATl07M!11KM~8Sn-MdMRc9MqX{65< zn8SXGfYE1|fR;%l0>^7Vee?m`_UvQm9W+`C@-=~YIzOF`_p=PMSo+Lt?X89x81Qy= z^_lMOngY1JceXsF7*{{b63D7Y@If?NlpLGon;fvd! z#IFu+i5Ist*ld$Th8M)vGo|Sj@yM(w6#31{iTsj~SR>O&2=;tZwg#7torP+HRoND%)`}XgtB8v6=<%k zRup1nq~-9g8k9%IUe2EBI-#}!zj()oQA@{6AyyW3_iW<{*F+nVU|Bu2PvUzWCBfK=v(^Qfw?L!UKBlb3Ndb@=q@7vYpivy}9_LIU#&3D8N>t+p7! z;6}J;;M;!YfS;?;Q?PYWOe7jh%oB42=1eY%k^n!7CFzzqVC%@!siW0|1|}E2?jqDk zP@>94+oqRZQUD#UyqFo3>m~5ht8HI@k5DGR{rRV#seqnkrqOsK=~P#Dx1O&= z*fU^WP~cp*lf1N(L#izY_u-I~_P2y{NIy;h%qNW-k59e#gE;%7QxS&4axAp>^{6pm>pq+ABX^hkS283YA$zXDw4^Q2FO2sF3?d>7GL=0L^a z9%biFjJc?x2EAiy1Ay*>^(=udxxQ5N4{YUengd9;;R-(MR(yKE$ zwIgt@RpblKJ3APo-Ad0Z8YDO$qrv&fwc~Jd-S_~U8ZaDy?D*b-;UeHSjBkK7F>inJ z=>53wg=fJzZ;PkSz~5c;MrBqPmmTY)*fPQLLNVZNnF{uKhA|kzI_d|^DXT8;={?)< z?Z@xK)4R6@ADYZDD)7FO&&4~IpRVi57#Ja=AOwkIXHF968~3l5Abrqwv`=8LduM|V zqrM&x9tHcYBE%<_vOUP&)1?s5Us2F5mq-M4Nd&$)YM_42l%*1^&qRVyuze7MXf~%w zz5V*TejRJoaG);Nifdkb2`*c~*0i+zk!Xn@H((AVh*9~BG-QYluFLx7I-_AU{2{|A zKz|j5x3oTvU47jOoDUBSXb>(51hO|Dl%T*$%+*ysA6r_1+ER0TjZyD)8@d5)3ZR>- zma4~~<^w04jSrsk+5ntW@Q0kYCi2b5{7y^b2LtqxzMlcX!fXpwFGNgqi6Rp86b`Ic zhKVECJ%vsw5xU{QJMqnD9zy5zaj;%`(I^QqNwk(%RiV722PN{al#Wg78}eE~@GO!N znPExT<{N92m*l8bN_EKD932593AZVY&G_33FT>+Iw%}*aueL~4m4BI(4E_B{=i->g z7RCJyH3W_n41E;u%@G6Y-~e5?ey{+A-Vxv%RtyNg%+{_BeD%Itv3BY5r{c=79$ z*Js55hzBxyBp|x|H@`u5XD4DwJPd79ur~^%=ev*2=DS!pY1wj2XlZfB_t9LfDlbQO zPmdw~Bke)Hpuu=7J=b%(t+yNNc5TNWBE0kb*Mk~P>c)@1xb9`8)j+*etDz=9Dhm!& z3%4$L*#JdBx&`N9TE3PDHf{}1&8)d48|;5h_048=fOSVPUOrzvAPGzlz{S(jD_qG_;+-=1nn8>{C1mPWF`1{XAd9c zV=wY_oLZL7X-5do;UwVkk&H+jLB{Vn5ViYjtoCm6^V5kUKNsVqWL*R4Fs*PPJ$z?@_tDgsOSgHR`y`Y7pYQsmI@UBvX#np(^<2Dt*=eYg z3=*r>ugWxS=l$S~5BdAv!#nYX?N2H7aEKxw)Gsb}G-=kHj2Ff1U$!p*XP52)cAJ+; ztF-yj7-8Y!WS@JB8T1ECp)m-kUpMo+V4Gw4@!ypqY9|U@8A}~NljyM z-4%abT-$~N@nYqL&%-q4IcbrO0`<}G#|onW{Z$ga*RxaEp63iF-Ad7E^R|Fpztht5 zCX`8VUK&fw35?`pX}JbK0XVk+uAo8Gq`C&&^}bJe_3)%!VQ?TNxkhq3WlkcAID(gr zzG1*Qc*YlKE+O@Z@0S5+!!7LgHBaIbzx+OSwwrT%$4ZIBl<|5OHH;1=QLmBicR_$lY(%Ec=&wSG*JVNs-g0WIKqT!CCO9sm?QOAF`A;4|(3k>kd=lPrh z>V>`m{7=Hz=4PC6(kZ$=1jOWbp!2GGCG*Isz8e5x;}i<9n)_p$Uepfd{y>4$-dE0F zh@aknH~uGqB`XA!Xfc^R^AITt;vo<-@ly>6}wPT zWQOw?-rYL#I%ytd>T3{99*f?&lhgAz*Nog-t9BoPkn4EsW6vVjujh2OsR1n~EJmjU z=&{Xt-uLjHHvpaX4>D%P*EG2bE=={Y=1QhCFWv+c7-x~nlYxExoWxa0W1T&U~$qU*c_y15+E21Xlth1J?oyqj~~8QGLbs$ zMz8$eT(;sAyyNuOdLTRm+gy6h>ynJeB;~q6i8^=7`Et{m*k%(rqmz$2Ty#AZeF)<~01 zgr%9cQA$3nx+y%1M4Zc7W+{pKq>7s0T`A3>p16rMZ zK!bG!(B)r!K7-m!i38^mmzqZrlcF1`dRiOrZ)C&|)4+-&?-;a?Z3Y=ZG?NXPAztkp$)?iB}l*d*~gW~Ru`W$03xnp z9>wD$@>SYnFpW4Kzi01U-a&c#r}<4q6k=d*2db5t+qxyv0STr;c{Z!AL(0Z7O2oJI z5y7`{eJ+Xdp+oH&Oux)<=4q!ZhjEc1)+^+g>Fe)z>aNx^G^x8gzY_A0Y z+$dfRM*`>+N{n}G*mO}CH@;cEWT`sOoYdNR$Vq0;AqmX8`}$05zff;okk|dCd3>zu z0D9JJKz{#W^%vJG$!N_q)gj;6qol;Vokl0bh4MMmQ95lr@>S*dzqJc+W?hQpnWc}? z2xSwtNe1Zux#w5PmOUyffoQ6Skj%}9OhimQ1|~LPaN-!=P=DR^RLUigmV08htp_DL z51`Q7&-zxlIc>im-Ip{-N&S=-^caAy`p^Ua1y!e-t*sLH>m%a`oF8~<9rE&O0eZ)n z>KK&g*%52zcbOd5!_=FU2enNy5y+=VrIgqX$~XhLA&)5#88vp#JG%wwEdTPl8dNma zE6Fx_Yrmv%I$E+-9?<0HjoW7&bi>BM2mn2i)~cgha2|teF1fyY-P8Eds(Y2ThaD88 z?oLdd^N(E~GYr)K#x}i3`5q$yNP_o(kwBpQ+fUr5&gXUm(gbn<_x_X5Ro5^t2l(xH z4FF(Qei+wneG)ex+zg*W5p3XtO@ss$<4MphKzG_cHjP~>M4WvT`B*xwn1aB_6qbkR zktB?GgrI43W9`!uKs_y&VE&W3`zL?$U98@+!C(8#J*m83BLx@gD8^!dKX~r-1E-5@ zx%NH?_2m_4uBsjd>Z9Qg5=OnazY4-PdbVTZK&SD_ZjVkIGS^}EM`ya$<^}3d;sCZ< zo0x66wMD1G%UI1kIG&DNgTOSH#!0_N4`IJJe%6Y#<1XJMKI z<yzeK$w2W$$CDwZL?C7{X>WGW5!_L;otM9xhOIk4KeX= zUa&a+^wszN`!G*N*P1lKTVn(#_M;-Ss*THPB@ESEEIub@byak!nWAy!T03(VzWnij zR8HXPYGT5d`h_IstzDfMWZDqL)<_G;evE_I*t!n|3A%GT_B#-S0Dw7lUVCmhdq=g- zFsUTH%eQr+WMYc~gnM`I##v*gDsRbN2#I8VHSf1~9CqM5Pi8>%)mn9zG%6}9w9UWV z<4U{xmCZM4DCH~515h6Mw%&O?KXchKbN(#o{(3OGAZ>Y>G-gfm>2xC32<)OLE0zP9`0G+ zPhr-C$(YbM2F|uz_@JAYYsa@eHHEhe?E1`}?fA^yzrruqJ%yoso*fXbkNx}b=@;Uk zUVAwvH8jO-PU)9$+|rR`-`Vp5KC)>Q)^s0YGtm-4dM>-?d;tm;3%BkF#9*D)6g&6Q zh@l`o&(iJgB374AtP*Jp7AJy=z87AzHuSaO-nL!n&JST_{dgq82=kvbZOGd@Ny=$F zki$y;o}S$zrR;;bVXQc2elp*1k9EXL>IRX#XK@E5)3aTUMS4qXSStNE8b-taFpNsj z|H8v_!(I5>_O-|v)pe5K<&4C9Xehp?zeo}y<8m1Xv@PeoU_f{*QP)QwaS6L6S|SF{ z3Cw@+wDa(xGcHU>TXZm60$n#B`?A>u&p*l$`&Z zZ6+Cf;H(SqiHqKd`pW9mv>k)}cx3-}tUb6F&mY>0J>8wy*w(6uF**RNKvci?n>>d$ zRa9Y8eWPM@%o;OJf^;n{uPU`tbL*|WM1k}Q3FPRUU#oh!v$G91Jb6F9_0$92CVA!J z6-uH$MFMpZv_EW{y@&TZ_zwA`6xwZshKk@oIj_{-^NH_7_J{xHH#cGPfxWKWSW+*X zl-b&vYYDf3;ZZ!63QosR7b$9wsSvfXH-M)~E9Y#gg z4&4y>FKyq7mv-;OA0r$)Yc8JK@)BODpon8qeJ6rB3dU;0m^N-*iQlR8@I9@&?-NDA z3KX&YI0MQ0C17h)1jq1#S2q701YSQgp9pA_aAH@cudA-Y2d=ytS6}*uL>66=cqIpa zd1XT3)1|Ri6EowGc~?sSLPv)EW6wt1`TSGJv~?rj+LltkcvXl3a-N$F@cCy$*_^3B zOCxFzbm5|=saVuF0mnB_#JH+jAdr|P%9GE?=KP~~{zQTETxkYF6=m+5XG${hF;;TC zi}o%jO&=api-EBXD9ALSw{#1tOZtXTCP8!Az%VjQVS>f03dQRJ! z^LK8%vm$wj{HoaZ&PiNkoW$AJ=3yZU&^XJ^pI%9N$L3X3-Tn6oK^Ccw}v*9?p!N=nKLj62V5((OIW{bfs6;)|EO zUDYMx1S#NCwJh*;nA@JmjL%@m1{~3#6hGr)98OD!j|9$xa%{NnRs+r{NJ3@t+QmU!Cvg^>&Bo2vb~ZZQ-Hjz%u{FAV7(Mo8GV#m zg?yfNj2h*;tE(MfeE3c!LFa)$`umohf%=L{6bnqLGY0b>H4p<*J#LT=_+td)p5>IS za}ucE^}@6G-edP;zpS^C`iNM?U1d>VXQ4+?43?w7$(sSa00-@GAUpxm0RyIE8}!h_ zyI+^coQPZ!F^N06`1@(fT{~me$!&R@Xyw!mzIv%Y@0`;S?Pp_`7#`zas zpgj4PN#@ZYd5^ZL?GBu4BEGk`7k5AY2>w`M@yyv+w{;U<>7b=rCH|BT$7K`+easn2 zG3bnEK!lj5$nWDME}knRUbpr2l`>2o&}|&zr!G*^CtAv!vbL*RQ4Mxts#%1itH} zB%VS?Z%=Gn&X@!da}JlSsYXRzt^prX))d%Q9>e!W>_1G@2baYG2uy14TJa)_bp{{Opys!y*8MkiU zEMyz&owVG7^cbK;u&VHvEnR^xU3L|dumsMI?5^z=N$-{slgZWHf-?WDzk#FQfa^F1$ z%@ll11sz0UZbm-s&c>kr4VOoP7gYqHA2m4lG6UZD+6(ZZw_SxvElE4`15#q%N4^fi zb8gu7=W&QXwW?!4_RN5#rpkw@WO%J*xs%^s!wkmt1?@?XDf)U*IpR*L3xRh zoCnXeHifK9j|A%dgDS5=X$ktq$$hLuAbYq+O3jBfz?E@_>nbo@l~u>hDUGJ#JgQqe z6j09}?nLfTyX)6dDM8Onn}F^`(~7FitNXj3MAw!b2t)Nm$&gqy2VGVCEcA%kqUV)F zyslKM=MQxEV7RkeNzq9&ppLxbOXh7?Pmj`y5fQULSNY^oE*Y>DwKdM6;uHyn$V>Z_ zirN@Jn+;c_0N79}4v{WRcXsFqrQ(PIxC7{PACzI<3vj@$=Dj_V*D<|0=J1W5d0xPyBH2AHDBm-hP_O3`nU*LYf+u3%}iT zRkm>g>a6sln}DseO=o_SB>hW|+=-nXZMLSETGxahyy0(@jk=dbYU$zCPoOsS0y`3o z#smlGu3f?dv*buF?0=G?fA5j|@Z0B~cJX4VH^0|;b5L%j>#pk!)5$xIO{I;0$3UKe z^~e%EF!{yp;?Cxj^xZN5qS(v3uyx_z-bmNIabep)v>GFR__&~dGpzkEk{ptx=X?7` z1nP^AU5ew6KVHrM$arjbUfkvH7dEWN`i&d$#|-nQO~?8j+wqDALFUN56xPiF3K6G$ zqeMURd1uq{_ri@UwkEKl8_#L|JO-{j|A5f7uicn{O{`w8TyiX~ecRRQ%7d~q@9R-V zJI2E)oVn(-0F!StfE22 zEtxzHg_Z{Nzpxq81~d4pGtYP0Ky)QdphvfEz!N(*VPJT;XjX>@`YzPDdFN>f#$XEYDwb*P~lgY{Krt%_oxEbmKYy6QvSC=j!I>z)`SNq}B5 zT>|ul(|m_>p{n}5Po)BMlB7>KZ8Ezq{M?r@+y2JI@kF;lS;e_s->?|TON@%1}y#+}bTpl^hYH%<-b4cVqCKD1P_Qq_#WuJ_M3S?4&ZLwyB;_0 zdl6ma^T9iM%nTYkw?({geCKW<%dxxaesk)MBW)KH(fr6d1_g zz7K=#$#Gs*SA(ia<1tt&TpHp0zUbeifc~a&$6!Xb76)?!$d#3#qN(2bjVP3D;W{TA2Zi; z24|h!?UQrmqj&rS-+uA|9PaNiVrUJRYbz@7*DKD%zg~1DmQ0$)>R{T)+MN-BJY2An|n)RB{05uUv#Ar>5nWAp;#Oj zBK}^C^fD&Q(xM7K$%ejm=SXe^Ig;Z**!DYLgxR;KlXMsz$48QK?MSZQE6Q^uH+z1Z zJ_OrGLz|^B3|KrWqen7oh%Try=0l@lG#pJB1?Ycaffj+UZhZ=Wv*|&UHPmUZ?YW%s z*iOJ)Uh~FlI}!2nucj{-J9;a`(r@}3IMj}|C)WW?x<}G;Qk9Fm_(p2SoW#*}L#;qn zLp_$(x4_#X_&#N>c5QSLDRxZ~wCTH;V6iOd<9kA40S-9Iserr)75|B!{?L^X;*}bxx9(BT$@BZS zB=JIFam!@pVVU>t2!Rm;j`lEU#R6&GMwg?hr}FmhKq>?36_t3?38!E}LnAh|?nN)D zX`6IzbO<;pfpGUa5AS$1myQwfraUgG7KU)M5_4;RuZfo^Y_|7IuzV9*5n$lYc(&*1 zG}wzNEerhIm^RdtVDrPNoR*iNL=u{+tvTPJ6@^0w4q}s(l4mK7EW4|#8_z%gJT`55 zRoKFspP3x26e_vO-y4pb!ubaH|n9eMhV5X(936&Caml**T8ke}F$lJU(D5~Qo6%*(BD!QuR+f9x$&psG=UBCv}RKksvtJScf69X)na;>4Hh$RUSKAG1S(P_|jD^V^BV}N!im|@9Xwz zZOY5!cz#XeWTl#ZOoc2C4BNBjFh~zUv`^=Y2maETrE%W%^ z??{S4NdvFmv|g#h6QG|jnS~y$7UboA7b`c>jyNvS9DNd`cgZnGv<;agj)u{2q%f-D z{udHxF@C0_6@T~Q19+%yH)c$lf@u>bV$hs#uGHK62c4?BJrBL2vkw)6M%C33C$SWL zxT_aCe}1O}=;e|SnQ-BmOiEGkB`%ee%7qRQj&3eqTw?d6^?lEVcn~8`db; zAd2nl{2h-IrAWfOxZ!)oG`MNqYJBnGJFv^MPj6E8=~qY)pWdP(-!Ovb6wXPzmBc6V z;wwTwteXMZ^e@?_7g7eqe?5E;zPsu!a3+iGw>)$5W~=0!#PPmj0NTDQ7-HP{V6gGc zFp}m}pskfF#Z#+yb&|kRN^U5h zZ28NO;K;-`Ze5EnJpZ8H7s!6OsSaqYMG1k%J}FIZ>w*}RU|o(gApvVdN3BV~Nc%yd zGK&Hoe^zC@lkYnW&{^u_1zoNIE039ncb^D+#5j->ia4&$Nibt<&HE-aB3Du7_B{*8 zy(86>3739e-93O(+1{b;dyywQaQVDMwzqulG<40IY*gE$zy@8bxJQq!Mfb+-UKf+b z3!Q;JR9lH5QrFMND(&18{M_0xc>DOddOZ=^q-ED&FMiv(6P>xFw#;M+%ulWvkEupI zn5(TT_{(jxMJyF|zkyht+}u;h1UB8;=3IkltNE6JDaV!xR<;A;Fy&al4p)z`z8S9a zE;5NY{P$H~`#d)7+oQ((y`TG<0$!BwPu7r~`2*-48t}I<`EL2-8F=4GXRBjSZ9R0` zGOk~upe(x(ke&H;TUXVW+gY(I4yBT|Wg@Er75N*-Z)wG@if zjzfJ}R;AtE*NKkYAl7sq@R*G@Z@0%3scO=jdy(MkIzk(Zv?o13ohD-#858Ar{sK9Y zvq!U~&~e4W9!aw)kMDQhoK}$X`2MNO-yS=X3-c~k){&lvU?1d2t`y^I>J)QvG>nE~ zVHBYMnFsoKhll#`nT?O(`t8rU=ZojeLqk=K0pl8!4@rQo9mWkh`}6v?K6R9gm3kAA z;(KV1uLMLHIV}sj+y0Y!Qu$@FM;~1J0(y*?4A)&|IL*j!W4N;s{;oq?w)P@ z!fx;u1Y6Auq-^7y;bMOaq#nz*ud`i&^gGu+vSwcu_qH4xsB6=;yi; z1v7?ieeCe_ZPy(|t)@kpXNXifzz^=?ibXxDLz!tQ_~P*f+A{ivBXD&EfcQ zeJ`)%+%ebFr{a}Jz>+Wg=>YU;<0oLp{(bnL1O*66<6{LWcnq&_J|+4meDBBg){!7> zSXLaxK8wd7ubW7z3om=+lBM|4-+l~}6As|=IMmaPJ_|C;2ToftY}Uk#sg)q5sGa37j8E$9A3Ni9xMacce!e1wSz+#N9^Lt2e1GqYN@CuX8$tqb+WDDE0Pu%t^N($q zMW&}sDa0b2p^icOqnsHh3$P^cu+DEJ}c?P9etfKSocVTBt7H-bZ^5) z@wTTl%T+us%wGq?{xa*S)u^-k=OxZQJuoRuqwRuw0_xZQ_7>c%v@IgGmg=aNop?H~ zIsa1S`8_q?BZZf-&emru&|F!K(o6|P!)OS@C_w)+4x|Qn^S;ga`}GfCd%sl?)eFH1 z3m2>NO_ijaIfxswfL-1b@l@M2;DEW6nybY*Z||jKV#gjFxbHE82qH{=^99Nd&#AcE zv@9QrH&j`Uwizv&fGaOkL>Xf*nUi=n(;^H?SIT2$9cvts&#^0%sGvm!iI6;n43^Wm?dVAj|>-)hU+@Juv zm4>VI)^j?m6YpnzHhJ7HF>@gD&No-!9QqhF@3=Mx?qjd+yv~jpV(TJ=JDLX`=+|% zu!#Y28Xy;7Cq5X0&%ck4SxW>C_D!7d8`$3*w5LhX6B6~{`w}p}_R6cVa`95H3iPcy z`}@>i^1DT;gfo5fn#wBm7lI2Dq3Akf<8?`(eB(oR$R$!qp%ln|{ZL;wa`Y{fKY|Pz%PQ2KNZ#9fCuwjFtHvHhoKzr>LbiKG)*_>BTY*F6Z zhh#ej6hF=f^|7P52;-}3mGr#6q&&&YG}i&y-`RviH{;JkAXaB*Al(C>fV1&-X*mey z(sY+D!$39|nAf%Q0C^?=;t3GX$`^qIpZx}FgZgD9`zGG|Z}>8v*}9Q*{W0g*#n{+( z0H$(oeiJaKlLOv%{3)n2j`uQ0@L131DxGJ3-S#K7133O6?7f>qz(@ewd+8P=lNq0n zMe?x#y`eOV|1eCk&G+tw%1nHvmZ2pltYnzlR7#SY@~)>)jgM;}JE zXAq^NZpfUx!2oURnVlA(-fg=vuwP5iX`!r|HW4Kx6)(hRqep>SAU&HmVKVCL>v14I zsPiYAcX{b74B^&;+ptp-P)q8XJ#KG^AuR_U#o5ut_k@&%(j$&V?A_c`UkmbZ&l#b6 zyOf|mxox9L{kD_OQjU&_a&N=>@aN{}=kI zvm%P$u;ekrgl7w%@Y3lcZ`HAqLQc5yG$G)3pM=Fpo7?07?$hu706zVm4<_u)36vk` z>QMHJ-YC?QIao{dqw14y2}@QF~46-n|$a8g_k- zL?7FQ1TpACiWdxUcr~GBi zv1bPShVkRy8MVeca85~#?Rh51FC(}^IvwA0U?+TMZD3_t(}%C#`g07`RRQCgaCo2> zAvk~iqGkBk3$DOxj#+?gDYG+&u8*w~_hDh@#OiLcGk;XunTHxkX&*)K2jTk6D#aHS z2qftS#8ZI%Zj`|MAha>KFpE%>km^&~J^({&IX!-MfY8!n|O>sF#} z-ry_}KP_zr9-Y8AntHv5zh#20s~dXTac}!BrNywKp@<{7B6Y8m`_4tjuT%hh!`_|# z9whVGuy+@3dHf;mNbZ<<@j4mOZ!xi&(s9B5%+s;^!F*0>{!pkKReK)|CX7nZ|BM2C zs6#`2`1;mo@b&G_qEjxY_6QIA5vNtw<4f~S!#ARR?(B0q_k3t@!2H&#?IcC7J=BYg z;`qnMs)gs7)9<#`Ytbz^{}$jitUMOgGbd?#VkIFlK;5O&zt5RE2X&$h`^)5FSDDB9 zih?ng5?Wo6)FS2eQ&O7#53^S$8XFbug(5)|k%SkK5QPhFsB#`>n>_mL6E}Sq-@NZO zqBd~D)z?Une&)zEv|u@k5_=j4kO|^>gpd|XgwIW0(MatnlDE!F6Tu`&|Kh_&?cKbj zW7$4-+64;4r=_(&+>u4PaHLDg;h9$W?+t(s>a6V3qbTv5yuANp^+Wj1gTM3C=pFM2 z(2ky>6q|H2@GLOr9@$kKo1Xz5ia$|sZ&e$J(6s$#RopXS{i7)HoppSB3|6dGFT8Q% z_k{!PE)NqUiu;J$CR$Z{HEkszqN5X||7B_V_@*(~-*)(j@sz4_X^9VVBOjnAB;jG- z*iH@J+i%6cwe4HZtPg7f6jdxvgIo^568&d7e5B79wvyOl; z59@C8FDsJ(u%-rOMBt^Zc{WW7p=sN?dh1KL{;uC(_2v!kU7ZBiSDkhat~&i(MbuK- zJ$+qReP9=U_~Mh;)6=0eQF1xW)FpztRx({1W!yKhBA&MpG!IhUq;_a(6!JlUmJFuQbB7CD78Agp&{BXZd?UkgB9O32|G z^|joe3bubz)ZDW}c~r>1!Cm{+ABB>t3RE9E51q%1Padu2)Yc`im;0~gWf%r{K8hvh z_DD5$rM@|5+;p5;H^IX$e0_EI!k7H&-$3%5ap3`XA#7TGx&)U}bF&c(ur+Xo!I2t~ zCCiEHj(af*PreK$-0gEm51)cPu4!JX^UV^s`JcOz6Q&u&eV0b zs{F;uVRK{8K#y_&N6yMp!LqmE9c>VPCW(#P6vDwf<43-8@*;e7;aNph*0(R*0Tcu26bi=v(#Khm$DEH0h_(-NX=?8sZTR+szs0T3t#+F`2cw=d zI8#4;FXFLm>uIhuA8cD3*fT^o2g~%Y2g+T0S?{v%Lit*6yi~)C1#^M{$0%~?t-~@a zaZK|kV7u92$|&|EZXMlG{-pMv@9qCz1L|WN8*#9`z36)-U9%E%-ZmQ{0=!Y=4;(+U zMBF0j*t_@}MDesS3G@*383Vd*5j^IK{hp?ncfI}%xc17c62N>&N~;g|bYsA(&$_Bb zDGPue(`BWl5}4O2M{q|X{=&A|xO>_T;gdi8E>>@`$L#5GL|)cEc-|!%m=_6~Iu78O zgM08|d#gI}fsPWYkkVq31eM4|;KjW=lr)NhvJsbz<-S5r)w{Z+3>8AFb>?K-+YTK@ zUw>Z=&bebaV~-v`6AsW}3v z7F9o)cW+&V2I+-@Q*+nfupj|?0Tsgq4D8&8!5w>5KV>S)P`i9Fx~7lS-O7Nre}_^#smOmr_yk+@1!`?NALtw|d6yw^M*8JxQG99XNjrCFW*Cxbw)J z{ka3_VL5PsUZ)lDF=}SqSzxd8Ev{njr|}sK^z4~E+wqzEf2}>UyY)|}nON6=_nr6} zT(V%9eiHNz&x`NLbS0^*KF@r;IK>f2DA5~!8R=Y6La>Cme5)L{7T;+K;glTOE^2kJflE{oJUtnU!7xX$g zALh<~!cd zT3Xb`M`x_p_6?&#Udc0Wza*U2R19NXc}{t*g*ldou5X{RP_>1;zQ@3o_XrD|8<_$! z0k%CH4M-2Uv?=*of}74jfB%1fUBw|4_Z!}M4My08{j%K8BY2H362g5jwOm#fkDpM1 z=Xr*EY210>lEqz83ihQ{cf|JTCIv~-KX%%M8mOB>y>DlTpePNTn|isr%HMhGe2{@3 zzaRYPN-~wwhotiUk)Ql0_Ou^%-`4?Y3(_rsir_H^3f%cx6ic-|@NR44@`M9W>iZf7 z0M(7u^IbaYd7X^|TaMpOL+hE0!~V;dLL(bTa0xq)AI09ngLniCa*~;s?>`Dc^c4g#d1Gvyp~T06?i;v>L`vp59KZIlNCYi3`|%a6ewyy#vGMs6RTN znzUOY3#>~u*e;X%R(+<-f$N^`Zgh5ZXr`O-#_lmeQ^IF^uM%C^;M>#9CaryMc3ppnknf4@W=u5sE~K(qDRKgoWBQ?oS)k=4W}5% zx%>RA?=6_LU|2BKaT+T0uIRfQgQp)LvQN(84OdgSlJ^=#W@g9dmlX8 z)YgixJbV|P-n$*HQ_NVbmD0zzEj?M;nUj651Ln3Lg>ZK39uvSkBVd00uIFMqbJyvB z2f}#}NCNakNQvZqz`*CyYCZxLD^@s`-X)Ds>A1RG_ zmx+qdQs!Z?Znn=~&+XF_Wkg8OkBlxNh9ybUZ+`rtWZmRH%8?w|r_=hFLQf^u;$rJ^ z8eJN!vt(}ATv;;;)L(^R6rlf!1|sH($hm&oGx+?b$I(vqLD1`i{a`gxQt`oQOL5Ma zX(~QBjZ;?{;~Tx(acH1d0di7pAF|+_BcB-S5=9z7oQ1!UV6c#r)L$}ELN^q!N9eyag~%l5OG5xFE`9iDMf!KJA}<;=B#Ez zY>=A%2WA|HYo;A*Ql^7D8*~u?*hP`WeT49Gro!4?+i>bvCg2jZ`oj6#?qEKKBH^b^o2%D}lNL(*`7KAPzSlc=@&o*6X{c z_8zfbi5xm}WyU;?n6S3rL9jk>v?UlrA|&?QdzkZX*}fk|iSO&cIJHmqUj`}tZZk07 zxIm8N`Xu9Sa0q{_&{$K4_8xW)y3n@rq|TlgkBQ&zADz^w7KI>My+iOVTpva(FIJ7n z*Wn5Cg8TkR(w`AOPL2Gljl;iw`0rDJIXPof4&d0g2!kh~&u9m5)sk@IS_K}Vm^P=` zdjiS?%5PkCkMq|+6<#rOF0Og)>(w=Fz9xuSuafce&pq%PJhyj;x*w2xPFs7s+LuI$ zm4e3ro$d*6*GQmVDd8!Rp(IW3l1!U!3D$)zIJqH=U2G| z_Ei#FlSWzZ`mL&cnQS>4PB{+U6C0i6J@n-6T?6p0?9PqW5J}BQ3ywUo^EE<`0l)*_ z_9|j9jUphJ|K@3s0(>(ZC=;-aY&sr~yCw_C1|v{_Uq8)avYf z$2D`8G-WfU5I!=bzLf%k~CuyNNkgFG4jVH z<3?~K7v84;^)LM5hq(9oXW)nqx|+IsKs{O6;(3SVr~L-JJ*>H^Mlzc!M}hjQD2xL1 zKe0d`gMjt}hj!xg8y{C^n%n0*&ec;E;jI(rsN+=f1~lXiIH&uc_U^_Dy@!4Eb|X#C z4Gt)^zHI-nlX#0L%?4cpewlvX&+gQ)!4N8Z@?+m*=Apk3_) z%70J5gtQ-SxbJqIF9%Xa6_!6EDZcGWuIB9ef|Wtr2-#U+I=4`ollHw z4hW+dq}SyFR%_mq71=0ke-lz`fzWb1{r_gP*+`zu3qiXi5$R*GGk|M zh#{|EQhwWzP<=G*K`8kFPaCU7hD2YO7k?J_SL8ET1b-%!o%x5Q#QdEXUxqOBNZ{Jh z-|Nn#PtM19HrQ%cRaBs+s#-Y@(-jdo8o@873S zRIrkF8J8U3ks~~sTltDI^i66;p)?Z%bH+S&0G+_OoX-}V=lch6;Hh=U$t#7@$_h+4 zZMo9&>aHqwkDb2dl@vcz zZMQ(4;*6I7;*DcxqNS|T!$6Vi{s6S{`v8zz@ag>y)0lg%?jGK&xyBd(Lip(&SWkFq zkK-wy941@+-lPrG+EXR#9I(s3?+`Fw_XN5J2NL7%gh?~-{u5uLuCVp7D)g+punfw> zvorsF0{>`&J#>pTVjyo9jqn~&w&mo&?f;vx>=m;!|9!&ucD;blYUEvHrwMx6n8|qe)FqfM!K(%4MEp})cQ3Dx_8-C>9lMmoJST~5B{?66Rots2K(83c zIn{K2T$%>lnz$J{+<^nPKjz0x)<7n9X1ZzN4{?O^fwrXG7nQ}ZDV zdnGgBTld|8UqAhrmY!!ajzLACU_cgJS#gaL5_U`M7W;NlT)_nMAuWvu(vdQtVn<`{ zi$qN2-biiKH?`l+1KK(vNh95{U3Lt(dg`=!DM`=w4g5jv)2k)u>>nORSymq-rAmrB z%Byy<{^H|MGckng6Z3VzRoZp5D4Cz_LX&S4l#?iz6Ok;fasVem`8!{Kxne-@FgQFU zfq5SWWSbM_9z{SpIew!&%gf6A`haHCAta$$^BriYi=n+S%FxTt!tf z&tkvBa)P)PpmWAxS63IhdwcYllb}3fK)RKvD~$<~=*nxQ1nE_h-DRD<_4M?hv#V3> zbG5a#Xsp*v+u{0Z^i61Vdzij*33|cvq)z)@S^rQ1oKtWBUDmg&vLxY1j+oQs^0rc< z&4E1!(DVF8l`d0}MbjC}(LJtS?Y|s&r`ey|^UWgz1fz;l{w{2p1 zJ-u%SK70Rfw4J#Z1SZO{@b{-)h_hzRkG;Diu4jUwz{>5)4dceWFDR*bBD~y<+-=I{ z#x2fa1QRFXkK@P$%>Q}L$$t{SJPiLVLHhT1zkuz%2B>F564;Iu_*!Q->!NN$OCB$oz~)0mR1eWdd4fI5Xj9G|?wUvtsrQmTHMzy5|5 zwa<&KqhUQ#@)~s{_v#3v0R2xOSk?3&NG|2aUVKm;cVV5tQJ@b*i}j~wpNKb2oQu7E z-42LbyW4z$q~`@gixjZ4uM0P{ZPcFIo%DRbNYjT<-QJ7rU_J&V4y1FS9w}A6gZDh9 zh^LxGvr#pBng`~2db-Y^KVM4ECm^{J>tS}NryEc2+=6A}r(#CqSP9Nak}lMlxczcp zelXLGPUI2`)>O%m_=jUw;L3^fQ^B~tvc(xGdutl8Nx>5HEb5V@^mOfz#OC5pyeB+z5jWF_0hu^`$_^O>IzRni>HPFHPk3?O zx_E`MPoH8P$qC=y*Dt$BJ4}5QcrId}9@o<+lGN}{sq;(wcHzT6{10^|YWNKbPI~U! zt@C;)37WCJw32sU!V8ZvB6yp;{QOa?HClqlls;pQWnw`@QS$q`-*Gt!$7KKmD@V!LHX&w{(w?z=Yb^Uhb3qw5A9x&Q4|*M-j+y0tiHC+2j)SY;tHe_dE|S6 zfcec&Jr9K)R8lTPJX(F=(%rfVIkasiL&KJ-w)J(2QDAX&;b(@!9JU{Bz9<3E*Des!b{O zt$NY}=M-Xq4s>Tr66c^R3A+4AYWM@YThYDtB{#RnUU)jXnrpQUdZ5}KF|ba++{nFU3GQe-^5r+kE1CcJ!G*vJOb9 zxfhj)i=-5vGZfN(KfQMczVq~hcxK;DHr{l3+RpqWyzSVNl?I4kmT_bUB=WUwPVh@9$F5^zZF@{s=L{wp!ZB z{N#N_-=~m%#G=2Sv1FVPf`HHA4;2g@#dp{=XQk-T02aPP}K$U3lp5 z?!@9wALsSsX5l}Uo~O=AY#r=%cIG5apeudCT%-X)*I++xe(nkUdd+GyAHNXgHPy~J zTH(u z(GsUoK=|9d6b)|F-b0WM1FHG>vqLBkKB+@?6=5#sOSq~x})){8rf!%gX8 ziLIWG8Su>>&MT|~fc#t7&tp18@OB`i|DqVE+rEnYyt(8!o*`gIu)%?IH%82R&uN-4 z5_mbshsi#j&a3`C1A&6F@^X~OHKkNahld@JtUr`yJhFb{A1wsN5pP>CK3P9H0uEjW zm{L>be@GZ6=A}RGPF%DU*Iw}stX#Cji$xOSZewT8Qo3>M+QjChvNMuSr_MFcM~dliV)a-lF>gUOQxeZXw$`~lNyO4}2Qt~Y-5lszV6Kyj zR6v{xqD5>w?o`w*_|629auGjvm1i;+n1K|OFSC$E@yIOJG)+e#Px6RL1jq#M?5bt|})xkd3>7w}565+4; zQA}jOt>8?<>&)h0c4+QTL4X@PI{?3p|3-0vEM+kbp7*t`LJ z=Blr0=<;RdTRPzN<7VR;DN%QhZo)@c^$&VZ%l0>~UQ-aa(mlH+-`u{?p4p-{z!vpxnwrB8#O^-ZDop2JKtIo%s+>OIH0_jy< z{iq(yI|p2rpothbSI1pRVw|KKo_F^sa`xshAfuw90>>VAoVq#*oxIYqL0b;)$L0h3 zFqF?Z5OC`BIhfTv9(lPk4--fyDSAeO^inA;I(f=0{QIu8*d;dsM_G$_w-@D6Fza#n^1fJ#KgEIPL4DU z-5zh_TkqmP$O|c+4k;jmbD>!VJXZ1iqtwDUI3!Q+Us`3(Pd9N$ivDp4(v^KWkji9; zQh-BRTP=Vt($-n!NIu$ajqZq^9+K1 z=VO^OO&Ie-+q2tTZF&YW6JQ)J9tZp?2jSOCL`X1j;`+msqwVIcTZkCIM@o{O9~k@} zsl8JkIx2m^ugp8kGqf9(5LjRKUx0 z&6nE+sonV{icXKnTCxo8P4!zE7Iy@s>@Rj`y!TOWBzV)=!)P z!0Te`ssL4JXYR(d`Kep(?(H>Vp9H~jT!+$f<%3}VziI_F6P_YJt(pFvHgyNe4B^hWvi>DFEq=A;$KqC z<2wVv#R38KFZ}!mxOeT-!Cs>8FFWZBT=TlieV~4{Yc9VpmX^gOklt8P{W+$;k|3`QmW`2EYQ5FE6w7X8OPy+@~>Xr)4OfG9T+U+4M5kDbM74O z`ulFfFD1C3$ZuPAD*oogv(!1XpWXfx)^)Uc>g@#7$%A`szb2f>fei=Wg+feNSSeo& z%40%ENfV*tpK;phD9dKGw{5dHY;N6$4f}UvIG>N#Y}3{Y<}Jn8x&{r(Guo^Bkbnm4 z@;b4m0iRoX7QV6bIecNW<)ZuBAW6|bdCUsDebRidk2z6WWSfWqA6xK`dMa>mEyIq( z2XN6>{uw*TF3Au$Q=7-)hIhOlr_ElN4nTo);44IOL1yh5i7)_AyuIKcE3St}bsVv* z=m;h7vUn3Bt@gh53FZ0SzFgEY3E#i`Z!ooCjLNkDzfbH&s(!Q0DeXH&hM{C_s(IOd z^Zc}F5R-uVH}1X_zkKpxlQxPQWy-RIpJS9nz9ibF;}Vtxbz;Xs6jKx2(mzT7NAo5! zQv5>XZ{p_aq&-pi?QFjAfIS}%{%ywPz|J<<_&lu1_ra&*PdK_!W&fFv>+wyD9 zy%<-Wc5Zt6!ZoJ|an~mS)2%Q0zu@D3t)-dZ<^n&=RoIv1$2i8LD<4!f;&``o=eSj^I?ka!I^^4inXwKG$QPd3TNjA$CGHDy@^(Ayt=+I^St1kh_M6tmfR zXJ?XglBSQ($bEg>0*|5ReG2%1+mkv2{#oTB_H@ow%!TmJGxwzIKnkEn0RDB&)H`ld z4pyg33gAzu$#bw?1jO_9%6n2{|LjAzYCH2_gPd5~fX|-&1}vXE!|MYvV%u~aQepqb z&NcY{-WT0|mkD~`laO=Z&Ze7tWS%E>Lc3lfjvKcj&VXpTYiBM~e}9U0=Ks?`D(~8n zT$lgC`<8L9g~zD5J=LzcY?%*3!U22~=V6QCdmO;E3iX{eql!#IV!T~7VZM^6duw30 zCKma9^J5Qc`}D&HQ}-4E=~utz0_97@TgQ*I_NK)R-`OBW?bH8~!YDxhBLxerNu8a} zG+%XCavK?Y&(7ylrL_FpQffY}ver$=!&1_IXt19w%#E#iEH$UU&)@bl{C4eASTJ=4 z{{3}t!?@ad3`@d^&XL_e*n^+$ehK&N+oDz7>Buz+)oH>_cuG!d2gFI51*R7GEjR*0%RUBSO8Y+ zeaOXRQ%8aHuS$@9_)xq%$m^+d7T|`ry&qGFdm5Bu`y-U1r4jKXwJSj=1Z|2Kw|T@c z(vFxYD@rSh0T@{*$%7+Ts;UR^abXy+EIz&Ja)(ZeBq>!dh6Mla~a4 z6ea7J_RKy>Yc)C5`ovHF3%lfoYd3kfxg+xG;2XtU# zn2PcGUT5eSE>_|mtE4B(=XK=$?8XL-6Vpdg*vY}TaCOOR4V{tH-bo@_R*}{A#;lFb zJ2Y>9${5VEbLqoAPuPJ+$urGOi|maf(&D7P{W?<03d?(!1m)MhIB$IOHd_2dXxaNWxc1&?P{(K>_sYa+8>R_cIO~Y zUdNSI!~i`Q=XNb93Sd(~c_4*85)fw3E!gG~^0*xTxHM(U&EvcBF=Bm3Y7+<31kAtl z%tP|JgY||UDYX?9xN^yfc>B_m72vS>Ajrq9rNTd=jqJ?7v1<*wnVmUjpn4x5ikCbQ zzlqmQK9TJ-VZdIkv%*iKr&7GnUps3V{^?KD0o*GUl*ji?kEpZM6CnbuwY2TiaM>5; zh2E0@|1$|rR=_u1g1_IX`&ba8vokxW`E8jk5&Q5Hvo%VZt{lmw0pc_e&BD!(J)~6M z_Z&J1k7|mHBRL7wFFW~+WJ10#9El-tq&UzX-&?9`B(43IJJ$Zv!ze)i1BZeE<|JeK z!p6t&jcw1Olg>1!{aeKA&y=b<{M&*vaPjzK66s0J+moJ0vGn|Fzq=VfTfGXInkwA? z;m@E&5=a)D6JbkNl3;J?ZO0d$djNa8+O;HI5;T+gWA#8HLikzcaNs-!?c5ohz0R9G z7jx&#R%MW#<&)bs;!r^nP0JUcWb!x+-2FK62inyFSg?z*Pz_Ur&V|cXDs|UkBSj~I zkM4(L2W92+8<$;-YDvISnjh_cQA*RFz>fZIAcUs!LTc}ylt4Wd;HE5oT!P3q(U%+C z1}?*OxBpE3{nG9ZrYwOgPdo#kIRA1?GeA8$N-%z;^4PwFqsb*I0)mdVOa~B6srp%z zQVu+61EW^dactMz_Cq|fbF<5e)ZPiGlOwq(kx`)DhIA0apQ-Vj&m*=aAGsy2TaXOD zdh!u`{jOiTW2q=^M!*4MIB*+*Ybl7x2UwP%fM8ky#l8c7o}?o^$8aims`tVtFr&~3 zT%ZfV>cNTEG?42_s~3g7e$&q}W5kX>+PgCK zHEs>gRcA3cwgI`Sa;>Fh$EO3`dY>xq>_tUqzdo|eHc95uJ*LJm&`cYx%DKF)?HxKQ zfZkac(&{ylJ{C=lt4Nz;kZ%AbL4V{K_l?KC_EcpPpQC06-r8X#=q;s{_|SxfuI;wI z9ft#=}> zmfg<)e+Lm}wJE@VDZmoR%jwNi@RdIU2XL=cAVTVqt(Q4`+yBG zhIQXr2fhNE?_=KEJ=VU8cxD2Ob>cpXyN%rM*qlzK7jYy~cmMc|6Y$!W8Kx>x`Z~(r znS15`<38(1j%}KT;XbKJ0`-gE7z1^__NLvtrWIMoi`7d8ZJ%B<>PYU-FqHhW3>Xcs zc%W%z?Zh9F((<=Hbt`_pccTK@Du{U5xn|~ZxbgUlu%y;VS=nUk$xBIv7JB@?q$=1FmTW!w`0<|gIIVFK#%HTzP{P3nTfjU51723%W`Csb%z@Wwa~3Kv-qqiWb$fQe0`zrz zcjATze~X>%2eEk4G&EFJF@i~?)&)h%w_Aw4=hwK&QKJ3{a5Pd7NQ2!B`h^>)@G51R zZS2QjcRzJQ&f?y>^h8XpYr?wLJ<73Rm)yi3m4Kb3>-2unxXG>*+fT4pA-vN0m482N zk132PM*KY3%Hyj@_9KP)lcwUFMaQ95N;#j9QuRd72*H}D30MK)GYoh;1U!{}+qRgf zOV&bo_fdju5lH!75g1{M7__J#QA|L4P07SS+@D_Hu{9kZKgp0sZYdE}UgqWeE|n7V zatY2eC5$Mr<1e8S&pJNRb+poTTb5FHMzJ6g^^2eSzdi6Cv`d(~#Fzs)fLnrp{m@5o z?G+?7kL?t#1GqN1o(|`ST^pie<(s)qD%{FDdudrp9nC!-^Cmpgc>rJB^cZ$ZrY)(6 zHVjLtaLShlcu<>U&jbxvO88ulpSkdf$GMe41$K zqdMxWvlGL_gyG-I1%-$~x&s3lEp09(ui}+iWU49JQXH7-EqFn7^!tSMo$ee+$ z83fc9r__!2`d)!mwgm#vtvfP~kZT3~&d`;f$#pL>e{vC~<1+Mbb5;InXgmSC4pTAFeUk@P-Ie+`<39rZ7rYuyTlTSh`IU#BLkW%rqz#I+> zefAeWz`nM_YV$9fIsvQ4lwf~h5VK2bP($aby8kTc6k%FzBhH$93`*rr^z8nfs3^n) zMdXMfE;YB$jf%U>qf9QoXPcLAlMuCaszp?({c(^k^E}+qjVf2 zm*Z_K&y-TNUIotW`*nMF;D(2OCxQAQERjIHzOp(MOdqjm0L4<+vBQ5S>TTnUP_9Vk z8eEGcFt4K!UkJYri^fgDdsm#P-ao&8k7D!?Sike7wF<1CGW8hkr8n4sLU3xYE_?P( z6oFV`oNXaOs{JpL06+Xe8|cbe^Kkj{QzY}HAM5w+@&I*c6UQYkBCL17ZTI_-gG9mi zbl8=_BO5LAiI-)FOE{3r?gbKnUhs60simz4A4gG?3Oackl~;DjoKr_@@yb(ro4+Y5 z68yVGK7$9zkYQ`1Ujxo?VT`qi$AMv3ur{R>;-uBkpp3xy$KLTSeD1y1CZy(l5@^t| zkpl|AvhQFB{Dh-J`MaV#i^jS-1@el{c|JbOm89l3Y<~{7?B9rCxjt9Q5jS3PGRueZ z+L@>E8m`uBNsN{p=ukv%K?2sD!@aoc;C5BJd9`DZAohawi`Vz|m!5L~y0Ain2zi!ZK@pqRLtQ)`GC$8FEyZ%ytu3HE{X6caRiziJM}fH21AWC9w6s&NX@;v zwG*Y1a4*O`<>0f=qi4rnz+ZH1JG+5AE29kKOYNd3{MS;Nx+a!9+=$ zeC2|-DkpH0>zhm;*|bn)>E`(TL)Y#?4qx5zEIzvFVFl7PVD{GwJ~u-~9bd;R^OPTT z8(wX&Yr3~i%j>kNdVFQkYvkWqm|j(nS8HgH0-;N0F2Yswm!o570Ba8Ih3%*`24uWX z#qZo(_wC|uNg=YG&-Vv`#M*{%z0L4)c^scM+#&(`4|hF}Jp;X%FW1jHVsxgg?P*}W zPSS$s9D9P4lrx}iH#ix6+y1?{@t!-BcHaES)003wtY2ENZ7jmvw}F6qj}(g0+Dtw| zM#GX zBF>mO4+n<_(0cHoIqy|V&9%KAjI%f{IWJ)R*cP03+L`E!a=7i;)z~@Ejdz^?I(+uD zi}Aqb7x1&+-iCfDX~{2`j;L8~Op;hfa|3XoU2{7*KsV}tPkSqzOu@t=OBt# z!S;bj**qPw8v^1u3aeAfs4(wo13h=~@lvWj1#5O}QPR^iA}+>=Ng^=O?~LGdeJ+^O zBEY<}4&YeLGLBr)UnYvZts{!db&{gUAGiFg@-NHFvH5!KV2lC3j(+=tUUY4>91OwA zN-&Hf#5H0=FG|muHV_y~z7z>$TmHT`ycz%gcb`lgn>I8&jJCcW^b8DynzBqm z!Oy!)GA;=0S5;OPo#U>F!J6_+M{6uKm+zWOvS>!BmYN&&X${b|e-NcsPR_|BXcbb@ ztQZ9~mDtd87!S++h-}5DTF{V2)Umm00eV47<{{fa<_c9+RdUTP!yvWa>=lBo*<4E_ zCkNb+84zUz0JG&vrCrsXq-X}z6Zk5T4BeJuI71*Phkuu7QhREP7avj>unV7EbsKJ6_qYTJ1M0mUW3`eA`&Y-E zqQLpYx(1yWYo}&Hl<4+yMumy@&0TBp_ZwGXb$2VXKc+ZQlJV|9Tr4^FW}EpYE~;Q_ zq)Thw2UktoRp4XCtWZ+(m5mcH8gx)Vea1qZG4U8Yw08?ShRM9d|0Z_17Hq%u_IkmY zwCaZeH{N@P`Z-U=$#MYghzYmQ`elai?DG_c0nzk*N)R%j zG@P=f$x|{$!=FwV1?aCpplP5S8OeFDw(9_{e)bN0cgI?-&dzWP#Ut$xK2v0Z+qeqM|@j;B zK%Z4orRkffRF#zG96xC~7D&z=soL}t!DSr4S#WNp2P8du&B~K8b?Q{yw|*_2 z+`SdUGbf;A**rAoGWf*}ZYEh?u8%l4MMq zbBqM&841oK15R{1>AJS-AWo2*od)Ut;!5EAb>n8CO9J$@-3PsoLZJP&16%QXe44gJwO8qylSD@T3=1=Y1irb%m z2AzX_nASK}b5M^EM9RHacUCV4#)L272;;fsci!Jg#pQ}zcRyext(r@o>J z7tUFNE0&(9&Q3qrqt7dTzI6}o+_)B95(u9xCF-et%&SnWP3Ac{DpQ+&AbIA^YgtoXOk?P5o#37?S5h?Gua-^zNl=k<#H(IN)9!B_G@ zG6(%kVr0g98ZqG*PDQY*?E@Px;=hFm#tlp_AZ8N86tlZmIXT8k3Uh?rQrwIX@$qYj zH44&MWl>62oDJa<=yZSiS?A*4|Mn9&cg4v@omq#xT+iEjd(>FWxzhsDN2GmTB7sFi zZ7r(hyymv}scF1w>oM`;-7l&?lA4n*j%ji~An7@*j|BtJX)P@j7*Nkg+H^@WMWcfq zh*}RrG}WO`B21cqfg_ppKQ&`CD5*`)kmET$o^m)GTsSp z?;_`)Ly`!tjEwZRx)KFCs&BwZ;^jMX7^l3p*HEBIdo_O3+m~l^US;OMq@$s{wk6 zQW6sh5AJ%uE0JwHvuTP3uz-<(TObQhO0EGn1HKs_D68}d(5(C1O1S;~EtYm?z@^eL zFn4vy_<%hVc#pUB$*?-v_aQ@}wWkwbd*WVvb@e^kJ3HSG#aLTkftbJg0 z&-g0-B4#|0o%uUoxEuGj?{c)37-3$et}O9Jm0(nc%aM(rfk=RK8sp>u?w89i#d$3= zl#2IgNQUWkjktE{X_zLjYY**HQgsK=9XY{1q~Lp=|11jc?QekqxclkchZQN5R8WS0 zFVs5xRL6e&djDorNpQQcwmAueLjcW)h?(Oj;_9<6z@+9DY>+fJsk-w`bM;Fv;*O^t zm6G==DNr)TpfQAgKa;lB^lm_Yj{G0MmfM1|Z}%0|ym|C0=(0R8s}8uV&)b^?R{ zxPBGhv-U3Rkia@7()B-!C6FXL^S_?91eIp-cAk^XDLE%~(eY)K zUU@bhIidLF!}mGKLZPh-<@2Tk6^5XcS@sl$@$kT5+|_?jsl3lDt5zUg=M@>i9x10c zr_Nu5@l8#dt0oDQ`l=d?YiPp!=`(Qh;^VM%{sKI`Ya4#M?rC(@Wg(863o)i1(MvnA z`I+aitEL3G>5^kyk=2b>Ty(SD(Ukx@wNny`8l)SdN!}C5Fl*6#MN}1xq(5TqZ5tlM z&HFc@NA}6o%35EVF7vD}Eyu+Yq?6kF?GlianmY%Aow*@=bNh43TwrcZqvCRVYULbN zrb(256yPy(5GnDeHjTvx&bR>6Nj0X3M z&41f7R#iT>Q3p#GOV9;pFbF?ya7iYkQDPvQXp(K1H%0{e3+WfzPYPJW{}vIe7uN!C z0*x@Zc^6cO7v}wrplxUw0|dNc{`~V8X{bS7Fni>Vtj}*ctQk{wE^X<(~E3G05P34uS5~apxhz5WK0|vF4y8-MT zP}gw?4Cl7&)@tH))!5P1hI`gOhuv+5@WO#Tm{i|{YAdy21fG?kM|}gvUNficKvf&% znqDEL$<pJ4#p#pCbe=RI=F{6I>}Kljk>c>drXA2`UcY|;z`%rBXD zoRSuXi~%n{+Xv?N>h1&hhs}@T8&Y!KlOKxLWyHgmGv4(&)#kjJDmz}|G}M#E}d9Wi&>JcEJ|N_`RRL-q+fmZh3bC8 zo}H2*IOO&&WuN}sQ@Huj2Q^TiIs+bUT1=8&gm(4b@p&k(Bv|Z}eV;4j6;K~NKmAWD zi~{ukVX#M>(S*9Kbqn6|^lxy_!5!+IU_>IRSbuijY50$0&%?ATb_`e`HQPSWg{P$S z{Sc}CSfC&!0nMcqzJ05iYos#yy}NFUB@I#vJ^18{O3G0(ajf#LYt>wP3pqR{rRm$G zRDDiajU@6)w1e6xwo<2Tn(G^|V9E?Eoizu?&YF$+(`I69(-^GTy92*^`Vs6L=s`4p znnar8fqjQiSi4ytk2Yfh3X6x&ckKfRyH-yB|)?T^3y5P>;#;=9u;g z51iXJQy;x385fTn-XkSRKd7<_fK~2R}%FGqxK%> z&(GczsSB-Oaf^i!$aXJ~leqVvejcVawkTD14b-K?UrM5GfALxTSoSrkbT6JTwRl-h zl{u)NW5B@Q)W8T65ff)41rD|xkpu+rJl@|4oU4Eu|MpAS# z^&r4P(sTk8MBouv=NPV7YlI_2;el*Yo6InvUJUSylz!q|e$sk4ecoccVa2IRb#1-; zrWpr@V?Zs6^tbmkiX8I*>7_D;1LE(?zH^wlNIvo<4*Z=ENL4Y6(rUx5LHpc0%PWW? zufB=OxJnfvFs!{GYde96LPEm@34+}P)(~OpMqiz@gTD2#f5NqIe!FL9PGF1F*a?^q z*<;g$@PU8O9vV+oa$FKHFDonasx?v&!S%+6@4+?S|E9{GZ2C(i8-}Iid9b$+gAyEXZ$E^Mhgo{1!jeHi(sT*hg{&7bl1X)+&Tl8jpQ=1)Kw6W{D)rVZsVK|;a@kT8 z$|U&9Y47fohaK~!%?%o$*Ht=@Yt5?^z?Go5Y%r(P+WQY2PV`~6sX+mBwI>O<6G}v6 z0W-7&UDo4>0G*`!vYpmJoRy$cA6A&7^?EmLcObBG>O{y7uTf7!VQftuOUvqJ>jmb)UjB9Ghw;^&&*C#%p1_{L zUcI&zTbDS1W6uR*Qz3+Fw234_z>k3bGPk4!X&u15633=lGsFU-qW@Vgr#Rw@^5+o< zsK0*3LbXObxOZznT*P~ulMR>%F&JkF?6(O4c#vQ6KF-$z>^2yG7O~BGJf6t}^Ui}? zv8}H|Nz>~}na?9O?%f{;(ei%9+=aMs`N`ToefM@RdwNDdef=(SBzKHs8Vj2bQtDca zrVF13La9NySHzCwMuGaDN*D#`FEiNV&1exNo4_|e`5Sy?%ackvtEEZ8t?1+g?v|Br zz-z}&_ZEA-2Iwd4q?#>(CxPEQVsNDwnhYA{!e1sb9`K@2E{TYzHoPct@&U%-hw@-M zwEvJQqolUREh@C|@5v3|*4}+Mm>wGQfxaz?$sk7JDk{)L)E!UK@X=El|@GRGJtOVAZc5Ba^1E~mbB#PJV+vQ2r6;OZX5iL=lFeO=V3LvwD zNrZIpz|$2~-RD(?9m)|agZmIS1KqtrGO;TzN+l}7C$1#(O-`OR8<&xNx|E_@kH4D_ zwL1Ish2tmrXFRj?VS9xTi2^NAOay5m;!eOyjH-DRlCAuA>!V*C76t={? zt1~0i?KQ54Uk%lML;PBXg(ePcTua1&cQ(t+br5*a#2as$A5#Q`n1LoLT3t;w-goJn z6gUqZzzs;CN$Ttaxnb5PtXe`KX=h%B`r2CO08Riei;SkYPi%b&SN+GAamSh`l_&8r zO-%|M5A8dMvIeaJ-Xj^V#4b?2#GUj!@+Ij;dhR6Y^iHN79^ZsQB~#m%`3w<(Hav{g zySL(jZ5yy+%1kLu*M#Q%+tzD(x@?Lt0pLo_S+! zog((AFLD)S=pR#y!J0}5&hyHSJd%upJlT*(+V@FW%NSKBbxa&{5NCmU>2Ut&0J_j8 zfjG5vXR0OpOlTGFjASYdc6KX=dOXAceZZ;KTL*LWeJbl}P(q%?={s!Ls=u3TU**&Z zhzOt)sCVk^+SEW8Nj$AHE1D-M4LXqmxScdwBvso*0E}Z`4+m=D)YLQHQE0jxkx-M@ zWyTVAl^y~<)4&Jcw(Lx33oif2&ir$$Zo^Mrdpo!h;tZQHj;JCZweP)gpXq1HN*6C$MzMM6lA zhWF;Zb|j}6mz9Cvr_u1o52NRxA7!9U${TO$Q0ylYee!4C_N@;4yu&_jO7;awuF#g|fZ^7R7!+P!o{gFifzPh-g z&UhLY-}a#b=R}mK%0~%yiBrdn*f%tBO-AvziDE1F0+~mLF(F#q~Jx2mg6NQ|f13+7=!5>0t2Z6=_Y9ovNL^H>zDmiSh>DE5Z9Pshv_f`CqoG64~l1 z)n@V@-z&knQgJtZ&~)lD6vosm0vCmXYpXpAofs(P64-xqft0pNDVuff=)gw^is=!z z?KW{Tj4;>tU?5cZ`SN!TtTK@AsktL~XUewahj!+y&ij;|`8{}0O3V}B26UeJO1yX3 z8Rk55c=b~`W#W2;42XN%cHxWLpGrN?od5QM0b5a&zE9e^Cmdhe80^9ylWE+CW*(1g zXDs`@CFWE%(Oe|4WDl|S;2x}#jOZO*?bzAX=4v)oGBuX8OqAC#@;b@OtBia?`~|7j zZ|`o$M;^Tmzuxg88xQ(j!T#U?-Mn)^odam^BLWFdjg4o)IS>4ICLCq)+;fA<#IGqS z!#k!d#JeUhM4dgFjfXTkgq3ygvrpm+Kl{D{DXK_^>FicZfV9o3Zlde$zeb7@+wlEyui* zRyfeDkEn~>8S+E&O2k3I{B|l}46Kin;QX(qF2;r9W_W$g2R4E9FKk{7kCSM`c~dLu z@X6UL@V1Hb@jn?KM~*bY!s(*uI0Vjbl9KapK76<2CilnbY|;Pip*wt_Uf+a| zo_2w9GM518mSGdz2UYa|IH?O43{VfHrBRVKa{|)_IzEL!`pr+Q@*{I*g>^UvBV{+w zQ$aP`jJ-Yu|IYd!o6gXyUS94~b3p9?a}3CJUEqMSN9=Nd9rm-W2U|ms0-9q`9wpby zh{ZB_SugZFuB~3))T3Bm&~?Rv#rW3`{w*et^(E#c`6N5@0du5Slu{oa=)Ll2EcN|2%vqytI88VU)TQOxk z%8kUiL~aT--GeBXpf{KEf%%Xe!#x$H$d~AEK=$1O@-H(mjG6V4aAzmf=(1H~!{ zV5<#GZ>g?T*8@_5p4)p+skPhD?`D7GFB^1JTFKZ(b&h%Oghs_&6&7sgv@^SG?*J-# zhcL9KRsJ1teNnSwvAT9n)b^YalYmnJ<+9F|y~P1KNxvKB&Q#!hKt#^`OR1_;7BZhI z$%rVI`&5f0oDU3ZyVsm?I!DguEI{w+=tORC2&EEKSJl_5zES;ms2!cpyoh{XzxRC= zKyRv3$Fs!%UALRgTc&!HmuKt6g#jk0{icvuJlKewx7QEr2KnIIhzX6@UjWh@tHGUkBPMn;2Hw9&YTfp_o|F3 z@5!zM_~!04N@DKKA&aH_pL#61XL_NCSUh)RS_E%@gj@ftBwgo_A^JjS{|WT|Lo=7* zpXR(Ws_aDP^bQRuNjR1F@QzKmed8MZ`1#e!%uQ8PGK3(RXle|pGs6emJeVr?x>Kjl z#wSj_K>2D>bAyJ|VMmX4E~uEsLfe|V^_c;54chHS0k&7djXMXZ zDL98Oae5cjzzXsKi|6GZ*M)~km9_Z5^kZ@PxY>LMsM;xEv?M(xp|kBYvyD*hf6b~=coU%!ze%>Aru%GBz5+` zfBpg7dSHuw8PkkvOp`kMe@Ll09s89Eeq`v7l8P<;9a8E(#0bi`oaeg^;+tEa#=$|Y z8vEuMi}A*33ot`eqDm4?1p{7{bUBLL@6Y__4Y=(|N#HFrz>QBfR*$lYW7T=-86qf+ z^P7S?n_S)>>VSCcdA1Sit&eO1bCRITHPj#?Z|ub+;~bFdT{rp6dNHPF53fbzz7CxI zt}8XrF2rq9YF|c$>&0zb&@Kg2v#J{xxP9aIM^kr^toX5i*uYfiV09M#LBvEGKp{0kLy5A#Dm0 zQ2*k+zrl~6ebjvmYcg=r{Nr%l`ESD1#^!V&?q}}m5t-jMJ{Sl~Bx5@lC%}DZRjffe zvfnIU;+CvtWHwkvRN4BgN^oLIzx%(d?o>dXhYRK`#%Eu9Ii}P##qD-rDR3y}f*I@H zFP$!*(?C7h#3-#19udt00rjunc{6@RKs{U^ygo_oJdfDq?*yCY9zZN4`LtC)3!_k$ErEgoHV_nZwT2o3FHSPP?r*Q zIvcvz2-XT^rRX15?~UP{h;#x*Erumb3L4N|FW^2T1#Ka9a$`!Tq?)qUH#la{JHI>*<; zO1@#4H*`+Alw8;J3}LXXQv&L>2t(DxaTv2`4rq7nLxG)t9^0amrT|GB&u^HbKFnj? zt;(*S0s;SW@*PlDjsEUll*xW2`+wDcvh0p0)?ioybfm0f^~WzjX7V`nOQ5RTYfmaE zxIQ3T{ZAZsrjo>Zz?ey^k;(XA*xo~fW`o6HL5Kpk3fbEI3h^3b(g?GEa4EPg*RB_~ zOh0Y>JB&|u=3iTVZw$TA2{nMBedxm<|v1zwRI?HZ* zd|gfiMude-v2fmr;DH6^dby3$#{Wdx&V1#C6j_i?-_JYO-2CHcC-cq%bZ^YE8*h+_*#4ZS=K~Br4e7tvX(mk+NY*2~58~9z{f;E+ zrK29-|7c;<20bOj=b1xIHagS%s%P#{$9?fYLz!%t|KH+s@UIKb^th{fe(J$a>g;r; zIUSoe808U?VsrfgzJgv?sw3#nxXu-mUaVkxdD3kHxt%|!1Ipg@{=#p#u3+=5M z6ssu0zf!w;`Y|je7R1pfsk*QejR*?Kp%uFixOP$9^Oac)j;%+YIJqicv-ntNa2$ks z3mB7@y9JRio&q&B)i~#*QxwsnXfW0*_K2zejt*>H_aX-BDsix{TWxBULP~@3+96WG zcr(r$yGycPx@FHx*w@>I$#PN5N~T6mZgRPNL8;5vNa@gpEi*7FTZ@;v4{74UsZv5Q zNPaHUmr2&@v2|m$U1wx%@)_|SMXpT|>o4J&Cq{7e#lC$Juu6f>P`_xwaZ0V7xa#Zn z?{XW#ro*kc;qm*hQ%aDROqh~xgU$Nd0Y@+}6n+cx~m$VHdNNWSxr<25-j=dsBaujE??3?dA`JhsVUo>vAI=;;kg2^0`trI~HSo`!N zO3>|ih$0`vnzktW^d-wMsfmtd+o{y3T^mhV{yY)sHH{)i10?!H_>2LrvTDJvVEKxG zxnuM2>ch$}2F^Vq*Z^={uKtT9>XDz;m8I*KSQuRt#!(P{T24slr76S46r`lQ1)sk9 zJ^06Wz9%6y@0Gxu9Ka3cVs-W?B%mVSK9ztuIqM;HWf81@>0>AvNLv2x|N4JewefjV zw!oyuG5F?HAHX%|zFr;OXUnuU;Oa;+BZ;+t^lWfXtLh)JPtQrI^>9_WlAe=Sn; z4K~%FUrMD(3(A5Q>EMuEk_aUMhh{UgnjPU|ZYC=TR|jqAGu&D$9;Yi>2rpnYMhmpq#s< zd2uhRtwB}G7!W&cxS;zgKF3@G^)gYSG#`$ssK=tJMl7ptR^M&61cr2mdWm6n5IE1U zWAT)Rf&At}?OhVA4=28HcEVU>=nVE;&e`bO^UrPhWNW>A+%(imNqPu+?Vb@kbklRy zIfzs15aM_C2sW5|5rSRo1zp!LR8iMn+I9Sl$&Z(>o^$(nb3Em`GN3o{+P~5k|?`Jpt9TxJ} zK*9Di!P10Vh)A88aF~mL`F|aI!7C;;x1oP{NUl2ric$KHx7~zqulk*4UR6tp`O1Yr zbAui;^o+oH&VY0JqxoA~r2%_qul&CoH?MgDCr+P@NexYE>{YR`^_LQsj+uaK7oDQ+ zxA(N8ogK~Q(J%I+Cw|*O%7T|JcxLa@^0J%(N8gE*MV{~F+HcamhjuC_$H&&U#E#@B za3IVFDQn+~ISX*%ij$Rh_YJ$Yd*yXY(0=c8PpaeDR?J(d?Aa6TI=OjvbfR%sd`HUH*Ek zXqey)cD_K8I{U`H4sYjecx^~&IH!A?#2zP z@0Vb-O*xVyujc6m*)fw+fyj*4D7|hm=-`0eCQOM8{q(`)$X{<+pV8A=38LS3=^OE{ z@BdpIH)kGrAUpH6o*wk)h7#+e;DjH0NA~3e%(H0(u!$@9Z+5l0tlqW}pZLZ1@cl>b z#b90o^SX*^y!*`a6*!+LC1N7@w$4c#c{>jPx&^Pfj&9``lpLQyO0c{3AJAG!Qd-_O zr5Qu@)jCbyq?N?Kka=aL#GSx)PhT%NR($IvcBc8>eY%j7Z>7Mqb?e`Y}Y#-V}(H41>wz6%ZcEmclY@1PA{F&~rxt z(4$Bl8{{0s6=FsL`3tZBeQdebw3?EwQDFUGt{+|bm|1gtO*1YZGXqVf73y71GOFpk zbJaE}N#~=0j#{ld;HEnJ`ENO z6YwsHy}W|~W6uM-dloPT3=j|Nu)n7h|MJKk_~tVYDw}f`%)bBLWvAo1*Icfp<{~aH z!^?y4Bbhw-7c_hRRO?_7+A>}is_`t4)S!{4Yg&C6232&u8J?`ucf@POjzb0Dez zB6V!er(b*!FBr)@2MQDx8h}p3Go9^SUsA3JCA%+a8pgSS!ZDMk;I#46(bd(3&4>2K z;9A8G2v51N=cP0tcSr)@QnQinIjB77lAU5?&n>59{M<>k*=E=-$$Ca02_h``lZ#rp zOh*!nmDMd$lA$-pNZ2;O03dzs+SiKbe|9S{p;`Woad{2PZ$!)v^4=bT0S+Mu%s9}@i`l%!+VNVVNb)Fn8#NRsZqML?Y->PBkJ)t%WFa&`1J zk0OCHJ$&`>BIy9VKtjLlkg(re1TVlggG3pAO#L#czV9SYK^kICyl2H(m{LDRf^&0z z`oIA0-?CnxpPtp{r~Cb!o>$_bsi{7etwh~NzBtCjNCqcS7a{@FNuo}Y^!2-U_^E|| zb%bbGPd5H+$k?)?xW8;%?&!}Q`xdMZo`gMxhH)C56EAY_g-=AoFWVKt zoJN^m)c=YjjaVJtdEx7E!$&`j(~n)IJgVD34w*V6810thf>hi6GITM{p3$_eR7whJ zs;f~|QQ?ljN~<#m31Drc>z?+*_}~w3!1edss_tzkU481=_=W`Lr_WjB^&9U?4$w)0 zE)3Sxn}{(%AZhwg4nqgqq*Q%Kt+jb6z3!bgUP`h{8IacCTuFB&(9QM_DWbl+ryKrk zkLyljW>EeZERpLs0W1mrO6qFVTA1tTiao`fKPtOy+!)2Y86t1-%of|KeV58>nPe8^ zx_U9#dRUht<5W%=Cz-~QIh?h6`2}}6iIPB*qqSUg6ac-TK#)5_-AdA_Y`G_v$}(EY zDlxUZ+O?I=uU=TygnGG8RLD7gNz-)9lb~Jh?iefM5}+rCdStg=-rT5;RU@_cd}p^x zuO#R-RTv%`c7WV^j#uTDm1FL>i8#J_qNW+RCw1@WG%#RnfO$glY`wnwXQ{{1Z<`0p zi-B#IAK)%2h9F29I?J^;xv5cqN{M$^m)ow)jxI(UY;34 zH&~5;r@9Z|)0-c|5BF`*(s(zO;xu+Zy1k3fF=6Iw%DND*QAxTgUS8uhkC-MOpS1%2 zb=(D5**Fn@sL<7~kFq)-1rI-Z@Gd#WU4|Z;YeZ2{PB}< z&Z6VBntL|;7p;QaP+o~kr!BzMvyWFm{kgV%{#xv9T5hv~2fj1x9S@9FEjnnzADJK@ zuP!Vud%mH!4L9z2QN5d2-KdVB4C$goy-ZM=coL}3pE@0Dwroasf3G(&$OHV1#wXRJ zv15IpUJP^xLKN>lEk3lCF|7?_PnKN`y` z@Tu7+DtnSJSVEMXn(dNO^Y-BZXIrgLg^fLr({-N&exG{bK^&m7#ob0>Noc4PZ=SgX zZ=AMJd6lNW5}DELY^?&fRt?`ej*IK;=mopX?itwB4@3Z&z2JNw@+P+@ls-Ck0!BR zaOv00Qeyt~9nWERe|OwA{b7|&`rpkw0dJW!&vaMD5jPYuV~7z8m@>@ijSm0o)hqrIU7azC{5AWC*FQldppE&(Oymj%3%0|rt>ZTkAe4}XOjltH( z{hJ0qilkwa$Hh!*zgLD|reO8Oh^6_p>X4xKJCb2RHtJlu{gx#w@e$cKyEG}P91gldNG5RE8&%vG>FZEs*^RXD|9?gI;hoWFSv2-gJ zF2)Bhe`|`Jxm;7^nCK;wmS7l%<_b1nn#1Ioog`9rZYG=@jxcxVDKTHjGyC@+xyO6A za>g87_vUwDQp1=;nsBaM|M)$)f9rb5OzlL0w48Xnl(GfcO}8{DX>afJ7ER3OH8>_4 zc45v(FOh_PZCkGzzxxlgs`Zzo*VYa_Ltddvj5Jn0Bin8AgJMbl!r%~kpL@yce+py| zUwzy>rB2^Hrdmtwt(09kCzarQsDP^B0tWURK+p3V-B=%U#&TuD-c^^?dm`5sAj!PU zt7a&FWE*tZzCO8s#}4KqCq=K2-?KuNaZ>F#oGkxD(C^_2=Xb8aLAkHr(zeant@rQT zi=GYJP&01^O2#*%udiQ&a|6=LWO?&iCgRL-(=nsE9!{#w)&WoBDtsmoGm_jM!}c5g z8-g*n;`a`)+p>X#%Deh@>F;{@S9oCmc7O6J(#-lmu|dRftQ4GYNPO@)pw9-A2i)0vGyX$uzf`a;{QSq2 zR^Y0}SvbFTLR>|a7h4~^r2d+4=hKhlyZ7FK{cVTi#+zzPXc~jRIrm~*wEQGSbBF+g z3vbN{ubPJ8P2&9|T=Qw499vV5a%&b8`RkgOUZ?cxAcRP+x1-_J3(!MgzN4=b?^$yf z9y+|s%t21l0`Hi-5dXID3^d3I6>k8Bz%+wy-(as&g&WQnoU=3QrJVyt`u(Bj?#AXm zrY08!b6@_rX2FR#LvHLApcCh{OitW6m32yzu7P!meV zduXqemX{(;?Og+Gd$pk4K4UiJnM_Jtk)rCJ!Q|>XENK{r2^BTU17x9)2F&9#KFK=2 zM}qT5{_{uZBXCX|5vhL7o0j?w>3sOf5Xe5)$)5g3v3MoP>{`b^$9sO5aG1VhM;$^vE7_#mQQ8 zn;?jU-*h{lLf{(DG}bS{Zx46|^(q#2bson5edrGSbj@n7q;#Mjl$mY4N!89L2f8!x#n;ikFYznf#m7G7evS--nflcK>yRJ_*Xn*1W)uVY4O} z_TCFZJAkXqRvdX=3gORV^H@26d*nXl1TJ;}Cm9Gc=Hi-jF2;(Pb1+iii~tMJ$t0=J z+vn{UX+cOyT&a{`=c}{mnc5N)_zI+30$)m(CBa|Up@9N1nmW6>&_}?*0CN48LA7L3 z)JW-KiR}w|Py3q$=-p4ffRIU&k>%GeU#tLnS3{*aVyqB*a#vFD0xG2>d|>x}bghX& z{+P2?qNGX!^qO+xsadD9Ht!@suN{t**Y$m?o=miz&OvWEVX@jLdj|V8qse)K*JCA1 z&i$$?B{&~1L3)d~o(3~4?$dx^rSI}|_dz_|sa3Kg*_TS1UYbEaofj|5Cy(tbCrrax zk}*tDKc0u@Fik)80XHMSVeY{+v^YrD`HF;Bc3~*`kP)D+#m-bW_jI25f3AH5&se`F zk?RO$KmXq2PM3SjO20ftVDtTt=S#M@BrX5J{tZe)4M0(%+^!$?eZf3b`@4v%l(D#6 z`0>H#u4zQX^Oo<=7&G}5cK~;!K{hkizGDtc zCrLnb{poMO{FVu-ZOt`xXf`J=yxPL|Hb0Bcthrwu(dJO23L>V993w~glieGHJidJ- z;&=-Dhv|j47kKI1bHw%X2cr6ad}$@FYM6=h>LxPmVdqt3<~-cox5+c6UEjU;cKmGB zeQMp)`(j*&35`woo7Y~9*DXIeZXbX&rl8H23z%FSmM)H$VRZ#+D=N`cS%Wfj>|eZ| zA#MZAnkKY=6Q1X2@WZPPpzFQ5;E3>!+<-dI{PPKailN*v?B)e(WzKPouDPDk53^xYq$?@(L3Ih}s21nARIO!`b9eUfDWM7{)FF+&*1 zrM^AL+j}*>eC*khaZu}Q*;O+Pq47k=etc~GgG#;KTinh4M`ta^d#5fzU0K!Z z%(*l{7axA$U|a^%@JK-2#WTNeTJt!rlR$l!Iy)6^b08`D$4@;US4xR`qMVTcy5Ns} zVCn%l;kO}NX#G3@4%FOJwz0V^qsX1_9R)xr3{lV*e)*BIM}ODPzpamDGX+c;KLH;- z;cUEl>4{!0UJIkJlp^1b#xdFD8c)?xY8+P~_lz7br8!#p{sX^L+An)00cUZ!KstEl z{UgA>uj}sELpuKi4m{iXM&YQkEi*?A8IZ5j@Pk)>5p&{j&J@07>=)4L+TkIDj`|_%atTAKY)n)Y!iLhWiK0$Wo zKmVRqWjAG#6UR?o{AOHs{K+U5!qH{t($_6OKh%M!r_aT8eTZOo$}v?ob-eQ0-aT`i zBIu)>v4=JUeq;c9g8}F^3`$1E;f{8-W|LY!^{=&OC42UPmU^W+U%P3aa!}X#_!_VZ zsVbF7>2&=`OO=DU?)plnQqR4alX`oVtWN)~edv1TMVH0cb5GF#y}B&+rC^-PQRbvU zR-4CB0rXySeCLi?(^uuiUAC)Seq$>^4}Sw0jphOqVW=LE0=;h}UKj?mSr?(m5B@F=r5Z;=4vF24D_zqbpY zd*qH7nA=W}<<`o*?yZYf;L3$7P?xO)2mfBaR)TNqY3lZa{V(C0yVp8k?&`pqkf#3y z*qDNFfZT#}TQ5bP@DFE~I04VZ$Bs=~CINGjn3E&8|BY~GWM#M&g~_+~fOGYFhBYra20*^Yy@&I3ot>3@q>OXv25! zz8!Zw{TLF=Z5i+QS#$7DSG)@o$27CN48UgV0M8RCzwHlB>xB%uMhVgzDy!B0AzXKf z5!y5=K~D>>DuAAge6>!JmXi~>9sRzYIdurBJ6$vVczk;9N&X9mX(DA0y>F;b*)Lnr zp9fcEH|Lg9xk3|$zuNnf1nAEesi9A%T}uFc>@-DWYH&{1oVn&PQ3A~gaw1S&a<+Uc zou_MjIzJ&sj8M-vl0iql@vd+5_2v&r~b$@wS2AGzJJF~Jo1y9G2Go7 zZ&E}^p1xEAblvIt$^e^^ib#TbisXWGfL>tII=iV`fNr0YB>k^uEXBl%8V_hwAj0Kq zTc5$#wm%IEc1__L;K}knCV~1}CeCBFfbbph9AyY4bR&WDv=PGr?oDeS#}`-Kft?Dd z$Mx1g{Q_Jmi7YRzO^>*F49mGkas23r^%35U1vnx`nL;3qDd3254%Slux)`ArKBhKl zOg?0bP7?K9y^=VZs5hx8`7(UuxHBbCUm1g7F;b?<5@XD$fx0@DE#(`gX$@G3S|-rA zptDcEe#g!D$1G>ln7E$CKucXS~wo-V&&_#=)1c6PJnV7Rb?!PO8K(6T997Bn z$({{N$HP>WWtFOS+;zG>rKS2j^HQr_6pT0E$a9i_`Pc8g6|1*ybnPHuPIl&3pK`X6 zEF39>bD92i0R3RQa_$*NOb=wLM8}-bIpp0l#>LWeD}7^<^!iTiOf)a0^O^b@m6k$5 zIW%Lyd|U&D$SxIFUji;TU0lJighxX?L(o5xSjf7U@ zXszwxIFDv2K`*VZMNg$<>{5T{H86*jpv%A7g02^V^sX&CJzI1K&}CbD$?+X?Fh`Ez zNODf+no~HxaS9ezHkwWo{uqv_NO;m=VF_ABP|EANo6CEe+EC@@U&KE zx1-N=!?N*H@z1AUjEUr#o%N6B04_qTH6~(a2AyaArR`7QrM`9#JbNKROvJ0TKwk@X z{}2wZduwwh+_prVSGb2!Ag6Bx%&(rZ5dYhuwW|aBHXa|ZG&h31g&|i<47jxC)l9@Yn`U8rNrm$n0mfu<^BTN(!GQX0xc$k; zkVY(BBqizZIZw{#F=M?t$3R@f-C&M(wDZ~S;c- zMnf{ZDge5t&Mq%I`bnMrcM_oQ_Q$T-Jje+gNzJE85EiO}TA)qN-`aD77&a0hB^5?& zW1fdmYmYQ&Cn>YCy1d;;WzMn3{QkOW=`|!pETA0(|RKPfMwJyinENGAj5x+gDVt zfq?<6TlFY5Jn>YZ=9SmyfRc*)oTPrPvE%*h#wu8cB5pI)om!j;+6M>hV~`y)VIn7?=Y8XK4jZFqpRf}D@Q%Mh zsg%@SbH|U-D?!oA4OL|X+{+Yzua@fuIaQ>q`ja4@jw-4tFGq_3{q&rkwNx?nQB(ij zZHICFeYfFPPd#Fu>w3On*-7}+>)tGZd3*%oNMo~DZ~*;48wy>$-aDtU!snHgoR?3Z zr~&%4mKc}^YVIUOZ|Kp|%%SajmE@g{N6Xae`Vuf79N&bYhHBr2Twd#?1f9acJ6G}A zsQ)t(pp&Y)0_as`>^$(uNz+SYeQQLi1L(G2noeD&9I*A2XOI(mY)953x*6+r@Um|@Rvu+ZOsjPQnU)7$iF$mUgF6D->(r+Z< zywux<>+kvv4oV3rjhTOY{o5sYs&_t1Vld6h030|61L1m$3jnz-%MknAwwu>4O>*w) zYp(Y1r|X}_ch^1|pJz^eV0_3BFn{lHr(xNIX-KHDdyIl)Ioi(rQ3cTL+8>hO!khAu zBlhh+9eLmd-sG`|#kf38M&A0QE2@Mo*UmUz{w@2X*qI+~7?SkOUb#OXlHNu{5fnWNCn=E+)B~xxsSAG| z19T_=&f8+u=Zo35=M9iAm^^Y8uotWMO?p{uQ3s|P!Po{P@_ zB^CF%(~wvQ`G+$L!-R@z<&8Ho?E%tN-0e42-u2C5GES!bw?$`RZf%oU0Ap2k`tKL} zH{mPWp2DtyZnl`hExI3@wE~|wW~Kg_0<=;?0L~L2HYC2og z6I!Bf7g-s9YF!h4cqWgCKxDbFw;zyn9NsR%s`X=EhN+0_sNHy}nnUTrs$1mt&-mdOYRPwO|S& zu>Ih>%joh_)E@Kk2@>F^t2(8vr~Z@36w(#x&`RHWkMbCrq4l- zoGm%opw~%;bX`fgIt}ERzJur_?KJ}AJNBaQz#)u3>3H=mdb_$&p3Nd#seK8Mmeuhx z&9a)Y$XbAHyU`JvaKO_8x$e1e?=y%AZVuSV+&{)x631~ zpX48D6W*|UEpFVuf%PdHwC0+Lw`Gr!?r1XM$LDpV@xa}G*`*>}_v!Z^(*fN7Gz`k| z+tJs9w*Fr9N`XZ_DkPdszf0v9AusKunM3yGFV}N+gvc>N_&NhtmK!^NL1B9@0rdwo zP^VxAr9g6ka=MNOGY8$?WJ8)gdYH~Jz1%}TU6?i?62_NR;$4lia6$bf_mF=l!59WE zE2fcm$5W5tKkvHL2kNGf360Ho&jpuApnj^iuDG1s9B23Tc?9Ph18fboYl8i4?@5wA zrm9A1+PZZRY(2wh2*Y0_KvzI4DtL;Z$pB@H3L2EBWQrmY|u&7-PpRu zQgu?P)%Uc3kCh<3xk@`}NP2kpZPTqdPypQb;#_-@q`!B@v6x(0XFrbvOx?L5 z{O7Lc@ns3r5dd@rdS#k#EItRPHceuL4Fg8}YHMZc?$nJLa89PZ`cemFyz{zKnWj%d`v{XG*|Vo_c|% z^f8sHf5~y0P{8#_;jEeSamC`54#XdMhQP%W?B4dkPuH!Me@`HPr~|n}?TG|-|I&}- zH!3Sb`7u*8K%d;?d2#2GbOP=51Nzvuq0PG-gQaZpIAp3S&^vQHa#dLd%$WpzyFM0l z|1I|<+Cg<0cj-CmzC%jFokw%yEROOZ2d6bs!dwiX_mt=}*o6V;YHS&XctfcMS|6Rb zL>)c)Sa&O)@6-K4j@mM1rEdMV^JigIqp~-rV@h4u1^c@XIO8in$_yr55oL)5`(nRjZvD&S zzr{1HyMhlVP%9;|Z(Xn)SCVZrNoBqI8gOo;>$-@H*AL_X?wfna&U`pYY`bylpIKvV z%nN(~u{1HU7Uz*k*?Txhq3?QT^OQfj1GrZz49fl+w8y>z%D+yDWH8Y&b}u(*4a&Tu z2Ph=>?j$9l_8^h@i#k*Nf!6JK*Q2*6z^(!Z%@L9ynqBn0@b4|iPL0P_bT>z#_7b;e z3m_Hr1Br1@K>_t93DoN*1pqxRmor!5HA4pi>7@3)uiXH3w;$3;#?5%|g_mle&gQh8 z>sDjO<(=^z9D8dVtASlRTt7e`iK~Emro@n=B1ktHZ1{@<=y|g_>ky$MH;9kE@F0G$ z`vo(IjQr0K$s{fRzYEXAlDe^RCag!4^D+s{I`hM^8haEuX|V>taL+BswcgtErkpQs zdK|w!unjLKeEzs|u%ND)If(OZ(76QNdU-D>5TBPIy&5ew^{B0^WNPjaQ+?M*n&q4n zyx`l5bMTH>V$P-M>5_G=O3x+i1i%Ld2e9qMm#};DW(mx9VK}GjPw8@|EzTJV_h5R_ zDb8NleVeqIY!pjNEWlM$7rO<71NA+U z*>d&T-{OVt!%iKL*Xhd13-C!PO`kdfaZpSOoEjHLq+0-_fK$K&e!IHb@x@1_ME%7l z<4r;SU3knQeB-rOD55OAYjsJ&+=m-{F|eM%)FP;x%ikvtJZW$u&*J@66zoUb-Wma~ zL*M*#>sowy%|kIzm*Z~=viQu&7vlUmOA;knOL$-U%VL;K&Gws`50dE0GcUYRSjXU5 zF`%yP(|?6uJ@!xnc&bgb5V<-DgW~LW<7MT>)FChbBI6KpLCYk`lvsf!_-CYNz!+uYMC_w@f;w(2|s&&wUnT@WB6dZtAE&+ zu0Q3Yibb>3@6M^s@v%%P67<@kf-)}{B!E7w0YUlHiOA3qZ8OFpM`tsqNWklNY6o~0 zpoi<&cni?wrx9HGZmv{jZDkd5ojo{o|Ko01XtWaajA48$Mhcy04#mKsu|&QFC=ftb zT_OKAOQ!n+-FuWYorfveYMfL%9#hJzJ?2MD6Ei~80!|Yn!|ka9P)^Shp4eHO)jKxh zOZVKOqTc-OkLh`rfdkj->g>fiNY}q1fKck50Od$$7TEciI7UtW{cHoNvpdL&m4C+Ot za?#J8l=J7{zPIf;eCqj!JWzMznt4hAW>LVe;r+7s6+0Mmtdk2CAHCy?0*+ZiI6NbF zR`mq@eakVJTvC~$Yi`_|1qVG|gv-l;!)^HHJ-6eIryhfw)0unaDQDtcued^~y>oi! z#vLoap#uICBlpRtr^Y2;NH8>mzdv{WT78L;ADKX&s+gWp}{{5DtjNOw%aSLq}CgWBg>_iJ^8cj zjJuHqk5tTn{`B$-u(+-zLxRo$`Y2O@Hz3_Tk2*sQEwy#1k^rornmfFddyGlX`H^T= zW58R7XO`KSgkAxAc^w)Y#G$?Wv3u)w?Af{%1N{SPHFuOPHjL)|e)>>)9szVAJTY;| zFNUflMK4lT+O3heykT@LjN&Tpd1C5?S5LM8-7dIHsi?vA$6bhHYMLFRRAemJ6XWue zeVg!K+gD>Z85tCeP|M$p@*3QE>T8r#-M6&|%{C7)VG3*>{FVSd;dNuK0n=~%?CNUA z)eqi+yIx%H@_W_dlO#yLGK|EAiHprn0df}RS}jkNhjqZOd=4H=t>EX_Cm&d z#_E7G;-Lf~o%-&c4hhuPNud6a|3>@Mryhe3pL!vtGqrcmW@a#$_FcwIYxk#*WvFB) zynmR5Mq&CI8M%u43IX+x{NU?&YV(E==*B=eK_J0aV|-c*MqBEY5u!>dS$xlF=i$`p zbJTt@lGqf>3A99B+d4b&f&cdzZ55lRfpnY#VV_n~d!AuOGL48u`&Zx>6(nOv%)g~x(4R@ znSgmhT2!PJAr}JZY;k%wCh1nLv;@^l=AwMEK8tvEeKS6K&Shx%9gaS-_4yMiLBCl_ z&}k1k{l-Bvt{p0l)5o}VNeMcYPjCskDU*=*bYz(V=$m$WfIfSw1n3p$n<4v0MQP?9 z^18kHq=+mDIyno_yK;G7ujX8EYw(Q>{f+exqV?C0x^VTUE|YcF0=dkh=3aY-xi$7i z5_Mwg$Pqw~Y0~3Tnobh+GTCp>tDA~rtr0kKQ9rfsoJ1JD4`my1WB2xqZwAnL|69L* zH~!_$pQwaxdFMyqq~k6Obeh6T&E0d(vAysM2O-HK-p z?eRU!RKkgqX5wAT&%{ZSX1YayOi)hu?u>Q)+dI^Yf7!YctGW)PXp_jAwnRA329A?r z^&Ix|98x4UMP11V+rz4)HPYOgV5A@isEx1wAkUL&ni!O5pJBW6c zl|pF_(PHI*x8-rxjCr`~*i-SQ<4%R0#>z`emEhyg$tj?|)&g~r?qFsdc!@W^2jksE zI$Ura=NniY3HOskj9uEjymlJi-ZU3eN-Bf+xn$kZ7W=~7ALwkyH}1X_cdU9W#aB;h zY{Iqg`XClen_=jHi7!&iR~NgGnS|1GTQCvVr4Ve5LMV0j=89@1RsV}iyP|8P!=JDV z48XBYzq7wvfpfa5Pw)fd19MKm%_m>3S6Q<_g^aT|KO$G;hn)o02G?wLT3%>Do?_b* zAn#cC1W$JC$Ng=)O#JcSMmPZ3w+klEL5l>N3QQ_>zr0c3)7QLv)~V<7N$UNGlqz)g z_o*{C_Z@DRQn4<{laDBnTv1-8{RzKEpUd9ja@uc6V#{mKcGq7=WV{`%t=P5sMQmLC zEY_`j3OhD#LT6hWMn=qD+`Q2`FlzM)`^5~Ix-OJNqLwl0Zq?CLl4~3Nj#MMba9)lR zQ`Ua2Jc6B1&AxGq@7-e~xN+Y`MMzF_};_8evE!K#vrtcedHgB3q#Nzg4A6p?@a%;V>iRk#?EJ?oL=V8z6lxN_cc=o%iv z`ojm6_0{#gZTQyeN6_8djd_g|)np>#nTlt?y>c?^pbQRd^M!_JA;vC7SfEaTK48eD ztF5fUYgU|%sm(3exOXRd2Ks}x($aI=&-{d+G}dBd(G2xWb}Ozq|5aGnJPBKN?!ec7 zeiQB)ZAZ_98XQ+zr-0Pzjq7o+z0F0Os%M@iE0-Qitlot;dgLVfYQaU9|J21x@x!bC z1Fu+dmgF5veO<_JJ389%?+@Rhqp7cks3>!3#*V%f5zQDK%a*2_^z89n!YsV{Up}w^ zo)}t`nD^>vIHMw;SC+>0Go1}iXL}RyEUBweG~kX+df(&hh8C2L!{EkX!_uzx5>&01 zQZLdD7$Yn0QC?L(VDe=0Su$B`91T^M`}4=01U&E5&dc(;e<}lXM4#pNV%ax@Txb}n ztwdgFV{m|-=rg_tMpT^*?%Joq#cfc%XpXuL=FQ@OO`qy|OiRM)JaTHA{;j)QxUz{& zs@_JF5!AeE7vb7T0tI80e?{#i)1`dsGs>!QLbcv!9y6_0Yebl5wJ+NMPPOKm6T$i5 z!Oy(c_8}}-&IR8&t+`{}YHV)ZuT{-xTN-3PmUL!U>p|SP;aS}H z>=U?n!BS;Ko>cBh*z@P4^1i%b60VrONF9gxQ0va{BL~-(zZB6o`#tb45CHltfAq`8 zKtJ-EW=~RX%TPCd+PPB&m|R+c8rs4`QL3nOehdKt{-M{^a-s9wr6=IT`HQf6>n8Nl z0=!ALr@tRRd-Q%O7i+=7X*12Z_p}qPIt+gS;rzIiSh=zm4Q*bizxGOAt6MH8m6enz z$@*WMcYm?~U9H3#Aj&0#h9|^7z3`y2(rz7fjpJ%3;D^V*9Pga5Oc{+4jKn95k?hTn zs=ek$rlVZmAvp_ZJt%iS;qzwWr0Z=5Uc?3q;CzO%@7eRx`iYn=C(rzthQ=Pu8t~(u z^A4P=ddd6unitj2!;;AM4h*2Ry94{$4&lY!yOhdHYv*BgEKOH$k2(u?NHUruMjdxV zN>+wPhRt;Z{eAuD>FGiHp;olX-`-ujv17{?Y4UrQ_N6OO&Wz8bd8sf(k#T1qEkl}Q zO;{{MaAgY#Hs3D)QR+Ol@#eE6A>6Zh zt+G?GWWrPc*$Z`iK2HjyMT~Tjtmni4=R%qiyuIv}8OxwscZ{4w-+KO~>VB^r&)sq? z`w7`h*epZ&(~ngKz?voF^|4bf!lde2-2UKw3Y@ofbf9B$9bRZG#j?r{_T-F&@btb zsFsT(HB}+`6mc&U3Q!c0zfxJpa^WyABBv)0bI!H}+a8z;O+?xI#QC?tJhAji0b>u` z$?s^Fq@(*aAS!LFQvmz1om-S;ZvyI>?aa#Tm>PLcM#om|*@k3DE~<8PGn(%gH50a& zK+mO9v;=>sstoS@aq@4@Ay;J~fNuK5@PSsP2|()PMG_#BCH9fZGUSVMfqJ?yAib=A zSjE>T!L?lg;q74ox&j35{Bsr$0rc{66?S0TZr4}Jr1V??^t?8PY8Ouu)&}KedST`a zI>TI~sS?>?DUrXpFTyg148Z0rFqT)l*PI(^41ll$;kmT>xVb04hx)qlqg4;1PcogS z&0c^Nvlp7~pJR&vI=x)Z19A1t7+9BwT+T_fa}2~g3+^_*#vcdRWJK|O_uhp4{oTR* zN&C$I-}$do>dej{s(fF>78<-{9%}!yeZJwqCVX)7LwLI9klWp*qD}00huhX-!Fb+W zlY*U|x1heNg%g0`BN5VlfnYvpkn=DB^q0D$Q%Z)^vA+pk{?*Umv8^xYt%C%{Pg^3d zwVKY2^3!t0%;1qVE`>JemGfQyfaC{zao2`t@P^}8sKrGB{r1?(KNm;f{M?DN@U}V2 z7(-=U${1`=8jbHc0ouHLEeDX@Zob&t%mhc+!Th3SXq0a0ahA zeN_I8Fc6H+*(>!|;HgV5kpA=Y&lR$2LsF zf||yFAapAI)&@ZrYSkgEF_df9$ZvM^PuAP%95=1uDVoF6VmX$SNP@g09Px@Y5r2U#R z!j+o4RfEr)g+@cS^4z|FWQvClwM!}Uex>q0fH4KM4@tmHAf3Q^rW1bInC^FWw5f1; zSr-J*l_VRQj*2AGJTjAF>C}n34#~)y0pl>>nyajrnXx>)uT@FH>0NPsHHxY#Fj`)s z49#(HZtu$mM~wu1ztS3rUFF>As&0qXc7QRx=*kvfDQsCqg{q@ICP6QE0Da79CMe)% zRotm;)IPFa$naZPuGo!_LgU}_z9InU3h)ZUyc&SZfoUwAj(@or=*ue|ulBineu=$Z zht+nne|p&!O8RK`nG0d1@1EzN`&^Wc1KRAHzQQdKhywY7$-4&UH_LI;Z3zHsx?#11Qck1m2fTMdkKK{&{N~fb(kY3Qaj_#p1 zy(;1iKl98b_3;7>ox<`Db56ud-I2B=Z8N;<`PKN=L-*)yg3~5wa87MB!j4_E+X?OS zs2Ss=ozyp|e%(^g(j*sfmrtLiz*8+dHuBHO5m3Kk+9D0qM~3iR#{rl@ixhS73{@11 zKpLFWCFkD>#w0i-3;#}t@tK(dj)aN?((mfoqa^7I%4*;Nx{coBAuDLI?$}v#artRy z;lZ`fptrx@1$ywg)i`P4F?vtB&7;jei<6&&Zt0cPmJ8~M0rX&D-P{i;&5u5{^_6(h z+@F!{gWtvdNda`O&The!K>Do6AMM$I-?Z*lO@K zNxR*T6sW-Gz4ST--Vw&()#sxH0(SL_1#k-m`N*G^p!=Wmk0exaw^EZ8OZCcbTt5Lr z9@Z;K8EZ?*JhowexuntqaTbjstO*O3unl!&9aa$+qw*&hK_y~xVarTB*1i|J2D=>< znlj|o1d9VvYN20~?I)s}WfWqJ$p5WOuEHM@AYRk7kg0vsM@U)6_UEBUZik?m< zV^YfcBK)F&Wdvie3B*hod!J+w_YD6>oXZkqOr#m!>>{`D_cr+@jgxT6wE61QGlzC7 zM!#DEu3L9(z}}8_ENPmAT4Pwph8ID_WtH0}pcvyKEm2qCk7Y^}D8EvBH)Edzby^80 zkp9{g=U~&GU231YYkT^gxr{ax;*5r=xMK2rbar*)+8=#4$X6o4IhyKld`$~ZsA|IV zTQ_TfE=0g^3&+5){37f#eE+*`3d-ZVE_*eueg8k=)MJhf$~QtsW(^KtU}Qx3y>Z=> zSWOxpWMRI(#z`*(8?+YE+Ik842UU=yqhy&J!}@Icp~(#xsjtFlT{T9VYcN(_jxl); z=_g+&CHNImx?5YR_F|JdeX#`ibVhh_b3IBXHAw(G0i_a5(ywe1{hCoWLB5~ZgtF#F zlt`(0u>_hWHCh{hr0I!dhKuAvWFgL9eVxskl3UP6kBHes?QDSL*Fj zx>r)77hXw?y__*`0iWw$hG5%j^myQ#OR1`70&%9|p876-q}lO{=T_p258Z`BJ=)&Q z`_H)q$4#9bsIqJDtb1!NwOGLN5PkFM7kL`aBchQRFjoLpOvFJ1Ts zoFNxq?P5XBmI(;!NBD(@eM7zY#I`5!-@DeJCqLrGw8a|2OqzBsb<*;q;BR+J+&ki2OiIq)iyQdvXjvr$y_ngNf!=2|X zJ5hslYjZ~WB}w`vC!K~Gxu6cF4ItbV?46G<5S)$&%o(k-`6BJB-eJA?Tk$)bUhwDQ z{v@nESReY|ryujA7iknGg41DKMPdGL) zNyXngnwB!8xryjmhF#Zr2z^qTuNe`RSqrwgcehdyf^7Bd{MdJJz{LV_o&f=toMWuT zczDBj%^*%uo-+c4aq>hYvnZGPp+|laq9@+qg4c#W>W(ITy+=MaR+WjOOg59?_>4TO z<$Kz8;+lnLuyDFgv?`uqBIJvgPn?5CJNCM~Jt6`7duJ?TgexO2S@pt!ybYVA;c}Ak z9IaXSvhlK;xXze9Pf5)m-Ln;4!vktR`WaK_1aazkcxqwFyg7odYX6F?StW8TUrEM0 zovg2ixSW~J(R|_a6L60~v~H0m7+2Dffw)hdb}8O*>A2fAPmZ^mStENP@w9-+*G;besql zlxR%ljQ^li!doEc)KIW~|LQZ(!_{y48wJw2BjDUSIH)A%HqN`ZuE*WmHt0OjzVo77 zMEi7|C5az@HXlKMp~Z~OZnbSkNrqRhQ&!w_L|aHgDC5nOHTcpJB;^{I=Hs{&LDd8_aX)==ih;wr#}cFa0YgP4|?B z(+EGPJTSM#^6e4O5ObRt%_Lz!y*1M2RNaeoipr=cG3gY2-Zh8^)Ojy;PnecKkzCNI zJ7KX>X+Qkv(+cDV&KcP=`^go+A5)CJ4PPXI`dGg{XVc~L7d@!K$$sL>V?Z@xcz+66)L7DnAh5> zpok_lVA`R6oWEq5lAiyz=dS-LyDO$HRKM?SUxQDre;7OZ^un7b(wv7FhqrO_Y8DS5 z5HQ;@|HLPQB;$LK4uWhv)7Oqyzj!}hQ9m8;Xr8YuzT54dBIZMF9_4RxV>5pIvH!qT zpZNs#A8u2=diwkDp|4$spM3n&5s$CKCU#-ZzD^9kU}tF&YHnJ7mV@#@d(ez~;sr?K zj7}RMYwzml$CsGR*KuUJ%!!#~5i$Vq1HgQ8 zlQ39}0B5vgM$e?0BoSz5{!-w}GLl)8#HM(p3X$KJ&h>?i+lmu576h(OBnB4>pb!Xl zz%eRI!m21horxn#!bxuwo1w}|(g?aPt!;**SP7Kcdlo?DpocS+R_noo9k}}B3nf`3 zKX{G+eo`O|`6J27qkFfi$2+&JR{%Yp6LeS+L-VkjOvb{wg)Q>1JY`e};>esDuWiGO zaJ)%|S?BtjMXQl6#Gmq|@)0 z5~%-R`}5c+foMyv1jkf0VDpY0*mYo^i_aPoB*|u;z?Nj*ij1a6l7!U9S=bj*e{c-6 z?-#T(|JI8x!)wku4^x{bM0HO4%!kxob57TGbsfeH>sDz$Rpm-+A?Pz|AuZ>IuwUd5 zTIHO#KN(m%+>LyV1cRG)V3efAv;fj)tSX%WWCO4MF|1@H|mRp)^Vg@SOXqgxrjTQYHKdThphn82}Fvgq;GWu*ezEf<|B zrQy2+m36@k(%G;o4`Y78qUpR-(YJA1YCk)LqcKofQQ?j}qhr%9kql?8InP{}{-gVu z0GmK$zX#iS*Mj_%wr(sfu6h8@85-=tbP>h_L}m_Z*M&LEL^_q zR8-Sq0=z`pEe!Cz>+0FZFBm}Pc~L$hDYuQ!#cRd_eIwQOZ>@VG*e83*STD0ATfH#UK4VhSv!wB9;>#th{Y*qgcQ3#soJ;Zda0_#J zGcUjWS2y5+zxsR3s%^we)zwN`d$79`l~Qv4{lEVhKG3@Xd;5Es8hiA!z@1Cg4bhuA zX)?b5=BqHNzESNzZz+H2S;v3d>}BeAjRfhRTKBMd8*vC`odndkr z$1m`nSH9MVPyC!4JI8qWiB#|}-hsa+rHcYssEmK?-@9OTzNXM;*x;q~)g6h# zH+~J`66wsuYE@Ya1lD_IVv)OK-m&U;)8=P!?K2NxcTXpF5B1_hkKTrBSN#Upp8qC=9K)gdz!xr}AN?!R9myawA2SLq|vhzb^|qps#8?ncdAZy{m^ z?UWQ9dRql8EYO@gWYn1R%j0rzt~9Z>SSNOq;7@+=)9Qx7)DNKQoz&98<~=5 z)6gjpp7Bg~eR3h`JCFYc_itKjCjcy+G7}$q+2xqvtf3=J-z@|PlNAB3s_rYp^DYj2 zoAk5JUFYSn-`@S3p9Fp2-OJ9zc{3KMv~%t(cA?Ws5Y)@2{to=-j@5X&`=A?RF_m)8 z&Yy|RV>teuH|+~n2T<_I!Zr;;EBi?lma|1}+BS2X5{noQJG$C&-cSA;*PZtoylVb3 zycAu%lr(loiD&=N5av&rf$t1A;VXOB;pxu(*gMb_0(Gm-KAQLH?$Rx6Xu$>37vRmu zoP^raaY}Ttjf$vxzg9JJ3UQ8WX=7o#%bB0#U^6np(8y;^NE>5IH#7t0bQ`8-R#6d zlSRVS-rY=sYPsUC0J`JDh40Ds9iN%tnbkuBeYkVq7F;r6j*BmKA!;#%i4ewQkbUj zOFjt$CD!M(@*2mu*!B@jQehdIwcT1Z$Of)0jtoL&)P>H{IMU@Xw?UXVJ~7U7!a=%z zp`MRzp5oe>)ZF8=!0Ry>b7qjSVmWZm(oXmXpE0MhpZE;AyopFs$h$`{T##n!Qkn75 zXT;#|S<~{Ov&oXxrTq7X`O9(ogcI!aIxt*Y0nw+=1r|zWofbYu zv{atwz~=0>&GIz<`DAMO2wm&#L}C$e2h z;<*G<5qCM2jPzJ#tR|BL-`XeO<1k4jM&CZ=XSM;{oi=Kxk@0NNP%dT?u*%`}L&z_?%b*V~_8h1=IYjlRJ_ z6=qJ;M7;Cti?DR+OqNz8HMjdq-8YloIO*GQZzbm`FkYa1r1?aWCOh{`sjkDjk3TC& z%hiP*D9#Aet*zVCndWM-N`wp1KoZbi%b&PDoA_QC&}G5stcQ}M|&Uh$HYrq5_-#=fq@INaBZiN)pk)U15E~_L zvZ<>L`+5ynm*a7Y1UqC{Y+-#1PM$aeQ>wJ2oa&dr`J~!ClbF2wmD{hTD}$wa#SBmMZdeb1`vJ0~n+@e9NP400#V zKL&3+|6<&D|2--VGA4b)eRpdqx+cJIS~Kw0+xcW>c`=@Qul<%af)V7)U724xMv#8Z zmDLKY7ylU_&GttF(4A%W)MTKY9|wl{Fn<$~ZscXPE%+gQO*B#e;hqin!0Z!HU&4SA zcjzc>cE8~b5QO2aYpoXDH(Et(0;0aR_)nlj$5hY$-0W6u5J%FRZ z8f26aK{{?Hj|^400JtUonu;z>Y+pBde!vXs?qPwQW|(#j^x$i|)&$|)F>5^i$R$T2 zRxE3&6-S$84#bf`qgnlM73FX;hDa{yVYVT&^Q()E2l|C*~OB@>>Q>wznbc%qoT3##{ zm5P+=dT^A9O~_Wy;4oIdxDf|tE=HXM+S8gs)%2-N&FcL1sm&ARw-yP?7pK(MV@J#$ z0rL^FcRNVixEt0!slmBi_{x)^A|sjC5(VF@Q*E6j4h-cL{g&guy<5#IMG~B=C@L$o zB)UWbUAa(^A1RTN^bzC-h7?fOz|oHf-Q_H(@$@UunlXL{4|ig;w;yHA4T|<8W_WTQIT09@cRoY;p-G832cwmji8MmaO0msVUV?75Y zP(SG2(|P6u%*#cQs~=x#f93SpO02D#y2t?B77T{~nMt!bGim%jxqUOf{g~Zn4pfzw zcgXYad3G}i~Kc9;>~@V zx`wA~qspMMP*(pbo$3*$*<7mRshJ|5E2JnjB^ z+7kTx=9TKW1R^>+`nzz^6F)V_tkoLEm+b*%AhIG?i~-vF87v2Y?LF5_Tau&=|0Jdy zv0Gj@0goQu6LJ<{<;udF%pVzw`+pl(;uFW5fkrEh4Z^W$&W9l4g#1!0;Qg9mj<6-8m-!rNv?ToN>&`5Nn@OIs@ILkz{o!MgT2akXXj0KoV0c zY9v5k;1*wOa+V<@M=DTne(o*>(s_~2Y@CYmT%huO6jF4=QfC_n2iFYDs=RVQZvC)y zcK3AnNhL};)z)lL!;K-^Ji^RNd>(TBF`Rfj^A)D$VvpKBvE_>Ll@$--JlpcZ&Lq(w zgP&N^5eb$i*U1n*eHmtYO+CJF?yGUh)cN@2<9A^<^@Fk!T)X};+`RR9{NoAd;q^;T zW^w7jZkdeLtC__@Mi)?ETkTJmfM~=V2Wg9=LAqzAZLZZ`cKPd&lNkYY8H16WiF6TJ z7M|L-6EFMm7xCJ~C*TL4_zz60ZNP8qsDBZINc+e}&3U^bwJ4-7zufp7?v_$OwU@iH z%#_I#KA*62*n?i;`vFye!PxnpUU^GL29>J%QtH;)Q<~oK{3<{>X70y4Af+smaX4lq zjikpu1Bn9rV}nD;)zzT5qE6ap3ah z_m2yvZthgxS=_AIwd3mP5lGhs5UQRM1HkEsy83z36gXGg7i9e_&4fTjUQ6`V@pLke zq!~i?IqIbJj?Ohdwz2`w^tEALc^#&gRl_YL0)FA!^c@a#ZLbJGZve`LJND1*Ik)PO z44en*?DDs6?@rwG^rKiO$AN7f+GGC4Q_n`VIzCOLK)+5t1noy7HBU+U!PMH>`$C!l zn>!AvpJnh*nlu9^O`e(Z7i$sFy$9z=k1jR_MFg(1<_VD%hrMSDUygNiWH@pDLgqkI zMBo9qV6z%8o#KfIuxjL5_h=@Azo&7Z@$w5R7x4Bpy9LnlXv% zHwlyE-hx3VL3-;ZoLJSYeld^lAhXa~aU?~j{pS?8r>|d1(SL>aT=rT3oR`-AL!OM= zYVz-czQCCqkZXGA zN7GO0q@?`sW*;v>`V+>B!1LX$xM<~1@h|gF!<(lsNi&HqT?$`{!=%PS3?9XwXCNo9 zp@4wn1zkrIh*=#i{ULz%L=&zE!Y52?z+c^c#-I|MXl3RRLH<0OEq^C01RA+fKUNn`lE&i|@X{@P zC4jChxVwcPW&|(FZ*qhggUUxLU^g6pnPX_+q?e{9v3aLr z1}zG4LL)+BX#pS?N5~M*8>@v>xnPX)hzRU6Mu@k}uI|IIV%R!!WEfA!1s67U8-pdz zjENHIDMhff5q;x%iy6guVJ4os;(TUJVU|htS{zwE{ZT$ip#%^bb_<@_>x%4?0o z^~o_P!SPsq6^aQ|6BwiY=A#;1=gY!rFE=#m?)CZThN&p||989#oxiTiDAOI;dr0s3 zq-QyY-1T;_S3i0rXw!MwXB`W$!| zNwYdjTS}BCx1eG|Gjg;gL4NP+>cZIYNC57g)<8Z~ap$y!jK7^%Hx>325(ma@*(^vb z08-U`%AlH`*K-*lw^d4pRvp-hrw;7EIg6KJg`BI!a30@z?AN&eh3DNMSvrmOnZF7X zYU=~w9WE5W?KO|Vy1Ng;ghu1+LIN*?{mwykmNZ>S&xNN&vGf%@00y5zOld`2v;~ zQhUGVu{)(y{Rv!i<}2{lr7x}H*lHx;rXM*vU%KBM9Y%Y4sfNmnOC%WAzu(32BXh&q z%>IohRDHZ(PK>`;`p4*Y9NcfM9r_M#c)fIC1iLa1hBgG=IRBQnkt1V}u(W|_5|3_e zk|~}|yKlB`)&M=?O?h6ZHQ#jpB}&b`r>|GNyY2Bul$yI8^BRb=qPXOua>qqr?eBbX zz|PB;#RM_G!*bz#e{YxCo;kUy?$2g_`u_?**Za)X9eCr#%_S@%XPq_O%@7c2+n-6L z{k8*J6)?99%T6gtf5-E`#=mb_iC>@gdc9X2L6XW9%MsWlSIXL@M3jy(N-W4pk^m49 zv!IR`i5MiXJQ5L_SY9oq=o9f=_aOwhhG#fonaEm&F!}%ZhKKP{xyaCvD+R#R!c0fy zVA~E98y#0G_mjV=SKbuSm=6F;63np-^u&V)<2yF-3rnDuEdUlwN9Jmo13^iFhZjm8mz;~K)s!x<{}?y)^vUF>I0n}aMD$?2kdg!OE@ za^kysOk{b!EU^#DU?ga<0in1)vS4a<}JsEPQ5^>$7jZ$c-j{WB0SG3)P7ysXFfDE zq))uC-HZX7A8vmh*Kb~hig`0!ING1y&yRnU9*ZV4A~&Hy_Yc~WJTm4Jl^(vcVI@|| zesa@gZx2?gEg+}#Nm`y)_X&^MmB~_e`}qYcV(4rs~Iz^q9aZm#iloWNJ8l{Tw#C;Ip_E45Dao_0E7&4v{t zqpol{tG;deQO;-!keWH=md+L*ZSVB$sI0jov&~&Um(ML`KWGu1&7SwR0>WTPcKngvM*Opn~d6-UOQ-HXczZNZwY8&%w79P5J@z6!@pn-u_iA1~Zl;IIwufH%Q?X23knvb2Ug=FfBd^I zrwEM7J$-1mp+7lHt**nX<}HN-{0`8kCFr-dZgpwu@y5rUX$C{b5%Cs$elgrfhzUe^ zN{Qnq+;=!bblo^#NZ|l&MmD-f%Aw@59_(JpiU%XLTLs1r39#JbOPg3fT74MUKQ2*!m7gw`x+_rTl37a=uO zX1kbPQ8?Nkd)z!=#zbU;^P`MgI@$mYE+?Y<1fErnj~#@ve?g7___Y_k0Uus*5w3mue%$T<|K)w@~Lqk4aiJZ1$(yBJ7g@BB)L(uGL3hZfO>pvY;62)FVkFiH1IqAu zHt~@qb=Z0~i!#EVpFYNn%o)%CJ?VE*;Ad4mF9PswYrZ_0qm-?wPs9l7PCuhz_ zpS5TKnx{`i@!=l4XZcya9oT;I90$F&tpt;nYfqkbi3|xi1L;9~XZc}qsN>LV8uCA> zw(sxlb_DFC$uo?~zOJLJ#R}`5>ORQEWSah&VGTGMmv}nQ`yFd1#sNWO%}H$j(Vv+F z&e#n~IDIvdnrsv_*vRt{UJ-cO(cO;Yzw=qVZRx4_wx2DW_)G&4nU?IL>c8`{As{A>5rbLj61q8-{UBQi34H!S+9)W&4XQOHFrJf26?-D z#aR-dzu?x!A6WA=-h2VuGQz`)bn*$$n&1nKtUmZPejlfi<-Ft~FeKL?d8^zXao-xx z{f_{2s)s-;` z-gqCZ{DQv>;zl~H_#-idHI*1UaiJ7@muIA8{hE|SoKBhml@}GWlIS`O`iBrfS(A*I z-Z%SrylL8E)D@Sx`pFr!PV#uu{>}K8&5xs7F5K96wmh`G;$yNuy=UgKaVH$Xz`A3| zGQg?!!rIs69Kn8U%e*;;)`FTi4;H+#us5!MS{d7*2$nWamA}cXhTz~(FiLIQBK+zZ zE&W30L;DEpo@E^oqi53=(Zt0-TO1xNj@Ve98)L4{k(mQKo>Sh^-7J8J38H~wCO^K7 zgwF50_Pp0(Me}qC)bGP?I-6F4lxv@VL<05a@ZnP~!0VTuf~*PE*<=!YD@59Bk?i~B zGR{%SJdx2Z`u?`3ZkPRZ*aaEh--<_eAJpgH(mM%?OOKgNmao)tJFYZFD#Dp^9U#H? zo__-GTzWccO3PerBuUI0r2Db)>gtB}hTpj6DGko0|Is3I7*m4OxJ>ckK4sna$(UPL zzxGR#bCP!Fb4r3uZfJ5NhFab7@^Q%9|RTwAdPeD-%VokBE%L7+2l^uv*&b zPZvN0LUYX6n+@mQYu}hTVX|Bgo{Sgv?Z!s=>y_<5_C(0Q7S&2kO^uwJYH)YoKAc%e z3kfAEt{gK!XXzWyV+shH{PF$dfoeMs6Q7gL!o=}=ReH%}cec88Xqq~I<{|`4aFj1| zgS|t&Zmj9KgLN!EsHI((-N##;ytIFhkrhsK%T&e1XHy4{f`Yuk;V4^ zzWGPE?$)28lNP}+GwOGYH%&Rm6`98*aVz&iRSbGXqUTK7^l& zS-x0`Q3Zsm{M7&-GeNg&D!5I7!X!UiFWCkoh5qJh3860CceXjok*2Dn5!i6 zkO{KKi&ce+9q(mX-w=KG6Hmg%T)O6hQ5SdLBrC0mPGt-_5mE_tB_nZ9y#1rN_Wai< zqjL0VkL*_;e(0B2@r_So!-2iQ*mP?PI6~iWK}hYr6y+6_`0CE*uy>%>n>*h64ZAAW~ymE3v_Z3p&f18xJJSZ7FigLJEpSs|hDR1wI zwrWYem2xYoaAKcLwk2T9Qyg?08ChkQiw!xFo=ZO?^;H<0)ZowZW=DSo_s-Jzm;kZw%cLPZHnn^GQa;BDzTmL}ov-ch{}DALsq>^Z5SrPv9?Zd^ac;e|v`p z)n4}NAN@5h|K@+;(T(f$S?aT=0B0`K0G&>9puQv*VNqTR)K&vCCIR)eYH_%$L-HN> z{1UJH)_*A}`pDQQx(E94$K<|u?_D_l-EYJ-H+~15ea!H$XL^H+zUS`(W`Owt+g@bo zQzDe6W<2atNeLgF3J11OnPhQN8F@^3;k>=)132qV#^NS7wK#uIlQcbWj2AS-#Exyg zNXo(V5xJQofpZ%#Uts59P52Q>x5<#)w$3)S5c{X({$K!It>h;OV|{t&5sWAcwz6tf zn-KgQe94$Q>`F~IAXq-kA5vrg=CX_N&EqcCM!Rggnjm)^*ouYse_Kh~W2@S>3?yeD z>#`F{E5oz*63d{nsU{v-(p@}ZHkQ>+K)?vIlDK=peuz3>68di+8Nk16TZJ1AZBaly zWT-_5zQZPP51y$G(-H1Qq!u7t3ViS-iLZx#Na!+A$cJH^sLbFCS095;9IL zxatp!m%zE+b7J~eEQtwj;E38}x$yFTk3AP(KmKA&k@t>FnGVt|S5dFs{xnW~>;^o3 zc&|sDCXVS9HMsTUEAYc*mnsQ*iV2Ec1e|?Z?0Z)DB-C0Ear5PWgV)bHPJ=rcaF3K|U3}9Q@!`8~!tSoa zCXFa#$c!#{O}~8uJ-BuM7N4Pv@lrB#GK!n(&19MQc47&Hi>Ll}Q~QBb%0ZU+gbdGI zmz9>Hx~dw_9N3K?Kfh9a8>J)F>^|w}r$MF$?89LQ#bb#;_a+&*$ur^bRiUGv6ho+!yUN0y>zixG)kl&}jCW9-N3 zE+-dWt0%UoD*?jdigMJ+#m-kX&cc;Vb1}WNDkz^CHc8me9k9&#duvq6s-as@jtJ1Y zZw$2adphQCy_A@f4F(0$C4itkmS@acjM|C{^}a~tLZB|gy)fL3yL$Ix%TSL4-7s-G z!0hsz3jsIFkIlE5rRct>$b}7z*eNh=<}#HY+~NYvz5Q*p0A2oS$|{t$j;lApfI06M z(#@)zS*dYedM`~vw(tabJBIVy+g-BNg zE!4|CxVNVh2P9Y@&5z+v!Tmu1IstRVMwx~ZuEFAJE7V9?e&M*!knK%O=MKSx#j$<@ z(C^>3L ze)&K1PeHZXyBC%s#YFQ?9n?t@{q*Z6dk*5?_dJW6+P7i*FhAx?q$bsX5+p#NHY*Us z8&n?Bw=qHGTMrO@ADCE=u4Qx3d%`^AD@u_WsfY-T07HAi$8ED^zR}+EhRC{f;xoyj z^Kn$m_WPSz$Km@YUWT)qW_Sj}EZ0-0$2<1ol2t#)wcE@Yv%=R$tpwx$H0NYIc-k9q zX2aA}d0k#(7uAg;Z2Ws%uN+(h={5z%1?+D1(S%vox(~0s`6~*ftFlPO?zY#w$NP<= z0XH86r22(id56i1jb>0I<{FTPSr)IWlYXC7k$(XuAB(EZhHLZ6fiH7z`ROb5B2xp zx+k>O#)u3%YWh2ukb}Roshf-?T!KzuTuRIdh>N{#@^MT9Ecv5%eRC#hX5LE1E$AML zCFmKTJ&>L!X&|5Xd#yc?)oBzDji8j&+a;qqyuVE!M`nHmjFm*1@oTRB_*Itg&P~s$ z=OKy%X%~;cz)#}$#XAFWrhfS`Yq>0I_12%2z>j&!xnG3HyQApHU#SGT)stG(p7sfH zv9wHrCfaBI*Ddogue>%jUSnG3=z|5i5Qx>iLjbQqvyhDQbt;U`@?&@1m#ivl9dOT19PCH-I$^j!4K zgLQq3r(dKH&Tr^H=Q_aDntE`+Z;k39UH~@lj;n2Pb7Xu(vwAHMEYi95ag7-V^5b6c zXO?ut^}vIU#PM=)Y(M-QV8MSdpYu4Qyf~MH3EAlRBAFrhJG$EOw%fmt^M3IEu%ojL ze{rE3mJ)Q5ct5sjy;fbPA8A3>Rcl&FV4OfXfpG`aqbt?_#70TfMpXD+5(JM)8Zt_Y znMVR{e?Vt}IsG1d{u##VWX!vqhYqGlJ{iV-O`jGAm28|(4#4@h7?8)f z$lk~P;OEacUdl_CrkbAcqosg=peFvtO6znO zAdAmUQqPXcn^j$yNk?Rc`-BAOe?4t6{%ONQxc$J34g`>IG9ve`HTU4*_C5H-g3~Zl zt~A@QHa1dwpDZQmr24*nutyOX&uG9HJIO~W3rs4j!Y3D=DbsxbX#k##b8k|Q>w`}` z^7>S7E1v3W#pL1&oL13Re0$Fs1 z^*?QV7%MyX!Gao|*V7xO;7i9|ptLc(28dxI;(7&(oC8k#?kF5FdP;@V8Wc*Bj4_ar z7Q$-l92&rXK5-Ae_RPaUxxRAIl~RJwXFl^YzPG$Xn~w1@O$#7)i&Z=m%kN1UAqcj zSos^Zm4fp6g=ZebPd7e;51sllr5Zo}Nb_#5?7KhTvoXvsY)<5TSt-h9OmSazJ~VAv zTG5EvwN-fV@NRUB^&>VuX&0!vjp}uwOi`Rpm0iF_)4D zDSa;@AWe(PMaFu34(bp$uzAFF24`5#?(5d=Zvn3`X;c{1_L24ucMf`>g=83mLy}1d z%j>V-dFB!MUCu52gBUu{>gv@*Hvagxd1VrFzxUkKojFdfY;p^l&pQnrPp-jemtNde zGW85VF7oWCG^R3NR$GJedQF3Ak2xKe#u+_5E<`X6d0RVL#elg3?D0F1UTg~&0GX;g z0rNdOanq`Yv3~DP^{!#k1h?IzvM52^E?y^+|C45(B-6gVQ+7jt3NsdF_0#61~zgda$NoO zv?X{*j&bvsWzndZ0pt7| zn+G%fazE@l_eDUxNAe+!<&|ivsKOtk`@I76ti9yiSs)_whO}JU5vM6~B zj=785iU*DgQhSj3nMpQr_gbrbOJS;yn++n)uiy>af@Y?(Fq_^fJ1^8V2Q{IX}S zdPN|8L2)Ifp$yH!Gs|8b#gFyWU>0G1k~oK)d8M&iBqa@!N$#mEMo(=yhAO$FoW;qR zHJ5>`wBaXM9@bipmcQn*N|Xq@ht*8(LU^mktW0#|-9IlpO|^kOF3lPz%DMh_-S%hj z-QDZ7aV_C0OI@c+NzlJ9I31Txnw!e5J_aUn@fKVWOhN-3R#F%M))-7C%(8VfAw=}t z16;-oQ`oOP{SZF)_}%CtaAoR_46}UYqQAn+=Pc2AVsnabAI~s(wv`Pi)%CtPWD{USJ?z*@(SEo&D#P&(%3h^!Go4_R&ekaz zi6bEXt*0Nsch)|x)ZF*ScH{~5mU_&vD!s`q2Ml)4{D4e-gs`{0m*PtX;vbYhbN z>DpSmD?QGvY{9ve6Hp=nb%W4EC^IAC15QVoq>T}`*m1q zHwMtH1m5mvw|nrZ9;+raquA8@xphGK>6XtgF$H_&Ely?$e8~c8-pw9cAcH zX$z=DZ20~jWXT3(Ikxjq<|GPy@43hDy=Nc8HRoQAe>m*|)R+B!jwt(6b(LjhI4l

`AGqiWe7U~YgKzcA8|gU*>G@EKZe3#?K5)!wxZ>o~<)=~9R{YP_E$u7+@7sQc zYku(~t*USNVcW@^z(_<>eF6k3O=eJAcBLGU4Kv`NtN!^ljo}=c5c|S0r3>)Mx(1hUUGKtI)9S= z-Q;cOXrKC#Bg?rhve~Dh6pgaZs$?7u5Ug(tU$Ce;-FwqAw0E>$GOoRnN$xF{3{hDT z2Fi=!NH7Pt48;4o7Y1kx|8t<4Q7#+M)r&8Clav_L%eoJNZQk@5-v(6@G`wo^0=#D0 zBBk2y7KzOML29n2I`+$jnw8kldl;bDodHt0H*@i^rR_ zA3exWIzNdC6BFU6%xJR1r38Jo>Z4>x?(}bc2CrXq0zP>9MVMaSR3K--FZOTp zO^2N|Lt^cI)Ah__FdGPpfM}sgxWlRbe{ERtRwI zDAy*eu4&Kt%H3Plw*kqJC5-EEGXw8`8Fgh<=8GgFn8?L|M1u68)^?S4c}=zQGt^v* zq53Mfx1WP^6}JNCy(sJM*Wj~D8`6^nQkSagDz#6(PcBmQ8KW}R8a$3tBTlKZ_w*}G zg|Tiel^$;Iat)D}3}KNa(eQL&-N8Nh^2&RaU{WM?qG4)R&b~8@afrBOt8AE-|OL>`013#%|VdxbWi}9+>EQu0v%s8)*w-1Owg)^ z>9=rnIN+)1Wu6y+Wt=T2+O)2D@HPe3uQ}&(yj4ooU#hM_N=y&;^{V~6?|u2JF}Y(0 zzOrX6_R@S|rRSrOO1qVy+eJZHzf-I0@R5@*#0AqA$hh)q&S|Wy!T*Hol==Vf+kcAd zZYRs^y~qT60Y90<%vi`Eh$x&$^;mV!c@G_}0qQwXkhb$aFIb<~nm?Ym3I=2_PMMQ5B=S8sUi1}$>=LwMFX;XkY2KbymR&LwgT~^rvF|E0r0kmcJ6&4yAJ9Djw5}c|K2SpHe!V6si38%d`jNfE|_B z(cqj$g!5c*FMR$|8svgK;KTPn~0AuT%n}r zSs=GtGHjoH@hN@!^k zVM(&Svv{a{biVof?z{ny?J`?9%m!&P2KSNEFTqvEo)YFQy5}R2C1&0y8MBDZj)=l% zyvr(+ofvGGByCgSeNKf>#&pf}MC(KNg~T9N!EJgB+V@;q5mpu5x0tQrRANHJo1cE z)hw+jM@4N7ilh{tmNL&j_X4c!*@vzJhjh`6I=I?m!Wb%HjfGOi$qzC8k|E6-EA2T6 zVoMj!R`(-MJg2P7tA1nmft#ST81S9dkKloA+B)uVTL-$H*$`N7k7_2kW;#_~Fa5Ur z(cL@KRuhXWaFUb)(|+-r+qa?19LrZK>!N>XDBOQ<;;Cq?M`@Loc#(DXOX{biuBg zacTksvi-{>BQ~MF5m%mlA)eduB5r)-J}i`y^b_YUM0G`l&cC5a)~=4H^@)djp{@s_4}svfdb?=$a3dVxWAGl6h^VdVy^7wCQGO3fW)8)CrMQ=vZLKu3aVx!Am{ ztOED1cmpnY;%0Lct*6nv-84sF#^x#7lm50_z9Yf-ye<3^3UQSpiY{Lb>$UGq8=Cn|6g#wKY*ST z>ai)G;AA^74QwTvo^kceO9Bpl?6c|c=4B$Re-7N?=MTVrN7l1RfPng@^WK3U?pd#- zsk{2R{9utcKaybjk@nsA_<|L9^R&fCIhmnS6k%~ygOW<@8SF!Ue$?m3-k3r9Mj6K} z3HJYc>r-;Ie*_V#ftQ4tOcbnQKLEqQ8g*RFEd^z!Kg06fVQX=sSRh;;&mmYwOOHIH zs7W5<=M@n_EJW8*dDnR(YwMFrD|L@aG$>afUH(YTl&;g%e*e>PQvDo`UzyT$aj4W|qxBk7~nsKJwV$;gSNNIri(wKVNW=OxjnUdJtce zzb?C1So*Fjt-yOuIv4Lb@odzWS0a|Y7s?YaX#xgCIo(8o`3jr|VuUye$G3aW-^Ik5 zEv_WvELfx_2ewj+bgH^kaaFhq7Hkp#!p~j4dO* zIIg4-6U!=)nsi+~=!`9Hne0;F1%x;C*skQiAcSawIaX#O`rT!lw0no$t>ubC8gXXHlbH$W>Agvjs|T zztN#?-}D@Aetwk#=_EDp+qez=TXzLD>*J2+GhIiSi+j|a%pP&Dt2J&%7S_qNNV-6I zBVhT?35&39paXYz>_%y6DaK@ehWh#yFfW$$u|zJ)kTEz~q#&vJ={1wIN_*r)Mjm0> zHU{Fhz4^9?0I@($zZ7Uy!E$cC=hA1or~T!m&c0p(bn_CIpRfW~oqi5#l zL;{9QZF}+9mJK*<{$d<6bvjloT!O_jX5-1N8*%%yD{EXVQ(5lPU6Evqk&tuuQh989@$5MekL7rCj`WEgIjs$T`vBVuOr7&eeGIA`9| zmanJJehdC>^W*rzo^_mPr`pi>|HRQd$73g4OEKjwlsCX%d2ab0bjRme?@1EmC2*w8 zGjlSMjvo2zMnWpqbR(%RA4jw^orWxQ?q8TjORm%rrpr373;G5hU$!4b39@eGA%;zlr7oBkmL*LaeX?T$PNOHt zk4-BVOh%mfah~YZhVLGKsWJ@rlw6GL9_&ZY-~h_1OI)0q!(mSraWHBie*VXD0^!Q6R;T#)HJi7d1BT)$&5%`>dvE`1IzL z3Y62AyvaE2Re#rW_u%^N&*CR1y%ICIfigC5noBBBBblCk!~NJhsMRR-Waal)k@wT2 zgm(7Q3-R#{5982KUq+q^b~ENJHPfYO5IH-LgF&2xVJwA6*mLkbD+3wX9xuZg1gu~h z*00cO2Qwt0X*6o>>E0$#Eg9o0ryhf=rY=(FnIi*;KHuGnk8XSzkI=DcHc{q}K>5e! zor1bzBPEPLCC`?7Cj=wGzr9P9r2r5WDBLr?_T>He+@rse1ZKcU%=LJC`TXPXnG0Uy z)Y+4mFu6pLY960xeBrTs zaMPRq4%1}Y>T}OU=HygdFD1m~WfF9asCbDvcl&CIX_7RgL{3B%xe`UYT1R?=>{v&O zJ0uAh7#T)INq8EA1sv*0RwNlg``EqODBQ4=rVPnNNG}bwtLI+o#=)l3Fi=~V^wCr$ z5-5+{kNXSEy>ocALqB{~B;_Q~Jgpj=}z ze_e;#Fem|OZA+7qri-3_rTR{%PNYB%A~Fd-AJKF1ghmO{{R+Ib;tnIpCcjVZ-i8Oa z`7>q7NZa84)`DFq^Em=iw4Yp+hb*io-f}zt6_x9Dn;^k?xyS`^99z+VA;~~L(6tX0 zQc7M?mEIc~OU->B7S4V|AT_t&m}J=QrM6%(1m)(tWsF1yaF^G9*#~cuk|4d$T)U;y zXX72`U4q#ylk~A{U?3k_>Q4Q9x%5YiMXPsg!9F>jPMx<14OP{+=(v-xVc#yS+P(?T z@7sY(s9aWc^v**UQBblA3>T&WY;vQMiuq+~`t3W>7_n2SJ0_(wI0Sgx|xI|z+1=7ts0_vn` zRWE^hL;3%_{fVM$q{HvQnLdc1^Foa0JL6u5OMJiZUVvcH%C(0NcC^v^$Q&0GC79?A zSzlU?S5BO(_NPBAfn4{-ET zln@miRS^m?(^)kVY_Slb5gxa4X~K#E?PS04^d8%>8SlUShxqQZk17ykiT)Y0=Hn|b ze-l1*+RISK3}D1kjv$XL9p3$zf@Oi`7Hn8qrjo^Fi9`gr!^9bL9}!JnNAZy6OmaF` zI$psiuc!b584;#@aN?h_He4|J!efI=i_D8?LJ707V&;5YJZCYw2l}z$kTwQK7S+G= z^uyRIB|AMO0&9BP7${53eS%UcX)da&a8|t)NE|cZRRXf}swZKh1Z<>g_?%q8YNN$M zyJy)RLni$$oj4scY8tSor$Yf8GOXfN!E`0qX8ZI*w_|%ptCBF1(XMp0u=b1=!1z7Y zPZ?!JE-;L?b)bLuew0gTcV$bX`rbdO9%FQ#cgi4|uZOZ8DJ|&JQtIw!*CRhTj4VeB z82z1^Hk4LW=(E0M7P00GH(_G)P8SsEo!PmuCiPkmlK+Y8`!xI`Qc%ewc_e3 z6isYWXSQ=!Bo`a1=FU*}BW)c(MqQ};EQZST|C%N9)lp|dWMz45Oy|#8W7l<2-XNt` zWu;hHQIA<=H7PPYSpskZ-(6$FP6Mn~(%&VOjX0}jGR~-(sOV!n9#!iz5Xb45HuPq} zcrG{}+<{noM4I_-3KpCZZjTtdY1Je6!ri|xQgeX`QWAIdtFFQuPd^V0)iv&XawVB( zN3acuJi2JPUEWV=Xu|x7Q>8TUFz$O{jY@O61RfJ4fLK0rp3?aEwS3hMMP-;>B+OiGj3pAcy_j}?!$q==eIp*A;?|FIXD`A_$mQXE-tPog_f&P)=$coeE%iPn^e|>Txm^4#qQM2q1yr>jpH91<5))AcA z^PA?jZ8Dku)8sk!!QZBhEpZ|qJM}Heh(0?h_b%R}?%v5Osl5NkrYFL549%V)SD!yV zZw1~weX;+LQMF|0eaB!o`sKv0)Up$;j!i!4_zFtv{<_Tq$$~M3WbrZvh zoekJK4sOFoq||)ZKo8>Szg|kp-#c?TuAYCYPI{bSFL8aWB(b#k2=_ZB zD7*TepW)8UYb@uYu61$+`=e)Fiuas+o=ZQmd5BkAUB(%K%8*iLmN;SYAgqM2T!<-e9QEj|OT_phh)P!Y9y`2F1 zkN3WSU+sHQeM_v7pJ6iiGlKu~vMaDaN+*-PL6k|*S1uRKCQB*aW1dYe&lN87x++ba$ zuVV|&6aJH9;$T-$jkE`SOqL;$*OAU1wcmTJZ@^s%$deS^s=ce|B||kT7YUM_2%5t; zae`vm2Bg|fetPOFQQbA5jLaQ==qYDmy+nSmJncA?n#LWGZ6r&Gq9V_P?~w zpj&nB!^sG?jtyXUM?0$Jf=CT*_mS^OrSYfFJg(a96uGduVDdCIM|Vo#_2kYi*xG&w z$4#4wlO%8_|JGih+ZQVTUDYs0FCg=B*m@EMh>NI^gLt!nOonak3lqX**3IwMYeaoxNSB5d)sPVl?*UAEkO6iPX__1 z%bp&*v-Hvy(cUMfzW_m6srfY>Rcd~w^S1K`+O@$l_aV`i#_b{`fA0b&{_2>-e3&?! zG-lN`;hOWWz*~<$1AlS<^N#EUeCkKv#`hk$o3$Hr37?pgsErY{rTb1-+NyNElt5RP z!}S9`o_L4jb643$xNjK>n zdz&=5=(~?F*gN_!(DMd86T32 z_l45S%9l8rII^gWCqks2Go!plF+JAkn^i5Ox+1Ci>n1J08^}7lWsIU^NK*cuwf7oz z_H+fE)i@1bJ@!Inf52t&D2GKMTFISz7XkG+jQ~ViVipr4^GvJtUtM`0zWU??TAkfa z3iAI)&U!iCd&>Fh2sNa@DNDz^3kXvL(38wTlv zlj<0@-94QepiiHu0D9hlbOL6yAG79^r8@7DNwwc$060B=efnt%oRc*CovH+iqu5kAQy({l!H4qKaX!d{h;#G(`T*4hLbU0LC*=M zAz?|k&=Ny3zDd%Uk@6CBKmMG0SHEzMvLZetfw=?cdB$jSaBkj}4h*AMmVpe+k(#?W z!Wbt2Kia9Ssh5l;DjbIO5kmQ43GPda0-&la%$Jv`GkPInDT(^4nK9MWImE!- z`4`q72lc+6>}WlJ?>_Wvw$B`9pZR$TqKZ4f{{5T1YW@GKdDpx(f=v9sS*UfBNoqF=<7@(ldN z{o!3_e;@w)&Y$7BJASSWz*(1Mw2$i)2~rC!&cFFac8QLe1uUEI_$wEljlTfYS$_`5 zyALK@{4Ms!^8{q_=KMPgOPZr(p`EVe*6!Vj)jM8PhFODR%72xTw?0-bLaN{nY%;(N zON$fy)n`6|O}louI3Bs~Td1vO+ZY&KvGm(!O2l;oKxEMQuw3I%nL_o>!*V}7OqQRl z_gUehZ~qhf(|>O&?z$P-li;kbaPRpy@5eW&XR=}XGw;;tG@Wx53a}oABO)k~iqwov z4rcsfgHn5+G*?-1r+v&FBSUW7ky|U@UB6AMy`OCci5t(x4^4axQesO|;lcbUdf9oz z5Z+?!lIkX$)iNFZa&YbB>ewR6Z{ey=Kcj%3XrO1-8^*a}oO~d9UnouHuUsS174DEd&Gx zXb=&W-_^y;Q50t^WfAOsArSfFXP>~|{`}jxbK`SL1=FhRojGd(ZhP|waoNIUC@(H? z#Lt3O$0Q~hoQ!*pA4SwWN%%#^)jKwVf8Y8fzAwS~;gLc7{NPp<&+^(9mu_5UURIkd z2hiYcQJ@_Pg?B%;B5aKp2{yK~;uquVpEX#y^vKFO)HC+z4f^4FLv#-;Vs6gXGspi7$b zNZT%_3U5`UuRQS#weQe6GNd+CB0;^(XtgL-v%vn|E_`w2Z`4sd{ey#Y4L~!$p0^gM zVt9?Jp!hQ0oRXgBB{=VUW+R4s`_Odk0;Qf!=bZO9RtIYA8j$Cidb@u&Ces~}c^xj1 zc`l`^1pP%)A}*!Yqea44>DC7E$k3#aG>9|~ifgKrs;r=2V=Y8;9dZ*Jk)wUCQZlEd zaEUsHy+qbAsn-rxlwi25SX+&!MFDvo+;c!m=KHnZigJ|Am<+oKXK-i;!z05O&W~Y0 z{>UPIiIHrU)YYJ3#uOFruu*%Dy924k*Pp75Sq{m#lzO{UYuDu$rar2sOjL25S2q?!tL`snMb#~yXU0anp zZcA-FTB_?YZ^9%TlKuFB7uRD{wntM{t?Kv7r_EA}MbGWug~J1Vm|ou~pNnBt@{cXA zmvd1uD@Ryv0_TC+yBXtxsl_|hc6&ATj0{HC(()cjcOTia)qSJIvQwta3jJp=skB9< z!5R!jgjWDJCpJjF=T(#DsjYlZcJ4#x$dKy`VCw{bSac)S3Q(DBS7SlG1T zIsJ(3cDX(+=|--xR==T>7APKS6tWc&12&P7Ay@CQKLiEq9PGo_pL!5GB|RpPUSILj zK0o~r4oqp`1kZ;P zQJBno3uK}}BBIV~fp}_g@SCXbu~)P$<;N$!64yv@PEzxIT^+dN`KPeu(0*r>6%Zcp zOGihwR@y~8Afz=3U>D0b@7{&4-}!TwrSqiZ{PHu-c6^O6r#ac-;ua+l@wF@+u=gnm zdKNh5brbWj+)oxbkNsrbXSo=hM~*ZTI;5bC7HNsz=KcVN4u5YFbaMHm5bImWh4MKo zS?Avh1S?F)86FBa3o(A!U>_DdC{F^Zg6??MFC9li=c9jm^Anl*Ac^ePmR&3Xo)XpCf@YS!nkX^Tb$b*UzNR{?8IrD*yFm&$0uEM`UpD5UcK==M$c+=I8{(Vbi1EYM0nGa8z4 z{j1)BGiNSv=_f26M<98=GKp81>8vJ3cVvEsdhcSrUf6Jd$KwXFBs$| zPsm^jgOs?$C*d|wvwLCu?7|2uHP7~KmDPQ^NcYhsPFY`L`UN&O2mu@oc};i@0>;64 z@RxChAH62E_g6l1o3hXfB|#^Z=1~PUt4~<0CFV}WeMC!DE!PVVrapt8 ze`hVb`+NURAA=_O_C&|4&#@5dS7ezG>4tY7K)zcWqAFW36GBQ{MkM&}uPW2WnaQx# z{Ua!r67<85u2uk>LY6c)p!&=cU}UZ6=#=zdwpotArUVOgCFZG0mY-SDP-^d;Pp(nM zmukEBIqafck>eu;*MumqHjnu-n7sBA1-~3G6ZM)k_Eb8{fjRGQ12Ll2AZ_* zwIdnD9t?Y{$kNXX2X^C0DQQY%L%)2&@NdotJB~_<6>a}&#QLQBCVP0I>U>I7#z&ThQ@o`|=!wqo8+(h#R<>$hm zepy|qP-GY4-Z%Wcx*o~ZmsMb*T&#$Vt~$`3XycDLX4B>SQj-2}lJD6u(B&~mQh2&g zq8Ey8n0S+;m^@>DSyOT$w!+q_qO^%0yXrW^_D&?-%rGy}54GiIEX+uf1dr}roC!Wp zu{JoS6$^D_-_a3|lpZe(!%pP+_+Z_W0KX+66=NShPw`aoM&zTCGxKJ4f z4dz?71$`FJ>1XR9#c7ru+l*$p zu@=#IH_w^lf;)s}))9 z&C{3Q;|op;)ZUXQ4rg*l=xM`k`?sLYo_QKp1PX%W zLhlM?s?hy-;oOx47?;@S4&NwO=;umlIqe14`^yu@Fj#<2>g=zcG#_t~^%*;x1g-3o zI{O`mwz7fcd(v4A)9|(9E=+Af;BCsET`oo8qrC(W0{bkcDojkhGyoOnUa~!`ZYUmEJfsiqnKFCXly!I5d*Xv66(b zMlmI6&sjuI#Tn5Zo?Z|q@Wri9 zfW@V?`00U7=#^lIgLAsCl+y8kz2H@7s;mjp+tSsBn_pamJ>4C+dd3NON-_t#x(+J~ z;+9sAJr{6^Hyo`IU#)AZhM3C@!uL#q>Q zJ=g+lr7@r?Lo>+Acw1tavm|HQ<&~ zmLds~^D=LJRi)1It^()-qbMHp0G(W^0_ev}fUe8j-qwzONzV!3)A4Kys7ncdk-QQ} zuS~+#dD@pwV7+?L9Ox`37?5CcWO&5wJ15n4tKv>=Np)I1ZL->Xeo4c0EU9S7oC@MG z>}VBG*gCrf#|~`jHW!8w6)|Wx?ajk;V7+e7PF#P_EjZNEslr!D#^>@ASKz8M&o}Ye zc!W=b^(hb9^)uOgQPOPrqDzcnuY~Ly59~qD&>&8lJzq~lGAv2bSIOU^iBoXmjJf74 zbP3=O?Zv7++f`hbEj~f!r%VBOqCL)rjQo2=jRfdqEH+|#gRJ~6Mu2PyL)i`8hq+!Kfr?<*64*;MC}pJjjRpA zjucGNKuf&_=Jhq?ZwWhjLS;G$>n{s5TYp*iJdUhzSuRCzqIk3DDfIsI z-zQN2y#sV|W0o1xNwCy_BctuugMa532NrcY%Cs@LRPOi{w zm`W!G*6uyhE>#_@t+S-{me7$uwa@(Vd-2uB?lt?&;Rxt^PQL&jmEgSI7&ywzU&5sH z0DhaHUbq`+mG)OjDQ(xtupfgly-LD4=Is*~;eB(C_xa#JJj}di?xN>lfq{YZBqPt` zm9kJBBr#pElcJj-BmaoLpwsg1S&Ydq{b7!kl zgKyg3iI$3L86=PH?k;7hi~|o3J`AX`ppH-6=DxDDRDsj6fk7O4^l6keHK1vkmiYG9 zS4t^$v922jq;199AKZ6taa}cO?1xFa=f<_UR`Q2oJ2+D#ay}n^7{US@qAJ zsuI6|WPVkHVJp^a z=5s2Z*iks8+Uca`#xR_@SE}vuxqA9!wKae&vD0>jxI6KF#NQ=_zB{1pCD@$dvmo8K ztM2cCR5_OmXz8xDL-@{vcL)2-UwOjm3Ye4BCkEmAKIi(AO+Obc4)ly^T1D?ky?vh; zQQ-Xc=U1urI%(ECESxx1g{Aid(&_YprBh}opiZS8l=b<%l(au3CFjK89zS)K%9C0^ zh6d`dtez?XdWrAXrhX6sauM}+fp8pTGL-h&eviJ<8R!?>{3Z8XEBop%U;Qq93|k?_ zm8I2GS&c?>lu!z!JFl7^6{)D;V^G;GAFJJ(| z(dMo`B~e%72PRUY4TDA%-bJ3I3!^^g)9=(ZVb6(@qF-~t6%weQg}=C?cLc~uVy=KW zTR5_GTsc1hYiDf*AoAy4NY^ZZ`LS}Lb7G^3BN`tZcq1A=9IP37lVXH|@H}4YAHD&kGvQ67}S6x}vrDd4gJWoNzn2)`GK=V$=> zk-`;P05IPxCGSE;bN~PDPyd4ebaGyjZWvJ^Je}f1Ht6$NtgIh%!9W9h&T^c$4gf(I zPq5cKVP_P^U@eI(mWu2>!UWFA(mR3mIKOnEeb4ut{`!^r*OKh%e-p1VsO88~3TUqG~2oXVPX`(YecUXNQZ_LhZ}>S>Y` z2HC-wO;UL}Ql5kNNm8IB1mz-O9pam()(#HZtBF$A=~fNcVy)p!RL zt;pks?a$-db&qK+SMBZq2LnzPFj+=~tMb~3zFdNHI@`RwrUsX^%))&u zA46A}1g90{7%MKqV0D=WL{5UPLAJ1sXWto#Hxe#IH+~GzO_(aYFwbeQufx+3Tjf%! zpA1QGzIlfN=|bkIa z7sJEDMsi;4ejc!wsH>FSqjDs#;-y#zM~0*TP)di5Dm#~$Ybm-`ZLer-Kv_erQf+@j z(>&A`m-;fZUYBHC<8Qvb!nV!Dpp#t%5P)*)&-xjVp!lXI9>Sf^u5^9+SUS)A{7W%M zO0zBC4JGt8eZL?8!6fJKQgvEXls#Jk^d5OVAVwTRy!p_6JhOY7I-2c_Ig6CkoIY$n z+=|C{ZdU$Im^K>=CQMfT>1^E9k}2LJrTZsLor(EPlQd&2zgX?f%&agCoh%otHF zvH)+}{h{{V_?zc`r2xB12Jpt)B3{T9E9zu^u435@g9h)i-=~cY2YegY2|^KLt;Tw{ z0qVnJK2ln&B<&On!fmj8KFUZEK69Gm-fJ3luIy{yef_)sqV|3Sx1;SK-u{hi@!<2% zAOPRUx;HPZU_D{qERe8aD$9VArNC@r1K?MI%$4;pi%kT3k#!%3$-*3btn>8Z^y6^S zo!dMEpImUdGXS@4-!446Yn$wrBWiBe!G*yF2`g@*w9%&4HfjT;0x2-g?=AiO#$CU} z*YEtL$xDJMO%rg}XRh<%8GmWZrgvR>`s)UIhNN8G8td^Qu9pn{k7YD-+!o?NgZD?{ z##zM7j3dq~^Fv1)lSIAv2uC&-a(^&@PEOIMh{1H<5^upB5wq;eC`>%zk!&$R_VnF+ zPvTr;42YN$;ocE7kfpH*0lSK>0K-gb@0UFBQw7-V-YlNf`)41I_s=;&0d|%|%5CUt z$9f6A^Jbta##Bxc;Sp_C4PMvHUYRBxCAO}sRe|;WgS~MTXV(Lw?@7k}Z{vhM-<_o1 z&7~E}C>|Mo%0n~7PK`e?N2Vnnpp$C!l~WhtRg>mN8TI#cra1xg>vudCrn|2Vxb0C- zB;{|$gh{yi%D3Q6=Ux>0WuTc8or1$69{ea=HAk$rguyqH=A+v-;dA%iq5wKS?{-FG zGp>Kl+i}+Hg{%hB>BrqU_+E&Jv3vVhxUq*+><1)3|0@a5$p^Jzr<6{O#nJ=mhNbkg z>rksYSNe=evvBJrZ}s_Lai{WZ+W_f&7wVFQx-n13&(}xp%t!X(bDg=J1_&1X_}A3J zpJ4GsWAtdL^Nz6rG#8h-#XHhG*((|4Z@jPycWhs;09)DA2`HUnj$j+H>g}N_yOp3P zMv8vX^ac21DM2rAPI=G@GW=MP4m&2}$kW1)uo6vpwRgjqi2!)R z0QBfP;DEXfODo~k#?aHimYo>u=thpt{5@tia#AWyhVlkVL=Xot?=>TdGB`&n>0Qrk zaQQB#z2j1XUSb%Ry*qa5qt@IWbPvh}ld99Zy5U5r%u}0;S#5~CKS~V@q?7c#tgc3# z8BfMG-_kN)g7dQMjwM8JteUC^`^nsk3=pnb)Redp>_iR&OeW?W9$4be04XL-w!mQrA1Do3RV{%<1PBc<;D&wZZ2l34QU1~eS6^o8X zX;Cp=Ryhfk1X{U)x46EdI@A>%(c(f(vC&PIbsO$adk1HN1=96O*$9XWmhdpaxFo@l1U3!q@x-s7H`+^zz*^UQ3ECLW6-Frle6gcPOOR&zC6X|a(8pZ47 z`M9?q5-hKspd{uNnD6iEz$2SCs8c^YFXefoe3CdUM0AtRI-gKg3lEz8mFI=irQ26N zifexOjbMxrIG@tg5><=vMX_@Mo8K9O3(JcOR_sE+-PNUipT)EC`aT+3$unLkoOG@2f7}2Z-Jjnw1Miq|oC5b@b^3&VwD$#kbmJq&nrt{R z(*B$`_EzD}U)_bBtp^=pP<1|GB0l%__u`c&vvW#gknUa|p{-N7ia#W&{$L_Xr9tZK zpZm=%N}Zh(PTFVw-ZNf?k6!R9;I~QJr0pj(qvOVNWTF%Est%azSK|+;#0SzZ zMDQ`F_)NyuT;6u*Df~pecyLuDnf#i{O5`ibBx9@r27%S0^0J`LXc1v-UF{8=8*T}^PQhVpBn%ZzJ7=_bw3GGcMpib{eivEK=;r{eYfXtudQJFsN3$7L@XeC=8a6ieC{-0{xod>&VJ%&lQZ5c zaEym^6SF`_8*RQ~kS$!sm|?jnJ(Z6z&H9pZb(Xm`0Czyr$!B+MRRG-vvGtLM*BGIs zi)0>}t7_EGN^g0Qvgc)e-LiKVuKMi1fG6_q%U*-`zVfv!0!xc+5d^CW!MuqeemiV~ zG%fv(=@741pYinfLN1H%=JbvGD?|gc;*9A`oK}H!N@YKrQqzDka~$;1xc?D=Ze7~S zyK63YZ&j%rD2E}~QAkH~<8M4l{w@rZ?Jo$EI4T~fyz`X<6X%htaHpKC$kO`_dp4v$ z%J7#>oQwav_#CbFp3zY(aNIV~g|$8HYCoMjFFi>ZXioym=Uy#7?NEaJ=@=%FQ<9(R;!RBlm>onM?QHGbJ#;@Cww?zQAz+ zWx@B1hzOQ`P}V|(FcqJ#V-C={8_JL2knHQ-`wwaLXO&{K4aoL~|7*-VeWxP!9irMrHuu76uU2d}T0 zri{9cF*-m}zB{(A!;f0Gprf%;S#i%+ahIS@axCv!crq#_h`e$42Aojcj5kTi>vj9q zVoP@${(j0*oG4fINn-dP58Q^`U57C&rAnRcZHPfPml9@4(RKX^6iFanT~&!O3A%@O z>_z{^?apwUl5*3LXJsYI8vQV-pbqPOp>*zek(9U>Rg|Mx0{fEMY6WnME6WuK9?uco z>6IW??V&fE4Y%ZO8NHgCY9$dJk&6cX8@8${ub4X>Wm6_7iG6R0Fm01K4vEf8uNl+j z9zNKHfxU+mK(|KXNX5PGf)x&+lN%iw!Jz{OF+#_zIY4ft=(2wFe)MA0tG(zz&)|TP zpeuzv9_)zXAwxwt`+^EWij!9;VU8ZzjV2>NBl)R+tRbgQOI8F^#hgL~oV z8u7y0TMyuS57>R?x*rfQr+wyC5||6uFMv>D&Y4OuYw^M?27vW11PcTMQhI(+E>6?8 z0ol0iVobeXvu}s|?Zo_+DL8)GY`HL?&r^S~?I50&?@MKUUpRlM0;-hx_Wpj{yG?Ij zJAU#k3Ct%rYw$Fl&#jm!$6n6WjX(A+=caak9FL>Hg_u+R?r51t>Aw8z!}#(u4+Y=J zV4O1=mlZxv+q|c_zFxKq8Ij{bZ11rE-MZqUoKk(K7=!)kfCtPbxTT(1Jf008Qv93E z8w%1xUIcd!9Fe9MbOhA*wIUfDfg2$dIUQ$c_pHO0%P@}GaUjVq4%}O?DKE3?n{my{ zUX8b&bT(d!?!WK;C9e6|4;4sP6&36u_w-fQ8QD0=Ay^~{fD(M&FPbTV`E-4pUIa`- zAdT>wyHC>L;`>5jgJ=5=zAtl%D>N|whdC!IV6F|o4JuXk7Z2?R*UGb_&lTOgsVl3* zl-hc=s1``G0gqq6=4Oh((5pM7Hnhuog5{m z_b_E}vpP$ucZGa%53ZdG#S(D;K#B`-?+Z1ay&QQ@av}F4$?Sf5%M%K)yA<5y4t!$% z3S7P5v?yJlADia$z3o`j)0zV4S`A(^o9blsBymR8G$2NBbX(p`B9@Wo;6B*v9xVo_ zdxQWxNg|1OV$+TGmV*I#hQIt z7vikOsl2(A+jZywK6BF#aKl5tVFXe$es4PWWw=@b^%(}JYamo$;300Bw6VBMhO0n( zKC<-%yzeL9z^;z=AYW(BU8I2dnMR#GP9kTd2@7`2JZT*-bL@N z|B||DC50}PQsm+aDG98uK;?uc1$?P)x(E7oHj{9lx>bFzkP`hexd1@to%gKSi2UFX z%BD?1{e>?-I9qSZ3insPt0*@MKaFH#s*pLBDLrIRSmP$ zdFO?uKGzQ|1I5M~2iPLx*@N@6q!=PCIlgoC6ZozK=hhuNV>bTk%nPt|<{ZYy`LH&P z9N0TUD+ON&vi>Y>kMc|N;hrJQboPln+HzyAkJ1rtYxeI%Yi|$cwoH;heU=JCrx|Q$ z-HR6_V@!sXNNTS3a7wr3@F6^VaJMq7cFNRQsFKoi3iH~QIco8wmljWuL}#;`vL#5j zKn#*I=8qJOD8zso_t_`z!S~m#4Cv>p7c5sG-5HL{VuB0ANeq930qPbj+5Y4g^S$;E z?Ps?Z-`8}t;x$kI((O;T@D zDv7!~hK(Y!YVY|mA4kFxu#qz$?)OM1-XA#wC}%ac;Ja783+K)`1}_D7%X6#nq3?Ye zJ4oY^9Pj6@&Y}kL$Q14-em7n6b;ryED#@k-Yka{wLewdMu>bS%AtI0NUF4Wy$S^Hr zzcV!JZPS+E3roBKxDESvs~_z%=i^KlF>&Qo+d@mrFsY_q@zXJY7CcRZVVtnWy#MB} zZdLovxg)FX-~8~^O0}H>Zw~S;xK0CfPy5|m0yty8!CVp7%Te;ssdt5ddmQh0AU#?n zaQyO-@)M`U(^s#FW=K9uF2GE$nSi5l|KkANI;*DbU16>`WZ2D15q-@1_B*&NDQDI* zS9`bb$91d$Nt*uY%_~#u1N1_vz0W_*S$mHg+jewPCtlOrh8O!fJ-}w*S?%i%-Y0G! z`6&{{Bl}jrU&i zD%ZA0+pZ~QAbyuI;CFSj;k`fphTdmR&g5lA;{<&6@~iNQ#V16O3ZT#;L4ov-)NeE6 z%y^fu0cPVK7ohLoCjoi~k3cymK}X1F7OGw4FNuKZQM5^t6_33S!i7X51}{fT6|Kfk zoSt! zDNTH_zY|;gyHGAc#H_MfG)gJq&sw+P;lq10*pm!4f%Xe3Cg2tI)6h^_p^j)Nlke}| zycV}_d`@CuDUCdEK!J2v0AfLtV1}`Ty|PJJzf_WX+9yr4VPL1=K>AH6^S?+EL)cc1Aoyxlf>l1RC&UN+n1}g1BmZiM93T4$=^?!O<6)vft zj=G{!*Yyy0FCkL<&OP(UfVZlo3{`f)B@UlwkWo0jnA;=YuQ{|ATRSEATy(rraesC5Y}Cm5rW);& zZP!nR|BQ4>FDSs=F{*kL;24GIcFYV$@qFyzTk*@yYl1LStLyQB6VFj#J#!AoN}IhW z3&l<4+MZ03;^QJZD}$BFMWP}T{`44eBj^BHSv}5HYrK}=Q@{9;ysptNY}q-`whf+-=22IV8Gf7UrRY~KqBaxlH66*$Ikqj#j)edu z({t*4bND#8Kf&US$E^@y{60JoB0)sqz~y<^{XYa!2|e`Lhg`Zvc}oG5$H|N8{oZ7}}Hm&l?`Z9S2{8odP&b z`fEBneg3JKSy^Wza2=DUxJ5GA&q`@}_sFmY!3>DIBibBb2SN#XDD}1=o|Mon;P&4l z>AOt>*%I`mP#PxwMO96B-IN7bT-}5i;8?;#fc)KSe;rz9XKhc?@~+lsyifjW%PLZ797*DRq#k&r{_5&3O4qi7S3lIL9T@7@M&dH!QxbIAQ|cp7 z?u^Myf}WX9I#0@L)ZgPW$Ipf8BQcY*5dOYUs3h}sOiYrnJ46X5sW4gfo`lSzW7d($ zEhbqVJ91x@ApMr!J=iA~Q4~m(%;K`Ldi>L*rD%~Ww_^sN+wn}#v^V#T_MNy>{(8E* z6(}av@#D%HaAn<0Op!pZTmkbE1<*+?``SnEQHHa~^-F1QZ&$Yh>wMx@lM+*nw#*El zn9D0FP$fa|D4ln{=b$=AoTO3tJ_*kI2Q*NZj4zjpb5PFTE6pq_+n5FO@ZetlqrL3Y zFFI2J-u}TsWi?$@V`LDH-VsQwX{=ZI?Ax>rgFE*ta9(%vvB*`FOOR*4xygqOUnaxR zq6Vc)=cLow=(bK`33?*~=(eXE?CZwO5~x357Vk(ap{=J&)r$avKz+YGBi*X1cgnW1 zXPwhN!%DfxkuxguCssG(Jh?FA`kLve!rZHIWv)QfyG6FtJWB(@zR!t}856ec&AUUA zCcO9SpI3>Tzw|`>?S-#U`^+_u>lbkB7*(<4y!yKBFQL?2WE{H|E(TdjQJCdWD!(5S(J-@qnA`gBF4o^=>G9nZ_3=CK1Cr5Z~EE?}ZZR5(fcPXpy z&JoR}?KcBM2#7XszW}U91uzSX;d?1rJ?X4JE1>@1i|g^Jo6V7Fyq;~D$>rHSXOI#3 zmM{Y7vTb|FTNfqjs^o^KFqj7G&lInak?G3EczGw8;>TA2c(Y_Z&n&d0^)IA zGVp?upbNdAH>t8o^7@rHk}JAKI{Ys>JGrxN2iJ@ugP3Lvs)E&P!e=%HACy5nB6uHk zv{dvVqhiZbsGML$Aj$9Rrz}(e{Q2%yZS0KYgH+gmw08rB#zv)-dAiGY5~a~3neHXk z`uMQ@!~G64^X{T)zK=YKt~Oq{g#!I#?Q+|z;dxW@hkFj<%Jx?&SJSVYya?}@DFH)i z)IsgUD6gc-{+@Ne!RG!BcrdFoL6+|cnE%_t(@|&k#RdHYEaA90D7*yX?|b>H)&BFf zJGY@r?Y9x?dgtnuxI_M4w(LaIRTefv<>r$lG{BZcfXIF2!C&D`-~NJ^nw$4;I{9pT z=gsfKE0&yqvf`2;OlEpfSpuS0z@>;FCz{|x%G+45?GS=EDd1I$SmXc$c3b9FSV%5S%ay5zzsW85Sr&3*1BjMgtNQ@ERl`_{19c6P{GK zwzl6A2=@;K({fX%hz5>Lj?W1|YJ$viCzm(k{K|>w$&X^=P`3h*2Zjgmb5d!SQuDdx zwfdZ9Q-<3+cH;l;U4u1!Z5SbtJ1~fOwas{A%?w;xGev2olp2+G0_G$=Cp#36@7{{G zeyx&SA|;UJa;y;09igMgz$-Bc%9VrBj)72;#hTh$B&8+F@Y=}0fOEwT3CJP(4#RYI?d=R0~(R9^#8l*3?E8Hz{r zzVg0}+gx2#%$uPMM*TR(E%Fv|?a%L<3p*Z*)wTyGqgtN|(w& zs_rD^uc)b3fLN)tH_pa#$pECrRCbr~->A+AROh@(wvzsup98({^LEZY^Y0Njr|0k9 z|10c}pqDJPlkE=&&V>Ql#$QgQ&GL}5?{nsCbCLQM{Brtd)yhe_G_kr?g6S#+&WGia z^@zOo=f}_>`|RANNm#RgH?|)>gfi*3NxqvTrTe5}d`R~1m3y|Tane#laa3aQgee%6 z;UC?zP1!GKlpvj~x+_&~S&X?AbtoagYQ`6pk<{Pm`w(eVYWCqKzrgg96C&pZZZ|`cu57w?!7T=F)o{ZYkvZ!NZa1Q6EILIoWMNPdY2IJapA-ZJf2wRP>e?n7a+u1x+xSQgH{w;r=e zYo-&dD7p#~vn3)?OC`SggqtdKI7VC42lGsQQqrd!8LOmr9WqDvNjjyBkWqLmO%K}3 z&$xaP;h#;xSe-w&cPDOL_cXc&`f={O#rRWobY$9xZullX^uw=ub#{o1`pKBlgL-1% zoZeT+zPn_OKF_?;91$4z-xUAPzFU=a5r>QXvbo0H^Q;E`;5!@7hx`%$|8K{fg>M~y z31&$QaHzWzE4OXJ^Luv4aW!hZW>ajU4~>!*O(AvmlG0$l6($dY37#w;XFbyPn;%Ge z{D~iY1FN@eOvV4k^Df5cB{;8@w9v+vgFk-S7vGKxpN_9GyWPlT4vvo#*+4x-+hUr= zLGXAqu@E@V!h!L#b@p8rt<7qenD)kTt1{-VNI2|K)$JCv%2R@Q^Pk|hw;2zpa=+|>FDpDxeQm&JJqSP^ZIpOgAb7f zbM~I%>+0{t^>^KZ&;H`Zh^Pv@>8uOzu~%P(8O;-YxR_8JA?-(B+<=e%;)hr(Cs^eS zGkT_+WIuZGt8tdR7HZEz>BZ@0e+-Gy(QLuc)y#>Y2@V7xJxj&imOa>m&MGF$iaQ?@ z{Mp_ItCxx2-eEOec!>(NFW|?Ey=Nma2x*zW6eE~&UO_!sFb{q4eNI~ zndheC7o(Ka=w&&3WS)cL)c^<1N%CFO)TjXcXjd=VAAVZZX7$3^s9G>f(Y0QsVPq}7 z=he~3#Nb!UdLl-K_MHpKFp|W3w4?}q^_8e??^TNdhwgkx<%wM5%g;usltO;(Rd17k zyvDVkrCDTA{N}Z*aK|Qnj3jA3D8Me6J1Pfh0MPz;@=I3QPpE2J+@1L{*ISjhRaVm1A!DiNJr1# zfzdaxZ?-SNKRYHg$Y&9nBoj;0B^#!7bP$K+A{8CEc2JHDH9yjLlT6dr!);i5XphoF zIDPg4R7kK&$MtncK=jbAEoyFDI&qqoV9L1qM~1Psbst&>dL_7>goP65Q@Sr}n5K-U z(&;7CCvqn5O4&lz_ke8E{-GhI9$w0f#f?g^`{KUc*d;;B)TS29nLJHmjxuC2kF5QI zZxP*y=Z?L-UHHt(dvM=&&Xnu#r1t*mh0C?2_pJD`uFOxd(3)UWb}nho+R8*BX~D19 zXnX9C{Lh7t--I3gT`9hRtE?BI{jn24*+djy5}a(3atRA5P*^aAIID@Wn=yKzJyd%q zI@8k!rM}1`=Mmfc;2pn)-)A*8mr(;M?S!VzBEx7KUThK`-eYm0~tgOj6 z@8DJ36Xu63v(K6Y%$}m}&3xl0OU6S+eL=ZM<14tQw9Pr)sc(hi6oul`&&0RBZ|(_r z|C|%iDCu~=qzCIIZ@8oNkSmw&9ube3Xx@O%)ozr)xk!Y^I3_L$$Hcm$OFdgF@RNt{ z!?*6aO>OUr``eTz*{}Zkoj7scB11?0ydk1`u=9iZ8%faBRr!ab;WqumJo7y9xggVJ z(X*rA5mI!D#}U)9LSe^CFKb?n7ZKRAlb|PLC~hJuA`Fw|{-^-DbzH65x(s$W`&*JG z%Bk5QiDm)w-UE8ZrMvbujGPe2+J}BL03FPK>b-nNXQp5L#7~3$S)5M)^|ZzK#Qf8= zwfAw{b{Ws39eb7Pst4EkOci$rz!Psw$`S~^pF9BP3DSGplPuMCy$5|kbpx)Dz` z8*#(K_c{=5kKg#%tKW$Cz3i1ol&EvJ>u@XH`@^qDfc^p_=t9NuxmW)+-gxp^-~cjq zu1{rg2`o)c97IMwc>eUx* zG7Kld3xRWDuD|*8!+3D}3+Qd{z))Ww%A~}*qOQ&Xby9in>ox|V82Ew#RE^Ee$|&4$ z{{Y&bdLFfNXP|nDUI-hN<8H(b8JWML#4rg+VC>TVPA*Sphxhcz>sVl|ooJ)(NVJDr zE>1{kG^wL^Jh~dAGJjQ4(p_`x0$r|TR7y}2&AhOE?o}oFL<@CHU2i{%8fuZ3((}IB za;2VL(=mXY^hYZ06gRoLb5B7;?aqt`5=h%L6Xz5PyY>w2s;xkvP=TA!paM`T6NE|3P z@y8(B0dZ3z5yq{VKnv8vypSO?l5AT8W%f++uCXDdP0}sr#e*Y*NV9oHHL2B-@yg`%sndm#JC(5OD9>p;h0Y}mCOLlX4cFqLuvZO-JWnB2%zn#Tjo zgNqf>svX%XZlexL6mLvr2v7&e-H}vFL&K|EtpW_+xbeFn5;08UJS%Mx9fZ*@gFG z{l|W3o|!dSgY%NeBDOGpf~)-|yeAQdLBOkp{v*e~i;T20{9QGDF+O|jIm(s`*=*Tz za4)tV*ryif93*l4wko(Oz8+U}mNw19I_r$?vGVxRnP)TKKC^WbzHsYJ*s^zbSPmI? zjafD#f-^J>fQ%#a{E;25C_h1^~+>m(f%OGvr&YWJEcm%r->WbwprY{Bk94&2( z-$nTf;^ls_(#wi>9QvsCpi6*Ge8TLyDL5MUM+wlabAYdqhfHSHd`g(~Tomx15uQui zG;Pc}VvRBV08#+`NRwuie4ND}?Ou;hZ+TKm*1Oz0>xaPkCl;(wV7*||neW#f)TlRBaI`s;fpK>$bPHLhq@Uermc0B|uLC zfL<>FdU*uUeRoT1{yeblg}~bT5-HKW^4OErl^h+@*5BKQKG_~Uot?@W{GbE|A`|I^ zjFRnN9B^kbQ5Hb^BhLK0z(2goIk`7}{u=bjHu&JHUXKfxouo&GJGVUN7&;5obzZ_e zTISD7@52QI>X(g@&eAn51N}%feTQ76>dlWRX*wBwdvfynYEMDIp{?SkkdYNIABgc z0`9cXHKVK=T|$cp48$$d)^fius5XRDU^ooR|#2^E=8RLB1NWeSo;ou@8fo1llG>QwvKJrf7|pp z{(bWkjK9)>5=%l@Ufmc;vg`*%{FVLV@){Ofzj7PB%pE7|BFcXlHcn5SpWe}t=hoY+^&k-5bAU)`1g0Sb?>zoqu$B+T z1dm_4(Ra7|BggRS-h-$pXFk@Stz|MzcCLU$8!|Z{r!jSQKfwpBW(EPTQ-O>+E-)|?Ho4n& zyz$Zd@Y!2_%+62O-|0BE%Z@)4uQ>h`%xIbbiwqv!ya8+X?7$5xA4J#SfX{@S9L}15 z48HQ__bRCj&jas2L4Isr<9q@caI$b2gT*G`$csn?B9GJEFPYret-d`~$Mn2!|9*7! zbf+4L0J=U0-4noILBhn96v|4HWacH#XO=5epNLXVf&j?M1X8na94-ozgb@UxV*2E1 zS3Dv3xRaPG#8LJO^JRBOJ1)QDJLs#D(rXFKlfD6b{L@~ga@n0K&-j2bZ{ve%V$(+|tY*uRQQ?Cb7P`)jKtIIpRz#b|XoIv-xG zfN$eceVkx#E>Xu}af5A0yp?q|#;F*V;Wh76b(p>6v1B}q%ghx?WVv02rNxC>Bf;-p zyBPypcOh3PC9!9osLm`O%4=|L z93J2nGy-PHf?f`OffPvQfZX{L-dG%Gy6FDnKm3NW_i@4U6Y=3!UFn!iAY23pRofAO zcl}Q2HBZBYsx)OAv`C3L?X!+SIauE5w_~td0`yU|$;DtbUr8@7NYG8krcJD^!-AGc zk}1~e?b|!sv0h5YX-+#+O2{i^zowcaN&5r4wy1e<5drwRMmP7JT0H^f<_vJH+O9!) zktr`dlWOY}#-KeW=Z=+IU(g^u=Z|HhF{i-2tP_%?PnU}VsRA&C~FI7M_MPo2IET z-7fhes*~Z7Vc+|b@ap;>(FqBf$u@~vh_;KB2`>g;5cTQPJ?%5U?zW%euBV^yc{BVx z`95LZLVWU#e~T%NO#z*@Scd438y7jh2w>*`lX_64A_CyXhZ3~d1qC&)*t!-XGi;FG zOg=TEy|Tb^Ryg~;P+IYQA^))}fGv1<0_ZJOwQ|9w8b|B?Bmg=&#S_XZh_WMAA|%x2+gbA$FLFl>N=jYU~>OpKVI|nEt!4}95{gP-X0!EIdu-Y1n4P32&DV9y(UTK zne@rHc}zd+ZW4b9SvGJeZJQ%2LIAO3C5hupe4n@ev*Li8Y?)*{BTYNjk3?SL((qEz zdEeiD{C<4riTlt!*sngECPDeXzTyp7Jb79iATwU>EI{vWZO2egpDz=0RSk8hkYHR* z_7bG__w}QyrW$z(T)Up%jKM=~XgOsWilvl#NOA&$@W9(zR#&kGhTH%SwjKC(5_VGz z^Q@{g3FEM7Sb$CduXuw34H>vy+I4^B(P;{<)c!(Xvro$8Ps;a0^%*R+q zH*!@K5Tzv;Dlf)BS#gl(>i$s_83`Fl(0ey-L+Pw3N|WH9m!FGy5*SvHMf>XN^gI)f z0oO2TIu^JBOo{^SO3L0P+oTTz0|N@QTS>Z>ZimO=2(C^ZfpV|T?!VjQ?fs7*z85z> zbgyd9FTLjmseR9t~2c##dVV`j2RO3X>3Xu)mZ7hb*H-X9$9S6jc@M~3vA1wE(r zj*Q?13C4GJAI7|g@+nRFuP+)8?uL ztrM%7P?jq)=wvighhtM;cqemCyhxsgw<+Bl+I_;|~4D-=*qxvVs!K1)EI;;3|n%2N7TXD!f9jSS@!&?IeF#Q# zxw8{%F!ATZ0mF|}BVbN@%3Fi{PoNOXlcr@y%EJ-DRY;mf7Wb_&&7*aHG63B=r=D#c zr-LIbbnC_g<}PbdTrOVxE)LU87gI{o+p)H%U8&CplJLZwykxwyRC2{4A&`cI;Lnf$$J+g(zMPSW%n)EU0; zlLR9){=F+a@6tVDIFS2TfXEwX6NmP~~&q&t;PnYL)|L6erjP^UN2@BF& zin5LG0lqEv%f+56R{auL?ojI?ba(fpI-uHve#u)?3|x{?zEnH$dKf31s~7h&s&g

^@v~|Lq!>OIIra&A-0l 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 literal 188631 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

rn*FPhSDB$MXj7i9@& z4V2ekS>EuD^vSFjJ^b0V6H<;AS@l)mYSB#Zt?i|`7H3=xwEH5+?8{KAuflD=in0Ov z{hJv3Z{i%kjdR}TZXjT-%5SM6V5=_Rpdo0dA#A51Xs;n?t0o9uq$uzizT#DX!K3nw zSN5TR+Vh`mwWQ($vS}?S+yUhQ+`re6+5PA$&z(!8JEEl9qQu*xLUtM?eEx6D80C| z6?NEakWHtd9krtQp*xf9m$F}wwJSX5oPuLC(Bxh$Ns-g%z6xoGbZ#wQws`tBWdA$r zPS4L((Av*f>OhRfnd3qY;mW70GTvGi?p%?&?wXl2Iz6@c=HF58uhSp-!O4aBlA*qr zrSWXf-2fM20Jpz{8l$p(h3)?CAyd$Zw>p^SLn+V9YpyQ=`n|2Gw7t2kvo*f5_#3GQ zpZ$KuIyMZ#y$>Zg%tHZ*JA`M~wjpp| zcl9{_@*(KCANmI8ptMAGjPe@Wd<{tf5bkhI74h6R2a-PxG~O6wv?0`ZZHQ5JPdj~d zY;b%W>2sg?p!#QsXUB)JW~W+M43+Gt?|e)#xX(;;THW8)?3`jQ z*)s2d`@8r)1{-EzNBnpji)me!iL-b`a?$p!DQ;yk`;og4OuQr2I{^?6+h&^SeVhV_ z?-*pzSVN8UzPov0Cv&Mc?a6Czon3qP>>Byo;<=F*43tsNeJO!J^ze>57bKi4YJ0l6 z;XOfrZ6B?i))`$}e#*~sueULP`@UfFFGH=r46}iJkG2KM{VP9HA#-J3se3#xe-V^1KxGU(cPYg6A`Wy4O z>j_$^@asr@VW-I_eQ&qE6w&b2F3pz&wdVltd(Bly&#v<-KjSl15V2Cd>tSh+j7(x% zF~Q!EVbF((j`WL-VP^>-SZ7DX1g0e&)Ohv5-E)^sG&q@#DM`s?OXJZ;LcCvS(IsZAXF`xyYC36MU>0 zp6grKk401X0!%u_hOyK7A25dJUoUhHF=AS(H^JWj?wTCm7G;GwGvF@(l{?#$0TqcZsG4!6!eZtOYGFlFINv&v>E~5j z1mIrQ-i&t3c%~Gi4}T2*gnbsyF(gYvzW-h;t?5QRI8%0QE9z?sfTLVl0$ z_$RB zzn75gG&;AW+~1Yz{ubP4(9rfN0Ah#oY(5ulY(W*>(1ZquO(XR#<<8_%?u>s2cf_I@ z1z7LLR#V}VYG&W7^w3?78hhRJtxD3X=sg(L*?jK?H9Fh@gvKDf1;Blv7x2AlpcfFo zeTebAHT!Nw{8>LsoX!)x<|Dk?!^@#gH>~u(lX>#9{PQEKlGltiq?{}RvQxon(8U@A zSA~ZDZXj(cXr1BJr4J%J_Ieub^)uN^HQgIX0es&VV)<2=^;Z$rUw|)NbcM{71a+nO zRG;(8+~<+HOOm`z6u(6hzeRj`iy(G$*YoQn>AM0d&xDXRb|J;5!irCX^ksCj+hu94vLm2^61){bDQS~W7@sY5J{7Y~9-&`q&Y;~`=o0$dq z!XEYrRIP`2M{>2qaAi7%rY40VnU406A-|jH?2&zR#Z;3L>K~hzlYsWaAXW@#w#L$? z(~8m|FVQSW0GdWKGsdSP zKc~G3{8CNK&A#W$%3FVwPF)tF+zsE+3dyl$I3vH8D|d7P6#C_dah6);X{d59>uM|Q zXtj-x_~gc^7mntrcxNkjw(X9<9To3f>0`{MA$r&7Ft|;Bzb?&x4tKb4`j%jSjd<&? z5tNsX3~Y+GzEB)qIzk6bN4weq-&ylBh%GbT0vA+AV7JUT6NP&YtM7UlXF(O~(1OQU zvk&8~l$vtAV7ClhP&Kd4CYPn@WzLL;`YwNC(h}T_w^5BY2SQs0DEDm-VgZBZguZNdLlbUhhy4mm9wKrE?P(Zt(?oU>zIAL<1ECSf&L6yw9#Qe zXCT`F*+Df9^k#K59j%OfP?vl@z!saiM*h(tT0)2FHqA)@**1Iu;o!(QX?}OO@#y% z-&(t0S9ZtMQ`^oS-gfrTwljyeo;k>GDhG!m*{KrD6-g#?{F*O!TsuvAejR3kAu~>S z?bHR#6p6AAc-5W>7|D{f#R-7y@(&3z_k_suiheH6VL_B2U&pXO&#=&?F{fOej#xiT zQoLVcoL-Q(gpY%HOz16&&KI(e&l{@1-k991L^L9%q!>->U6Cb#uAQROt4mOM*wubJ z+=XPL5>#DW-Peso^}8W{OsoGRg@k?Xuv?~IxiN>3>cmm*@I7TO>+j3* zlB!8_YAttYDmF*DW%SS)A$|8ERn;0Js;>T2%96H%D1sDT&zQsEsoeTjQksfsfWI z4^5nd;!bCcnC8m1!2$GQP}kR8(@R6U2&%e4b-5H}eC4VyOzOVNeXCBe=K|5_q@ zXA80ivXjSF7(e{o8Qf%{bZF}S}&xnprObgpqR48(|Mg7b9deaz$jdvHfg zn0Ekuf$hdDEPoxKJa4$?a&ZJ9+3{>)NKkJhb9M^72+u9PX3kBwPNT}5Q8q?LTOm`1 zX!k1O@$5Sw?l%#<_(81s&)L2&%HvHdvrJ0UkOkFfE?dU+p`ZONck^q`%i*pr@ikQL zi0A(F+SW&xH$A_)%0LX~tODB~J=Dz5FKQL>8kFfRik9@p_O*G>t#eA^fVE0)e&^l6ZX&-@iY+j zHWWq>zt@+%$5l_*Q43r!1i~zcxn9I<4`QYpDcyyb=8O%;Ktr4rFZj%X-N)R7>nKPRd_l8j$}F}ho&ZbCB;3A^EvBp^P{Wb^=S9sV?AFc1zKe%gcN2Y_By~i^kFiq zE0(eZny_JbMeWdsDMfDiE4n)?dpbkwN^xX~$FaT;>&*Tpr0*iBxAj1_|1#r+o9r=c zs~tNo<;E12+C5OghXh2&Q5bgVd`G($@hW55%j z!zSitU;o>MRk**w8ra{h)43_nE$0^yE2d_Ysne9VBhi63#o4m0q;+B#wP*m} z2j{0-rbpop)o~_J?s+3He!!!r_R~b0CFRaFQXRc=?nrXDn;#)p5T~6Nuaz0Yv1OhI zB5?PxxaLg8;BE}z{sV;jQ*=Rfht%zll^(1mi$jz{nrm=bF&d~pv%yOnsCBrBi4Aw5 z5c8AqZjRM?aEB^$pq}0n)>i#}iu*^A<|ncPY@16)VJalT^jRzz@?F_7=<+GrnEUh;%=?{1`y*F0%)DbkmCo6ux&rP~9K#)hJGzL9 zHbjC#efHA6&PyTod!2R8Qf+*y3Thd>+~OTn+sNRaNE`45LbzMWtP3~UkYc+&+U%1M z699L-gCfpS`bRgT$lB5+xWl{D?gqv{Y*Y17FN%au%`i(@ZLsy z0O+k$MQqfCy^Kg1Zlp|iV!8_<%>||fBs#z}m{?n4locVuoDgPC451JMO~GeG^fMBs zC<;nH;D2&mQ1+o*N+`NR6`UuY<@q9#cekBA1O)q*6F+Y~4*AZfEzV=DOfZFsT_o+7 zg8I_C?p`E3xGZF=4#-ZR$V0O0N)j|*5R{(?>q#HA*T3veAwzpTWT}B2DX7+CBK_i{ z{n-wCFgoTmHQ6;fT-?d>X@dWgM8Ag#zV{P+?s7exRIkBVG8SyC-4x?vE|0Rp0Vh+e|zKAj)Dl;DV^ zv7;06DBNxgSJ$KY3T&;$W?nDB9b83l#u6~_%MjFyO%!Q^oVXlX3)_fa-n{-^`93)} z+cPr!vEqFo5#VU-ND~+f9zc3`$lIjudZx(vYmI$0B4 z=w9lBTnTq3cQr2m60J7YAUOBu|5KOdufl!tfBwvb`4iSdEx-6?a$!E6-Y!v{@>zn- z&)I&F{Vl`u)2Nj;y|~anJ6Sh5jMy@LS;IYP!?b7B$)6i9XazI<=2Q|rksxNr7UVLOGv({Dt z@1};CFyQF91b2=hLkKX2!C`@Lz%hYJ{w3Pcr7Ab4y{Wms8^ArHwdz7fz|JVEW9dE? z^#uSQK+&L)zoYC__Z7hdn0TLoV>;j%CS<%SvXyD7q`&h{q&ttT+NmI$sMZ>c;462f zVhIH7;NK}OPiD!M0dPlZ{0}{uKY)Axa{Q`0Jt$%m+b6A7(fKxc`*o_97#6gSQS;?v~QuIP3Y9=hgJm>iW8ynEe1lt;5WU zfu7$(oPj{*v)ABr(&Ts2f_!(?g?#rg*yE-rV5cr%A}{D`#G7Cbm`+G>A|=}Mg<1gC zL-XCoNB}85Z$myW15mp?^!YsW!5<=|@_hG$-+1p`=9joFmmCUPWri849gU){raP{j z*m~mUEkL%Tb$ma+)=OSwr*(IrqePyolt&tp`+yfy?WuVzfMlwLNlQdtD)WihI zat}PLEQ0-DiaO$J&8z%c3+u zUdxg+OyVKoXss-!WlPB_jxa~q#PMO+aWUpA1Uq&_LB^FD2D4N#xQkU$f_jK=D}2>02!xtQYI zdsYUY+&qoHZ~zv7y8;Gx*x??A!F^SP$*M@xRZ)~x(aSG;Tob^da$oMrY)f)T>uQEK zSnx93QPI^7K0Dm`U(V{tBcB=~+xu6-eJM2f&kM=F>lzy#{*R3KzYTZv$)CFIuQ|^- zKZ)2i;OjTB^j7{fSMWA{neI_D$^bt+k4Sic`1(m!9tQXBxWNvsmb{PRt&w-o6un4t zOL)!^b|%G|ZHTq_Jlf*RNbB!|t$q%$_`%oo8xO-TT=jTdb+)=`tpPW5NbDz^v*oXY zlrAnn2kuA?K%Rgj_yvNq7IjGB<|5IyLQyvSkyd-7Z7G#m9ZWiHh~7QS>}CzhlqP%_ zVY)Gr@~}8+XnbsNVuCd_#hRWRot;8c2gc^#nJUA!Ww6m4*o!J@AsfgN8%!myg}Wka zr-;u!w=#HuYfW`(h4cQ0%6)R2v3x1@uPAq<_i+^Q*)gjf91D8e_IsG%be~-Cw=V2% zN8$dcC}xe1E}VViruK1(_r@&ePf{J8XN5^+hC}5Jg8-!udQq}zEghpn?W04TBdiYA zVCTrNsJ{)rwc2iL7^}-?r_S#H5U<7WtS#WGBLom{rogNDLc~Rv6lY6Jav}o0$HDQW z0$Sp`Z(aOG@}Xpa%l8x&Vcl1I^kw%L$n7Jrsoazd#3UVqaOPD*Cy2g@tfA>>=KWz3z($cFvenYC$OoPZI+!2HAh>r1v<5`ZO!_ac1zttdRRzA$PKaALmA-qn(Z|P_1*>>+Kai9hJSE z$e7daI(mNtbHKSW4^NTO&P`xDDsp(R)P9j!Ik*CnlucP#WjxcVzj^dRoyLHvb7xbp{a zT*@62@9aKzJe%)pVz5<;54f_`@LYW|D1SD_TUU=VvtY1HEA(V&Fc>nUz6xAFE*Yvq zTV|jeZH45(9n?FO``gtx$lyF_9AW^(PtN~6BmQr|{qM^5cMJK&#etbgmAb62GCa2? z+UYdq)r}9%AiXqmi|A;*!CY2f(ok0%v%{dZa5uOT#F*jOn}GjTp_b0F&m+ck<&VW; z<_{ieY~oLG{Vv1n>kQAIvVHWM@^S~e$L40hO=8F7NKjA1PdR=Y<1KHM#gwr6MqqmT zG&(68@!7F;R5(u+8P5dRJ+^E*HjwsHBXA7UR6VobEHCj#7yT_1={wYutmm+o=yb+erRgZ(=?xepWzW?lqL#e&r-G z69WQ$3tPMc*7NH~@Q`JRS}%zzPk7ayKXS9O3#Gb;2WwG1f3eZpYpQU-O6!U%#R#Sd z1TSG7on9Rs0R?+p^ioRER7n4vbo{x4&mByw5V)hM zobOQX!|&ku+gOLkVa{L>I?z=U^M7@k)!&2rUomI6mHPsx7T>VHg~t5O){HTfYBRyx zw?Et0lUCh7Gl8o3$ptvFBY&hXVX!lX(dIy_zE&QyDbZ%d;wo-wa{~+^=-+I>q zH-0(gKZrF4R~m4=`61K$$1Go^x~%B_7NDRf=Vw8M^){a<2;PzG@Jp_LcyD9h^cWnP z3g8YWWzQmEkkg~sj6GDX&%CQW2C|zzHRe>D{-cxLHu9@0fp)PiRo$a3tYHTFHt$yM z-IF8jl<@p^>H-$ZJW5XmP2~APE#P8lq$M%jk{D(#=xHEqD8ncBkVot$@&4sq zH_j5SpWS`?qKNoynC~d_NJ!>^klf=vx~~M?bOkJxcHKO?^V+E&T@8=AoA8+_iWta{ zo?YL1>cCcH?9rCvfbT!=ym?NLqDasKvi&(x{^1^7N%a6XkMIz$@L;#_Ad?^;ZK{WP zu%AdxSfv>x$uJX1AqIXPn++rgmvoqq6VR*5{`Fe$k z4`O^C#`|6jb37a7bUohtSh(Y%FuNytkz$3hPm5wwn`(>OTS_|6h0}HzOM>oyq4ywI zhudCIj@K~zHdx6X4YV#B=t0kRP_3-NiMctToaYx8+a^aGJF33Q^jc=7a|?NB#jKdJ z#4+o+R#^e#fT{&|W@fLTO9RFGb380S_NOS^aXL@e>5GA1W=d|t@bu(gB+1`;EG{li z&(Dn^ekM4A=l|B7sO#>0=xDO@$<;O2j^i&M#a}*xyL7 z3W;rELYv&V`rPr+E81h0j++u}Bx}+#=^dGE4PoVlj#&xuRi$jty&b?^+1m-g-7+== z>6U?k5Zs>o65s!VEyJGiG>A>;Mmr{1OmM;i?EKqz?{CBXFKF}E+^eYzT+TcCt$B7Z zWas3lT}S!1ncn--J?@mnvXDu8b8sZn0)1|>eqtzburrd;=F(Ghr!t;5%>{dZ*(x5* z^uacdEz#VJgZrgp%v;}zHsxFZz`d1Fs_VZpy?@T~J(TNj(o$GE$^;;unx6$IbnK`A zr^;uEwik;+6B+I7(b$mVB_txS zn`D}Y=+x?>t3eK*Q;k>q>8|q9+L-P9ago=?Y?n_`9h6HF18R%6`5WOJmDbxS*mhL7 zch~y0SEtgN18YmJQte4(IRaS*n%We3l9>X+cjevIs>1e~1l6ZJN>2nm^odbc=wMP} zFq~kw+sBa4LvOc}CZClGzo|UG!7BkBNg>UbdvzrDYDtJFKNFO?C#?QL#9DQ?w*kMo z;?Aq5cU(UHwWIFWc3RsECHbtBg-sQBrS9y!dJ3`Kf8KHJl#rIhZev-3*2`VWPl&Sj z_nWCvf_%Ng!_dA)uW&Zau_uu4Xs4rZOr#%IrX$;7PkC-_{FUVWpX8n^xZ3(9#bIMk z3-jWO^I}U2jEj>W#CnT{I6O-ZyqDyEI>PyG8uhnC?_)784{{^KOB2#t>q&#W#x+^K&uuHXz~Q zpe1y(QtUF&FMoou9(5j85PwyK2^>~uBabtcz>#0#0NM4QBF@Zn1ny7Z<_6721d67U=U;@$KlB)znsiT#|5ZnJ@yY~UT;GJnN$M`SxJWXuz`{Q}#SLi?sb)dc7}UnD!RA|otarI;MX`KhKDSJSG|8H*s*n( zj&;j$W~hQfpR>0wA;B_J4I_i+io^D1dhN~hJXR2F+g8>$HOg_@(Ia=#@3K}%F4;aX zH9o-QzoQwy=o5}+=knY~xIvFP@0=&Ju@TxROQAe@wZG14U!9L~+)!I)TblEWqL^I~ zmT0%kot#j&zIq2*)!A&STzjS8=ovWTsku*x$b z=?5Z~D!aW5;H*1}{Prs+w~PL|$5vxEMg9}br=J;1?X*(dW2dp#TxHkYi^PYQMa-3X zjAaPg;zTtd+aK-L5WnYXwW0wAeeQ__#dt@evo&B^FEu`T3F}CffK{;n7vlFm`TdNl zn?qPeD$HR=vILTf@)Ju7;!5(POAAa&(iF0y)C&_&1lrt7@>4EOIud4oHOWW5JXyRX zLB1+IyR#8yI(EbH4(N_kXvHx58kzk-5Fk_B*ysEp!?&*J12buZ{G>`o4|>iw57XO* znXg~J#=@Oo^eQ+h|0i>>n=j3EU_ES-a%Q)bjrT*njyEQjqn9{A2v}W4LRej^eKViobFc zf%{=RAp50*IQcs`ClxgB7ta+5xs>+{;ZDXyAxpSXoR|RrE$?4+pjB5Z#dgnNfyPU^1vqL=iQt9mv^3XLfVQX@AEmI=kxu3(a8`t-xdDqTZacHW+h7y z#Qx5IN6!71#hmwr&&)}#MID=2p{~a=krQ*Xg@aw+7Cl{`5pbH45ZzrrIyW=(Gu&vM zc>TI=jF-l3f6?FgxTpR>7t@SYbha{SSKh--q~I0lzTzq#m6l=d7}m7Y9Xl7j28a|O zXVzxim&*$NSJBg-N@5O{yaY?HI{Rs7Eq7{Se0Bz)xt!PQ&{+EJ-CN6zm6cK-a;7H6 zpaTO<7=Utc1mb8Ex$W>AX?f(yOd-l+fk^1EgD2bhLy?t5N5b9LI@~()$TPmaYIp|A zL88FS6k_@m4tIQ{s<0De_xQ+PilP+r!ncq^!IC3|$fe#7XIBkPisv8~X+JO^Cf+5m zE+o775{J8}vCKjh50yLeYB~h`Rslbs)1jE)_g(~F;BO}9tiRFz)<#F|O?Pi^cGi<|(FYP<&fQ4S$Ly;x=g(|( zcHOsDeC#Cu?4H~c7uhIh*~q)HkM7Dow3i99m3d$T`ve470<6pUn#uUw0au0X-sU@O zv=ua!cDfqLKeU&93qA3EIp#KT}ipoRPc+fM^* zcKBI<|Ea8_uB_D!Su@S8RyV#6bPSApJ|A$DOgcu2ny{xON2jL>la84&*%?kzVP_o7 z6x7dMcC~qwo}5-vmS9s_a!&i90P%B@^Z#^$HW!b6;G|jnOGh zRxOOXm>+Y!EXBMwue7rnsk{zMt%HU8F!Kp$QG8+$e8~631Z)nnax2fq0?}-goaqn@ z16eRVH;eUg!t@hodfc<6;xC1fYcl*LA&w8ylS>r~$YWtyJH&(fHigGN?SKrms^19RHgKxR~+AN7Lz)W3E=j3;T%Ye*7PN%Ka_+8DUBiG{*RCPu^tYC`vZE=5VMV0S!tC%O z(sN2ddUa(5>GNOm2BcP(fHwws zxxk43-4&|hG?ya#++ypsB;v9287SN5Z5(X1jt@WTVE&`viClKDZVjn;sC)YLYp`Zs z&kdrDRv_LZdg?;EYC}3}TwAE-jfGlF(s^3y52epP%YV2f=fS$nfR*XKK%0Qy&GHnl z6{$X}(*4$D2Fa2_c9SFiQV{uFarC~@n4{&fCn&FumB$?_i&d#gP3&cLjE`VyoijD& z)>Qs!-b3(VI2Lm63!Q6QG?i`!dx48HQ0wC}$cF)-pGD$*dJ^|!@B}38#)vAIVynv! zhPbS?(fY>UKEAH9na6=SnJGL3$;ZycMyDdaV=cp=A=5TC{AI!8PYWK&fT~fA7!M#_E?>!6PjtTazQ6A>ZkcYZr2V3a^ZcJB$6+QPe zas+0}tRMxvo8D^)@#)*xrDAcL03X-Q=XV z*+pN$^UiknJAZXD{Ij!>qQh+kJ6(C3TMG9K6unJ%1lw#6wp9$aQ3$l&5?~>F-(1$m zRMzW`th=#{o55ChlkFBa6t17&Wp`WQfgRBGGNE?ce9YxyZ);d)jlD zbj*y)$@YkjJZ*2fTkGO!M>DUOr%0_AqMnW=SruiZ3Q7)*0$aq;E7RzdXS5PmdVxxI zw0=>NR(`BfW|Tf9?M7+h<@^}k$~4`obj$jJ(wIbF5KbR9D?cnf`ChlU>-r zT{1=UmSpk|YdtpqWs=8RfQjLjaHG}bCPe%^+|>wHssu|Fg2lCWt<`sVn);VybH}77 ze*CeV_sdE==5F(`<{3zK2={#ifN~(-0ol*)0hA}+Q6_laCO)>o#_Zv67jNb+K8Xo0 zVsaCb-SdMCf8APA!9ZI{Yh!9nWq2_utSB2CBTMr#nY5C|I#@g`3^7M-UoUbmd?5Vo`u>K%y*}O)?71xuy2e%H8(B(_w%2$9}x9`losYo z^IcrLV;iTKil0IHLd5rx*~#SoW;I&+C+Yq_mAr`TteyE8zD=iJ&k5#cYDWi?23i2z zgF9>PchG&>t32B(om;3t%j?wUYSxgh($kfyl22A79-}54qsD{dQA+#?YT}8C#IqGi z;9D1~Qj{u_&QKFhQsPfh;tiO&WKKKUIGqKWy=jc6Mo$L<`LBhMK)F-8nvF~{#~^B z6-mk!DS`FnKGfWQ_}gx>y|vEj`Z{Z^jdnLTK)&m2a)kPQo3EL?ndT<7)0;BWn$|^csyoG?cW|x?n1O5qv$2hi~{Y#M3tNuI>m{G)U?ZaF}jpgV>;<-NxW8N+TEry2Dhh{ z)5C=K?ENfcv#@!K!CjcIpbd2E=f-Uev^@Fh!OI3ZQup|WoIS(=x9Z?MFf}&|RME`b z?DQ-+c|AHa{jigHx*~B!nh&aS@_H*(h;kw2agHzGyYzZ>Z&{Qb!QTjXX#m-gawk}+ zz}DFo*Wfb0y5*jM+RF#Dp7uWm4}eS8)R*%5SC?NoOuV=s!Tl$~`5ytA*jYw32D%_?GyBe(t{y?qU~s zi8u`o^m*kDzsut^@%#G5B$7-2MdL7dTeQfA>f*bsk}nVmb`1BkXo(OHM13x|<3eT9 zhRlE;OJBtGG!D&701W~VKRGwsJU*Ps?Fj0u_U)hnxZi87aBikJHka8om0C3xTd<1n zG!z=v7u>GP1H`{kL(;6tQe$LXqh%=5QX$+cl2qyG)(u6B;em;{>B+g7iJ2MlV8@-h zymeWDU*A{@X3jd-k~*pzT86M{P*!5z0~(3m}JwT|-sT=4i$ z1(9DCM1J_pi4bf?2sZU(~b^N9cU|~auUE@ zDLNpxm6hAZ(o1{xCx5H;uKH`ejCT530NF0@r2j8p8=a`2`?1jg*u~z<+Me3U`%dSYH?vY^kGT>)3acI7`*crHoZEZ$tZ8Eqc*cLnrpq8 z#o9&jI;BaP#R*pn<8&%Aw5X|9OB3~~Nq6e<9h)f)W4!LkvDUF+)-b1Tu(x5TAA@@x zx1Yt6_`u^Iazy`_2AV>M?Z*QOSKu;HJ{IAg-oOBNz5gB6;X^PvQP5CxJCU z*iX2y4=niQjgv$dZNg()LX(HC0svgaoX{P);rCl<4WmOd$fcQn{qw-|WGbgMth?5;od)6FLUm{=w_^ji zmzXscn;^LB)sc0W5boEj0o*eY+|vNuwQER^JDK%@!HLTye zF;iXIDs-BQzN(5-%!{~E_A0ElETy@I+|G`wF5eU3w!`C&ytDoWpxiAq!CG&5eS_^y z1y2)sCq4PQdI0g;T#dHc+>$p@le>LY=H`Wsnr8vf*IqvIF<||<{ff3a@-7B4K|*jl_<^jZ@^XJ~Q2a zo7_yu&rK~Ugn5{bV0IUG2Gz zwe+6OhCvQX+NHs6%sH-Q*?XS*uJAJ0{=hDT4(BEcZF+2RI|O{*#5e%!%R}K@cx7@{Oi%#nbwJs6i%xv2KVN2+om$>#u9T@kqN87 zs6Jo6E>E{M_a+W^pxo0{85ses^g01o0N&-ZGeB_z@qU$-_CePDe-=IUYpHA=<%yy) z;&8`?ZveSX>um8!i~Hh^<_=>urTfm*cDBfwNnn(#NBXZdzV@lTUL>`kV|WnISuo#K z2G{8$Q&@Q_8k3C%sA4;)80b;k8R)k2r!eKt|19s(=lPGp@qM7&Ly#@Q$Xldw*wUVR zzf>A>3^w}}MIrmq0H)q?!9wLOI>&R6qiBiR{|fkx!$Uco4$J1UEhUjFGyPw z4zDSRYozD4H&S}qUp3J7g}ZIG(vr0WP&NXHUvF`Jt+~b)XMI^ybvaXwZTGF@gKf41 zS<44l$@^O<_?mC^HUr#ObTv|N)>m-6t#DTlTmxXKktiQbEv2>lzCIi2gWwK76#$f}Y^Fs{{Ds&Mm%_Bn zhMK(Us?^fLSLAFU(j^>rF|5OlQ$Y3{RCJo3lbBCVD#-szPwj-Q33AwrvhXpdlpNIj z3$}=y%2>6m_SUZ69xjL5&*4<|v_EUDv7u(#&`3_q0{x0~(`vFQGw%j1Q@1w9 zq_NPpjZz}$uO1()8y{j%4!2H@3`~rYSvBDW*@3yKktJkO1GA>TXP(+(j!pgGKyg>A zHWu zYfjQq0D1Fce0thDD`B69)heyi#A`5tISh4!VV3H8jZ1e{oSK~kS!zq8BKH$jBtQdAD^2Z zpM!U)8jP$}8Gb7>e61P^xl`ltTarvi&dd~+lMy`$INmhSbHdYhmBz`pRgZ5mRQa2o z{wBj~1dWp`buKt%#EJU3i?@Wt5kEDx$U@x+c#{)N6T_UTF+SEU63y6a8RPBB3Ezp< znkRQLxFcIeqC%C6M7k8*2cX-&cuV&_Vas2P@;$=osl7`@-LVZ-!3f^lePmd~_f|gF zm7GYpt#lyBCAppkAOd^7qt!kdXJl?}#fa<OHP--S2TWgo zY_FoJrh>19jHijLhw&B<w_byp$6@iT9Wjb{%Y33Y!_RZ=_9jqO0P zusd1)AR3&F55d7y54EKci0;JvT)^~aX~|+e9W!HdpkN2zN-7{H7v`lD6`pf2`_m2O z7r9wkl;W()@@zD{ItP#)_H?W%sH>*2S-pL|91e%eL&Q$!!$WNUDej)pPdw#lNLtK zh$_ydcD7=(r536@l%U{frauLIC)~TWFWRT9ryVuLgh6;X)D3w%;FrRmoEV>;0el~w zngnYawE(+RnIh8WZ{w;QP~QC=KzU@&yq)Z^93CZhS(jm5$%GBaVAV*1J0a)}!Q(c; zNo$pz)~QGzds2LUO9OxEx79a^dj9@0-~D&?_3P-ef)BON6V;9`1HxPtko^b&pj`P7 zjCbt z@b%OW-|&5!pGtU}5bCemyO-^o}}q>o)&i;Bi=} zvWYh(J?rBSACUcji}nPTFxX}gNe#IgDdOwmBHF%s=?Pl_sujPpd&w%s-qhxC zXkD#kO$`N1dQKHJqr4=otT?5(0AW2ptpqykfeA17+`09G$xWjNey@u1$n>gw)YGxJ zzJ}i1*wWRBVSR9LkjLW<4-fb72cLH}B=)ci2HFca9Z5ZG|Az9AR{DeXYS$);YYWx8 zgC5vhS1jnGj&i61PMM&;duF~LRxh8ITSt#7%8AI&dRjtGt*NZ+Zo~fY3!4an3s^yC z(~XR0GWX3t_BOhm9TQzwDQZ!Gbs1~obHI%nZ+vWIazZdQF)}$mHU(?(lKPr&FtR>K z^Lty&ctLiJm<5k|W?{AX;tK{#((rMZ@5j3zdlj79(jb_c6t;Z%4+AoS$%)XytbJa# z%Qa6C)s7R@jzQN2$o6X(-wzSgjuFBAf#+>Pm<8VZ?~yeO#sK9G4R>4EA{gp-Al_j+ zDPr_pME1Lc7(5NRI>AG^BKb*2ZAo`)dIRGbCI1Mt||Hy>WqH*|zvi&!~{WT&x^4S5$dnU&sI+$M< zKUY?$RxP7s7pd3fzdKa$Dm#O>vwSa7^U1)CvaqNW3?X^0uS}$fpO$rvr}hfdGU79`B|I6iVI5hGHO^V_e{wNC8}6CTs)mAHFx)Z1 zmx6m{bKS|u-W!art<}4{_Vy)gZTGi4{Ltf5UpVs`HX-mZmUA_fy$4-&83%xMZCUG^ z0P8a58k(L%0N-|jHa(kL{t9nmp>NjqYRlv{revR6Ooy zRg7}!V}rC&PSqG^@)wCu>Ez4|t*t4QPI**F0{T6fL7{Xu*Kzt;(ndVK(OT8tX;+l` zmk8%iyp8_(_}&9rVQF_OZt3vwphes85NB+3WMTp$eqtOT9=!Q^Q&SrBjL*pr-@&_y zVdFl$<~c4t*;sUvCOUe-lc#p?yXaha7Vv;VruVe>jPPb&F9IEDVu8lmc$5aY#sv?@5q)RM%ojg?d|{; zvSqNofyk4Ix%*D+y-%|qT&5=G_qFA=vR+gcKcf^zQwpC`i{dIuQt9QnOd3$=2=1^I zb0&itk(=?Ot>KzWN93<7X$H6^SCmp=T81^>>kT#zLp8<-?pR<3BYQ2%x#Br@uCR#V zH*cb1Oc(og3-8$Y+_vFC-sHH%U!Zsq)70$D%-q~>>_qe1A0i1qyTEf7C-|EYKZ_h2 z#P>POgaPmcn*CW%{YRMr8%ZI?b$MOj{_-{KSOC}DEL>QwW)=rJI++`o9FORz`MNlI zZC0QvBO|_-J&HC?p=_U9Qnq7;J8sLMF9oyng-ciw)95T7bw}H((*nHJjKqw+CE-3f zNgLzF4m5?f(Y+hW{2M9ZZ8dSdjaX);MZl9u@{>vS1;-VS?O|F*4DO;adrJ-LSiA$l z5=BhC(w;lsG7Kqqp2%=-8sgkZeDY@(<6|NB0M>zi?;PP{HLIO~`vM8L=d{)dl5x_LbJpA8VY0(cXP1{5oR$}2 zBO7G1!^eE5ozBj?27v6bx!H;N6+uxKj*ik#qZ-Dx-yBu9lJ!Ks}zv%COL7RJ+NQtdi$rR zJ}>I4nA~3c0)~v*Cf;Dj$k4!;V0dzZH!;Sa7$2UP7@Qho4ELMW7pzM6!)Q(v8&WTg z^+lh2Vh25U^J{NesBCvL{x;Cz;?n>JQha&?y}GZHJ1!l2mwJGUO$M_{CpvVS)y;Qr zog-+Rz~D{*a92G>Q~_{53_HWvX%PL4h#1*LaDOCRm~w}ndLGZ}fc3;_3_%%r`x1~QsTn>Ef| z^mKTfmsyUDQfX?0^U5)}w+Z1czMBxe|Y#A&8IU}SzhIl}5_81S4ePxCpG0n@Sp|~5| zWde@BevQ@~3^+ovgL9Yz3u~{I&v^j!#fRDV6Z)FqE9Y>Xj-)yaxsNYp%goIi@i6Z= zH$93$99lHfSSgcWMl2RNJ&Bzxn4!9u=RPw{ALC|>bR`Y7`ZSaUu&JS~^r!8$ScBgt z!O;46j}5Q9*2jCj`^feUp8LXeYZKwj-E?%VC2b8;D(Wnf>eIzyieS<4s zIvb_d(^~o5PGq|y+0I9@eHi-^Gu*+E)LMTo%5Q_7@+UWyKEA00k2lV5^Dy10d~~g_ zwL0v&_zPZy@=SrmCD@gX-wytV6*r z7y)D-8yf>;pPZbWo|>MWnT7B2pMP!`}`54;DVpAMQ?Vsg12FeOXx&Q(5w&qByn^sC7zCbtS+$ zSfuL8+?pyN+cW4?^#C`**&hjK_WWpT0G@=lI~&f)q5#?6#HwgwiQo=xnf4ZjxN=8D zs#24fM4tN_5|C1EjQA_Tx7a)`cYJhKEb=%{TPe81t#h=nFf2go{{rzMBYxhKS=xM` zMIC^Jp8L=9&O6{cB0Gk7fY(=j?8B6}jXA*|Wd;1DFv_?t-@BzU3f%K{Hw3pc?y}3y zRwk{>3IyN%vFxQoV+nV996%o}Ota65lslsQ5`3S5wmYm)6|>zj{XUA)jbc8`G-l37 zdG4ZS?otat9KwCLGkv)8Nq4<(V>wt6t#oWacJm1DYg>t{cL>x`nSiJYGD#`6a^p4q?Q)V}q{_pUqoZ#cAi|2Jp= z9@cXG8w~v)1L*Jn2E1~sp{l%#zKp%LtnEz&XM^q5aCVY{zoo1{>@&07&tjLO-frt# z2ZNl#GLquSBxuMW*$&H#sAX7GQ8=Cy4Y6xsQQO#69q`i#K7r`2xo@dYln{j+yj%#T z9R|p4EajIWpZQrmrk8NW1aot9Gcz;O)6-K^Q`6{yVu!Hfi|Dp^bZ+)>XRT91Npeq9 ztzf8UdZHareqxwEJ3TZvJvcWtEj?2W`qqNU@q8BZhkK^mOf_9oW1p85#8QgmD@&8B z%E|RL)b?idCl|Zn7{vP)fO`(`=S*m1D7|{HJpsM9@74m(DqY_F2NMGlZL`A>6e9+)ohz-!a(^5D&gw}i zY9Q8g5!vCfx-vXBP07oC^L3>Uub$XzYZ#K1LT1uSQBsOw3<*Q-Z)_YkAC-|Z+{dv! zRjli;04^AU#IX+U++S3c?(??$#?8zxFQd4vQ9KGMsobS^Q^Cq>XJ2~}vR3=ThZ?6p zR6qRzT8fT(n{+QL+T7aaZmf9EP|-<$n}e>Rt@d`So7>H`6!flb)m4_$I4g7I_{Pge zH(flk<<_Nb_P4j*y)9#XL&oa5g5&LN=GPT0wYK_M$oZMe_?RjBnC`UG*|-rR6L&_&kx8ep7^vFg3lm(NP`W6BHTD@v29%F-DX*|k+g?ApqXW|nmPnadi& z@m<5|O>3_G@rm~b?s^J-=9(!{RefFfMk_)*dx+b_aDBh1MN2c_X~Cz_)iq&VD75Znzf0=UDzj2PK(E8%31GVm{M+h{AQJf%C4DLX=6EWqE;4W0|);QLYEki`IebsZ9!v!zw8j9mv znThqZSM>6@s#35LXl1E1N;XvMv>ar`phbILoU3x*RsPnc_pct^YiH=2_9~rD!8TNd zQOJ6jhGaJY*_e!DX4wO4h7=^?i?PAN`yc&!yz67ZMEhCAV0?!*|- zpubCb6DE-@Gbx0-NW}xh|NLLfW?l60i*PE_teA2aC;OZb{kTpS)9lkowofmzpyAG$ zo~#=k%;R<>_cy)hX?WIM7uVaE+Sgpg@9UfZ*E)PpbpmB(Cg!F^WS@}mXWk6=snOX* z;665QxGyXm8eSC0od~jlX5G!JW&djfS?znQ7*i98Gcj16+Kz69y1;ezS_Ip{czP@L6 zJizG%t&}}DfJY%E`MxODp))hWE9_{}cp0`<`}~KhCqGm>^#M4lo>XwS{qeOETdi*> zgxG8eww4dFk`J)NLhg#*W{Pe`+W_(J>M7dmD%k04v%UphFK@2{$iCTJL)P;8j=OsD zx|g@Pn_#VGbQn|lv_FMPFNMV1QH8tyD zLq+TO@avzkPlv=~dT!1mJ?m?LqgykLZAsCJK#HnJF;bjGnK=veBd1iG5sTvdPG)%K+&?x@t@&C3p`0&~Z z87(E9aNh`WT7De^rXeNZju$aenpjA@ODcB}+`FYKRKXI#eTlaghb=ajLkM?n+XxS+ zJ%GpG%Ufx<=#; z+G>7%YoMi!pP7uWsjT-MSx*>kgqayGyBThAGuq{TXPd6_pYIypcoGa-tCO=6kZi{d8RWK;X%%_ZG$7lt z@zYSYBSQv|9g|l=8*~Xzh9@nrFwIMDV=Kpp!^-=6w_ukQ&)Wna{gpvxrlqMhLw&+2 z(re_OH6ePwhd1b(nee%p7U9|vqRLUC+EJp$al-YJn@!c7Q(~T! zkfAd}El#K?1==&cnwrh5%BiClG%`z?>S^6=03X6I21i(!F2p@=psKIaK0o>Ip-wBD zZv7?1Ar!`GTHx)7Sc6G``0im|uYlh_!tWjC^$GYRljGc}3GJHf&&Us9T`O!u*H7Z$t<%CIlG4@h@Nno5270yytF?8Sc=QiLl0V?vJfkKXdwPMyM(^U6kt*|7^S7)psr({LDbj=EcJ-Mg>aCF!Jka z3hS%E4PP;YdmX0SA>7$4D(1|zECF{(TL$kvfZH-lDR-{qP!3M#$Pf^P|1oI^82Bb6 zmAlBI8Aq(2nVbE?<1|12-!F5&NOXmig1fja^9IC2pbOzXJ%^n`Esv*VP*euB*FegB z-eI4Wg8Lf~FCx46iHPr_4b_FCvBgJrh;r`YhzA#B`WPo;qzkM7c7#l~f&iXP~_ZWL^8JnB1C9eexL;|@gXRIn`tg_8kXS=!9Hai_T z@UPr609OK_-xY6P-J!2?F2pS$Iqp?%c0wL5+hP2%yaYF7=#`jk2cJy171qpx`>368 z@H8wl0eWh%4~O7y*xeEq_j7@AzmoV6mfhdeCV1!)z4Zuw1^`3vKCnEQ85P^gTs+CJ z;A2!j*yl^l?Hb{M^SPgYu5NF!i;Macko_tk`wA-7Q@?fY2A{a;>1Vzm zw>Q=g_G9C=v5Nsv@$JJyJpk?_ydH2eJjfjz?j9d`+Esh7^yT~Mexib=WyxZDs>vR( z=JSyi9A@of07rZ{X%QjiQA07jBcxmudPU8P!s47?#T>lI-?>- zi1l116zpkLWg*$g&|bfB9;`ifhWFDGa;huIOd6R9;SObceGR7Ep*UcPJa-nQ32!eW zibBGBK`FSS0jl%8Ww7=GOTit_s=~kj=3y=nZ9s`GG9dOhKXABD3B^06+(l#;t%?5u z;(rk$es%%kMcb!KAs(msJl{pKeQpYMO2W=3XTf)0W0c3j?Ba&Z?@;cui+Jwy;#~~u z;^JL;%-&)Zs`N1~rrdp4yw1fpRIxqP zVZisYu2#K-s82M{thsb(&BcRjE*)Hb@!%G7&227*yFE-5ea!GKkIq<0s*bE3#JY^t zby*8d8B-0}JL)nLqZ9fDHg zUy?Et$fTsjWjpS+1RxsOU04Zf{afdIU#|y6Y0W^sWrw| zuE&HuDkMEC&5J27jH4DORh6gGDVf!kIkmKWATS$iOPd?005n~&hcFO{=r(@<)w|=Y z7yhuEvYxi+TI%IjVISPP{bi7yPU_RD{!W&V{*ADdm)p#TMorH!uZKU>%^&O?;d4g? zZK#2;Xp*1!{7a%}giewhEb)KlK!~s;2AdN7jKKAe;HHgF+y<)L39Y+Yux~ItzX`j2 zx$3}cq^|)aJAymmz9GOm9LXDK0?ipfcBI@P*%xf6u86Y#J}1(qvG{2-{Z(C6Qguag zH8r`qB85?rQC*phEcKlEW7YFw@^Lq7IqggDs~q2BXB3j1l3YA5bHv+FDl_hWXHaod@XclFdt?X+djqMJ}2Bw zofTmny#BX4GvbDfXe~5k7FMX@y=4|SGuVb|#z?0}U5QHeGq4N`6EIt*ad;5jhXP)h z0=N@{&4|Jc)xPnO+%}eW+Ou0JQAM3ii%w$t)e6-<6oLdNKwlj9zq|6rnL|EyHGSQ9 z+Fbxc?mdEGAm7B*I@W=pQ!uhL+<6_tu%@Y^r`;s^*(aK(SD)X%`oe)#;COz&%pElu z-OF2UUEJknyxY%mm#_IwFPL!O?rN;)YzQ588*O=un{szFWQVFDc+J8eZ5LwQ|gnG1(^dzjpfjfy2@ipL)@vSYG6AGO|v`=~&+H$mB60q&qX zTd?udu!Z_y9~Nz7^CaAuC6a2;lfk&3-r8_DJ_KoYM4|u_5!qpflSj62H}8duDTDOJ z_dfmz%1Zp)NS$zTAK~&L1w)ms#_9x>V??#%i0r4J6Mgdx(eM(G!{0^wh7@MLfSH-1 zLzkLtT7sCy&!H#Nm#PYS-rewJ=JC6eK=qUNjkP z;fy*sGbipI>;drDnh*{Yw;AT4!?$-3z_*DA+{kjV5aQbcTf~Q!B5jUGVz4#?*fGj} z59x78M|E^pZDL(jT5VM-vm&LYGNqcDA&kM~;0}8g671y{e4Rd2KehbQfn5eFL77Qe z^a`}IT8X_F=G^NT`C!#T*)HsqJ_2)0pjOg|M5n=2=R-m^*1>)bF)h=Ou}P= zecqN?VlCEtK%(2sQp#PlmAaH{U%H5CLZsYXn1xE|kxFTgZ<1sAlK}1$O#trT4ipJ? z%y5TMNJc)Or+s1am86Fk6T&XWKU7VQjHoUJT8s<3x(k{9E1o+%=@}g8TQ}+c;kv=eW`8L_7NVXr`Bs7)R-N0o^4w40xJqfC ztcixasoF-RLz^xfk-2hQ=GqCgPRXgA0c*?63tM%S<@K*B7^^FqU01Zw+Nys|M&;BN z{i}+uMzZ!gGB&riIO=S((Asq2@J>@ro7hNfkSP{_OfAY!m)J)|RZBv*9g@8j-$%s) z$grqH7%~y>+%hlQVW1G(Z^SJN)(|(hx5a~1wlgF6qeusWmp)LM!~n!5M0O$o5Q=IL zqEV%Ltjkwo3E;i}sd(>~twH!X+;`zOpo>syu@M;Z6o5YN#z@*O#`jD*$K!+oPfJMXEO;!{^Thkulv3I?SA1c@Z$8mbzdJeX_(kQF_{?)XQLdg9Y(3Y#WqwIc<$J4Dzs%p6{^BX zOjr>UtWbrwP#jU+Tr-aYK3x3iiACA?A3tzg<_&NcN4)3+@O@@>_K%HAVh!RKHQc4h zE)waY^Le0)o#T#^@C4soSOVxviFfg?VN2RFvx~s}H?<#_nuoh%b^h_>aIn-#FEHgU zf;-ggA(%gN2f&@u+j==6{9I!A>7>w8Nf9R#!>(pOC3iK!(b$vY14~6^#sng`1I^wt z$Vp>XsYm$kGQVN?JPe3;%z0nP_b%)x;8(V?U85g+ynNC&JhYT8gP#u%wGR(bdOCn| zhj3TgCt3jRGA1hWMylKFbQCPL6wEZY=v|T1xg@J`R`%M-&C16%T|B(;+<}d!_iZ@w zM{_ZY*=+e#K z`ixST4xO2bTnKk&QDc2cD~rxl^slz+bci+nLBNPP_vV7i6_qoR^oj_wizAApYD#KTZ zUD1?dPjPlH#Jc1Xdx1FFQzYx8(!50He=B-oRF}sX>TBWj<+E!uYO6Bps^F1XnTdS= zskBUw^stUP6v}FtuTaH+Y5kERPfvs`aSndz5YeiX_ zpCz1`r3L#U;{CO>Y!@l_-=*2#Y|bpSWu)LPI+^c4uviq5KLewXDg5>neupKEv^PHJ z$M~R28BZkOjF?O)lNOmaOu?FkaDE`P|mZDE%MH_TQ8QrjBA0B^{oK?n_mhGYiDPPdnc9EL+1v%n6 zEj_ZOrhagM$?3I7dPXo-hRQt*OXHztv)E8KtetEE9NR`r_kKIe@6(be=FKI7x#^L~ zvDnhQFO4t*xhP$wjfRX*mA;Pu8M}6((O*J9hwJ<~GG(r9NL01c>r02opwD7#l z$Au(Ma;kFjLz$;8KO_YDmynVf<+-)cNhoTpD`{?^v^7?I87YW_|$2UqL=V(#Ju}Obh>1d zGP&Fbnl)K;USGpNPf2rqW*sA=mYz{d%dDl5fI=7JbuJU0JTD<1_pp(@t^B_F$!o#x zA*7T{3BgVihdaKZx)|Jrbxd_jDR+hh+~+-aIP`n~hdbW#7qxN(pUSDZxnKS_b7l$n zZt8-x`$DFR#5*MW6!d2VQ{ZG`YCOyIJ6*hr>Uk|n0J0t z9cnqi?-5q0_Kl9rcXA&a=^EwtkB@-&^cQE362JX3;n?@Y1OJpiu`j)f0{h8`Wg%DW@fxiWIc>z z-3?`2^kK_iM_n0vY=8BZEb8|Q`@aMI?w~7s{ruYFKko5&xS#m)C7Ps>K+YAbS}M+> zl%b(W6?r1r4x|>cN3kt0Mo)V!UbWQ661E8wDk;>V3DBHBh_~NEW#Y9%{noVw^4THB zN?)dRv#~2dSzm{7%ConR{{a3@h!+pQZNNu1X!?&mB>PKe;;Vaba$B+|F~ggf>GM{$ z-`hF&my-kKb3=DpX}^7bKOE((^b>$PknM!afb5628mn0)L@8io*E~rCWWRobc=kua ziSG#~e%PvW8zNg`_9Nxz9`U1BtcyONi8CJ;Lk2<`F2 z=GafOLRL1$5{g5JS-!+X*H6=eS_TK2x&3_te(U(KDXVZ*b^syMhX8)+bZ|r~9ojg! z^iKAeA7L*J`V>jfc9|k260Pk`3|iT%f_ zc&xkR!cx_R`h8xlzn1pg=M(M-^#6@`U)+^*5AODeYf%Za=i$mG*78eHrWDtKegX67D84?nW|h zFbXN_q$dko{%XSk)>c|FmYOi+en&&bSWVVYRnb-(P;Z0Mq0b!joL@eU$w~vV{Z&pD zknPFHZO?!ss!OwRL#9%^Y6)}OX`M*6_u~7giwr7TV9Qi-y8YSI9}1&?D2ldX7BC0< zghP%v`2+3hbvY{{?Uw7D`~0TzAv=>zCTavfBWTFPIz!E#@fsbZ5VZ)q7f}2^zL2mlPg6znm+-IeEFDXSYChPC1m;%vV9@z(-GX0+%fZA zYDuX0#ah`cOZECVJK!kwRZ?G5|Hv@4t%=NH<}_53>KUXu2B}s;gM*dLsE95mYlQi1 zH&I`sdHOGww|vv%Gw4**U4SNZY@n{sSzyaxpvU08sB#zCGQW~@N5eYAa_-P`hs_=Y zeBRU_Y_Bi+5W-zZc2Pn``~>GaT(HOgyg7pv-%m{fzGG`@disxVApFZO*e)wpuk^b% zRDac$c@xTigCX+<nC&ftT?_&M=Q{&**&*l$e zo2Ufba5qH*vh&M(Td%}NT!{}qpAhkn%&=Xgu)h~X#&$Kd3WmhR`@muws*j6v54%|b zy(601(Tk7i81BX4KEjz8ZRZZC+gf~j;=sFm|GxhA<&VxCBz*HFVgFZ9MehIVmWNYC zTk|5Gi~!$CZ6ATkoi#M@yoU0Xr{xCS%d3PNsvC4K?Qk>Reb0Eiw;7b`u75mifAKInBLx~V$Zb!`&rK;R zNGpMcOcvTlh4DwUk18I2gnga5T9F~sC2q)I)Dup$nI}7Qu>WO8-LcZxT{)q5>xzr| zJL<8i7s9Y0_{C|k$g9g9t}X~Un;fxP>kRS8H?YZ+;RV=CJkb?-2wp(5FC+N_vcE$P zd>4?t@WDIjKFf5@LZ2OJ_H{Ru&iLK^+QD$EvBq2K$59i+V{mTVnZ1OQ-xDt#T4t-U zEXaiT$aY<%qgGsmYg%joDFur65^_vgVH~A6v8pt_s>G@=`MZ<{JLCO~ij%S%Ys=f% zl_(U%?8k3Y7H>0)wi8(R0Fyzm0N=rn>B6mFPm5N48Co{Oqs$GLPV$-T)@Old5JGZ}y)89VC?G_A^20Ja9O4k*H5p(tEHf+E|*o4(@;aIr{~nsVRBK(XzUnYUa0oK z>q`smHP=u7!}hlQt0yl?^T5j_IWF;~l;@6e?liFtRRs5j1#oX-Rkg6+RG})V+=2QT znVN)Yl>dznBlVEpP!-qgq7(70)G=|mW3nCjGZ@^#GgH5zHt+X7usHlew|(m8-xQU3 zgDvx>+9k!~SwZP;MQ|S$wG0z>g#?S=Hax`S^hPmC&qeud)W5Rk(m??C^|vkpQY+|P z+i_QKhqu`dAM@>ArrSL3DBLqrbT-)Ppu5HPmYk)QoT<8;vFc`ntFlI_+w66>*=TP( zy?@i?W2gM?My4gkWTv6frg+to(2yz3rj}u{UDzWGy6sT5x1;zYCfoaa8o2#Za2M<5 z0TIvS_LO!(3&AvP*0N3mVA@3E1zL(* z18SZcId)gl{ZlzlpVe2CwzI{ZnFZU$qHrg61mb-VEq*eBJE@jJ9b+-Q7-bgjrlh)ZL8rFd|As84Rt8HoeQC<0i@BjYM$-VMg7r(IA zleg4bb@?dq%k6}3z99VjuY?2t0^C{k?VrE4(Ic2$dB;NaOq6dbtGbyth)HYvV4LanjXD>%-qBcpbkDlOKdjyN&AJ2Mtv|kJ^Tz_6LXzX2r$gBeG&|n?5!It+logXI z%0=-8nRZfjNVH2 zd^;})ko~>F2Y~D=$|6=~-Cw4wL^!|i9p%Gcn%~@Jq6ROS*Nzge944Ook#PJwa9n0^ zX}Pb_7a1Xk$WQ-~9=bC6?y|?WDGj|C|tjtT)ZRY4hN{#3oBGb zMNF{kFRD-#wfseVhr^g~zPDp(B{rwGZ)A94W@Z48KySYWbux$TpZ`CxT?qFnNoVE_ zaL1W0Jc?|0jPKZ*?{V`#_$gisS;}BH?el2#3EW9{rZE{~zG|2jiQt zInQ+!9NDCF7zpzXCx3#_KlnX>{$`~k3Wll*`qws{KeYDXx9gAX`Tm}%)wA&D z>8US}Y>&?+iB~OQ@ki*%2)jQb*%FL z^CPcRq?8SG3TI5uYg^XPK@}0#+n=EO)rM zO#LL`(7y;udzU%id^^eG-2zDVl_lXT%O9>J1-zwqk$7SMd)JQb23Nb=mjT>mbuMqx zIP=!Y@0TebddphvqZh89WrbcWd3C1n#lEb^pT+wvdt~!=q}}>go?oT}9ZCyVNsltf zc9HqvW4s~-zN2)qn3fcAf z>^eyH1|}rCB-XPT6%X?>HN$;(m|kCVuehA=Z=j`&cK04*q92`0s8-=a-S5YW!?~iT-25U zM}bhe1HOyk{`!w@7cBL_vNSVN7L6q9u@mtkrU}$U?9F^%yp##==Pv9Twh-=1#M>q6 zm==vfPEWpUt}!pqFfB{pn-IJj2=EY#Pch{#gge#-Z)KFj`_Em#@~bN??QJ`g2y^a7 zlf$-V27`ZPm7mEfXYG|vH&=V;ul6|a30Va3<-CddA`Irk6wmXBtfQ>Q6V7Lv2ySo=ki(7@3LJOs(rS9(TP2*|YJ!z`@ zpL5PfeeHa?sEMahBc_vC8ZN4NCo10oE5qtb&sF64m*x88CvQuQTpE30a=_u?KHJ{g zx#HDri~qA>{&TD6Jhx){vx|OxcF~jwp+^|WvEI-YV8OnB{I$_Le|?Efxi@Ai{>|0$whQ~ zS4Gr)TO|X0EaftbnT^?uy36&Yi%S#UE4uVlLC})StJ4auJ)U_Mz}-FdxJTkqxA?=Z zm-h7AIFtJ8x70DisB^{+IJ^@0`hf)jgNp+PrJZzH{tIQ&Hy$&`0=WNc{x6jA|9yVW zgwHlE`M_tR>$z=jr}_O{5b;@h(BC5uy>adM;`H!oi5Gx#cM00+a%In;Xzyp^P7F=( z{Vp+ZVRGoE)CjM%=rdV~fbXz(3M`1M%0vZ`3H0j2RXLxgo?Bj)m?>c0G~6NIE7j7S zvf3A-j`(E77qx49HSZu2TI9o+iInV9)GPa_!P_1>w{29cKj1su@e5v9xwu}6cHT<_ zEV+cMkr$~&^DC2pg(q4U^owyQW4B(PS&@(~=YbczlF!X&(*fCY81e&x1nDD{2W!ocyGF#A$3SyIHN9SF_yS*!9^Nn^eIqqX3$nF4llI%K^mbnX0 zomgmXwWVm<$?1CCGBe%aenbC$9p_HWP`&;prt9*(_Zg}o^okXE(aUqMeHL+|t8*vQ zGE~I(hc51ZJp3StW3;avvG(`)tC=!6(-bIqyE|P-@1SLw(FZOQr>=v^3f2F z;Y0g>{6@cbU#7hO3gz3OlrP?){O~z-_=k^t{{Bmzt0-$wE7#?dC`*5#ESdPkhS?MR zz2X}x0pB^+kxWFQhFE3gXh5i#DmhcFsF#aNMZDZ5W)i*TT1}~6R`Q|vhz%irb50(Z zcG7d4&yJB^n?KvTcBt3JS9dK3^bd-O42h2+vhBpKrD&9GC-$QP+n!!tk_l`(S^fx{ z4S+9RF^4FUfu#%9A{lbZ5?yH>E2@;0%LOZ{(|#(tnk-~CD9HhPwvCf*vgl7?~ZqEGz2mte}U|P66lco_N$f&c{90dqC)J7q4a1 zsiVLdPru2-2W*__an0+YlAysEry$%X4|kb5^3j<;44U{YW$dv2GsZmWv-Z2pkdgUU zzRL?6lI;6vXUo8d?L1#SUB4c*o6>b`-oKL1KQF(m2inB?FYsh8HJ zN9;?#7Lbz?UYrZJs@7Iy(Q0!U4F%xO=Q4LyWW1Z|f3%^HXlZht0YHfFknD|WS%XI5 ztDwJ_8L;8>1>Z{aJx>Qu&!~w^r940M?UY`NT zURw#s-fh{_Gv@MGpY8v*_}9NJ{AKC+Q>Rm5dL0_}8lu>-ciZk*DA|qasn(6Gww!w= z+3&YExD#WTn2jYIhcSD6cCE09tCTB@aNvotrI~1dVt4MH9i4X-?p=%Ad#!`)nVhla z+`I4{{Ii$&Pk*L}S{j>k?|n_R56^!6HPtRv?XAr=xD!KD^{r&+`gZO&Ov|X6&F30Q z=B0(rNxM8W>~I&jTTG2wIbqvDJbvZC(=o?DoMM#birAy0fo=EuCKkc{$SM>ok$GPT zE)NwX_iR5vWZ13ynDk_|j8PX+oHu&s+7DLFTOD|+gxxrE_hz>*-tYI$OO)51qrUax zqr*R@eE1smlQ*bu{D<=HE3n}D>kl3s@ySPXr>qD+_u|%t{pbG(>-LD41Bary&iiq= z*XBL(SMqpthDwGOS5+LX8epBHQNV_g8WkH-Udh%dK(J7-)p8aHje;SUlyex_)fI^a zxxk)BBqv@KZ*6G28 zFz5cyZ2x~}`VC4y<&jFj{jgiCw`g>qp#-1vHRKQ;D{YtqqwrJZ>_-T#}63zIW1Z_bK7o|6<)UI;rx0v^$Ud8ZX~ z86|x7^rCB@W(D_3bHI97_U^E63t+uLE2nAYNoxLw+?4VAw3u3(jf!GS)s`9Hvxtqg-KHa_gp@kENY+AVN z!s#w19oqJC$J&_NaPGDpsu}iv?$ur3POm36G1(?DmGfBjB0g6k1AmAPEk=S9eWGi6 zb6cAu-0!M$Z|<>!sh4cK^*Xtl+Kl!wnE~0Ya7P=Onr}D)>Q1QJOVbJY4i{RjOt&Xw zhz?>lh<8JLPoQ5XEn~2zWqP&`BNkL)bWOY8uyL5$Z1$}!oSt-XYT||WE*-ML9o4y; z=o%U2B$;#n$JIkGB%OXC>C72cd9H{v5^hZO`zj8>-FpQLpXCF0%mXoK$8X+wN!@xw zTH6okP-aFiw$D(7QOKmtI=XM@>Uon6Y}<7DsQWh`x_s~|fIDT#e_TI)i}KOyl+WIx zy#Fc%-1z1TPk#OB>;pR@O7lbOOBY3)`)J#;fiuRyrYKXsMSCpa26_;K=8an#dN#VI z1o(8KG7?yLxN}uQ_;>pi8{e4-?kwo*A?U#+AaWU-nN?i@yg4xG*Rs;D0^Aejk3G6S z0rABhpEeAwC-mZQNb*hJN%H7!G=DbOgQX(chk^DbKlOv`OE>cEu-nWT{2 z7+PPltu$p!PT0E{K~E%|a*y?)UiBOh?=vXZcW}P{pzN~)GfodohtAytKtJvMi{ULg0{Y;V&C;qNH4(6%d(aljskJT#mu!p+ZPdtV z1m6|XdZ`e+3)OsX1)o#V1TX7E{6?vWtCVpRGMYpPY;Q4#QN*Davg!G(#vDc+aLx8& zm?|>8?w^q`$N$JU?p32t;ibE#uGzGq^+C)v|_tD;G>c7@EWtIS4S zfBPKP0m#mxSMXSM!X}Ygtuh+bCbGIjPc&PE6RKOg!Tl~k@(-D;c$vQ*T-tV_pxI5-#`dm=8 zm`_yZ{1_WBE+*iOiw6m~zjo16ZH91%Eq|eNUjyQ)=)ri$A(Im1*ojc6AOHg?XEP_NKME~Tfl)xyhzIb+Z;K_fE z{Jh`WFGIMapbmMS^7Z?akB9Vs=jD-0=0z4{hu4>F$&Y_0{KzLUr$uf~xUIW;Fa9I!C z9}j$sfN9FEtxPE`f`#qY@<*6$N9B)p+m4E4Dq+zA9DWTL-K7dbId;DRy7;m>IFSn})T(9f{jP4)0(**$9dl{^f9QQf(htFIV{Nxe3dr->pfm!IN z0Dci172*%O$9cO&d$~m(0M30t*xvq^cJ~k2(eL~=*xMj*Gn_yjvXgpwH#K5ECF%er z-iMlU^r7stFXf$ITM&1?Ff*+y%%Bt@^9DJ0vS3Eu zpi|WAUDopMU5G;^%j0FPnsKR9>MZ zc)NHlbzd@RMW`=L3r+$Q_Om60v_U4W67nJ3o4937Y;ayx*2Dsrz^xZ^=!oo%1x#92 z12pV56O9@=mPZujOg($(*)?+>Ui8Z-@2$I{LWmm2p39*0DauprEwTB ztfQeezo8ap{H!^5!nxN8cugvWRHp@G2ca<;NwbTbJA^x00wmy0IWoxxtc+~i}-QGB?xy{{`NY3<83VEm^b9m(GZ?2wxDR`efEn_yT%)q&$-ZH51z%#MO-^@P$MpjS& zuR2@E`6ABu%Xq(G@xIT-ofx=xA&9|y7kR9n2Eu*SRIl{7>&(c8h}Ub;_EcEjPRvkc zsujm8b00pr{@1WG@BB0dV*Pb%y!Jfh!`DAwG%F-4)!5n^SXKDK*=?@`?tS>&*0--7 zdnxSDC();7q+Z&RaOKG*(*WEd*%9JldEbohsNj|<-);;GD&upA6g&&%XNa5(8(^RWt7O$eUJ0u)x4s&78mR5Mj>=(F2sm&Tj|kXyalK3eTMH;; z*7`pwnjH$4Osiy~uX8U=FO7w_sL0Z9@DSFVyPPGL$+g-95%cj#FKW;>s{dw+pSArg z>N?vmV(-ATlYr5KQct=QaEECbuK|G3;rj<%+SNZ~mrL-@enH#%1#IrOe=%_DgL8Zb z=lO!8!l0}(B;1padBh*_i1&88hK>pm`&uX0%YEcV2Vap_=ny{)%dsK^By~X zA2@yNQ@a)|%ehvrf*q9|ACab%)Jg>4eF0Xzj0eeH!eN0Z;xO`=4cWAsbdu%{sI974 zdhyJ&>*rGDe*e#v(*v@SY^?$u;hth0u|#Y^x``xXUv60=}BjIlE=iXbX z4EVoyIm3RF>iDA*W@*~3UESMSx>vjR-5&D#T?dGvdipktzSUkTV<$UmML_V~mX^6* z`vHq-Uuo(mVMjj>JN$gm9t8LG5bn(;rP;(%Nq61dzI6V@o?(?tRX3?WktLd24Y*-{Z3TuQ_o zse~(&aOD!NLRK!~9VkxyPvCBsW2>k>%f1LbI%fF-SHii!{@j4KUwUuyxPu`<Co@jfy<`dYJdn=u^WJ0=|m(TU!_(!KfR^;z1Bs)F@z66ltx550SxV z18S5vp;5(v>|$cqQjbCra_S8^j&5pc^vL`$Ya2+SG7@EPQ05)ZPcUeKvtj6z2di`5 zia0tbaGQN1^Lf}Ttl#B5{jVK#PdMU%a_;Uihlrf};Be1T3I0!oA9TA2;qDT+$>sd^ zL7Ar?Dm*tR&u>sJY58HYKIy1i!V&klL*V4VHSz$k?JoXX;HlfmwQy#O-$rV{X82ek zu;1i>tDcX<`h1>sVSZlJxq|e|g<0Xnu)TX~U1cesOCGn#HDTGo#=_Ur15odTn{e(* zdA(L%t5ei!6%{(^v)s6mk!K==w7hmrL5B{|JhwyZab@3^6;a^^VNs`{5MUi~AB2y( z@v-8dC-yIWa{k2U_OJN2|Mu4d_v}lL$>KKJ_DrENB>2SY6_R?Hs8%W@qE05PgI5IT ziU8bFBjM+A8h0mzKezt3N0&``Z}+P0abb0+16Q3?1b>VO@WV3ri$v5}ulKxiOHqgF zIBO*$;0$2MMD=9AE&^P&0+@75Yim2KZfr+2jg0T!BKoO*M31=V}`>w$Y`dGC4h>rY(Xc;V@HlKkxZY_>rMer>19yU7}ai$l#Qj8mXW{#0x3S8+~}k!}BK~zK?>rnqNmy zCk?0m@{Q}{5pS)UxAuZ>DW3zg?dtAuC(`b=%U%MJ-9~vdWP^0^45IZFa@;h9qyaW* z5|MrQdd^0$ZZ{g8kG*1u=e&wt>O%=_~8q?NVo@W zg*|>R>;#^=-<7=st{rfV^>&Ma3#tIYgCjhDN;&&1c z0`9(KJ_jc2kGjPlfoU1=i-B8R0MmU|Qje~JLx@hTCE*^lm2zRn->x3~AlY|jM%dBp z#K4@C3%Thb`I*rbg?TI*K$1O~j(Yi)NChMEE|1O+ze%!#gBeXFuhA<2-)nS=^^%I$ zG6Ig4q^Fyu`5n6aPTf_bkQ%XXa${P4rxyGZ;6A@YUx2P0QZ+tS82FEUOP^cx>-MB8 zezcMip+^R;nKsmCvsY$(K@;27ya6s{+qU}>Zv=XsY2^L`d5v7SDLUl8hc*pbGHK}k zwa2rQGFgp8(W-UpnT+10HIPGrQ;)Ephuw70Y{!1v&}#I|xmqRoF1J~$z3*oR_6hf1 zI=Q3XGT^$UtreEy-|MG-Ga||*&DY(hdNXA226y{hY^#5V_t4rx)TQ21&WL)q`QcCK@{L|&xN2;<5xb&JPb}f$`-cU9a zlKt%aiD#aTJvwOfoPnEX58OP*ea;U=?2ZayBBrwe&7~3$faV0yQAnB;(o_y@QD(%m z!F%BRpyR8khgVX3R!|Nt|GyI(R)zY#Jz?yqMRR?xh1WH4jweO^aAfCudsjZUWAUda zcf56K`};wj?_4eO$kzmA|x z`iA<;*RWgGPhUPUbKI{!d#;w`vr+OLU>)V$yO6zCxHGI|??Q7p@Usit9ly!8hFLvI z03E)@N@?6>(^I;<@$dSA8<$ z{43KWrnsm9}kSqYlhAGleWulH+Bu0{dds@H-~H$_<-?s8bfQ#T}zt(kD{2Z3HN5lQg>@k z#!h)+Ku6mh?%sZP1YA5(5pT`U+<2~@(b`+ahIKSX)nO48O;rWAgEJW0n+^U09!1~# zJSK3x8Ah`ig?$)P4xh0xH)hBM&*2d#f-CbYILrzzyMo86;<2lF>>3`orioX}=hg~% z^};5auqm>!;@u?Qca#0zO7?vw_Nd3&=_qEnPyNn)>L|A!gG+u(2#KvK;mIXIDRDy< zPaCm&?W^nNf8(?LmEFrn1soi5a_gAzlS?xq1bPju6tXlkl`?SVTPbe3T$VpO@aUtz zPo&Nm4SS@(}aox3$f!&2L^ z-fNb|G3d(P$!@do=(5*g#4>A&fK0NN$%S9$hQ5~>bc|k{E9P0pGZB5>Wn>cqgiEGM z4*qsiYpb!XB}v2_oqpl(VFw}HVH@|IluNrPm-kS@_PSi%*Z=DN0ax}9xU~D1?63jB zI|l@9>vwLG%lT~(?nMypgMo903#unQl8?DZ9qfN*19kTT%HH`D&qZ)FtIu*`fFJeL z+DFc9dm;3|m>9neiC2y!M;%SRc04sEFemj|MNyfMXRX?_&Ciq8C`8eG+S1CDH#5$i zV^>uxM7=i->wQgCR;Q6y>gAO>aH*Kh&mR_b(yJsjr&F8TsY`EH?GjebWMsx#WI3I> zoKAgSrw(ynw^mZ&l78~3JxgC&J|mUa2#yd9YM9!};?jP*;`__D_i|NXL``X6NzSRf)E$Z8Klyt7bN!sb%YJ=*=dwLXky)HZYo}tz&TjVO zLHC1G?~`wDcsz4lgdp#nrj*(0WjR`<*r09ullNxcadoM4@2eUT1T2PWyRY*wH&h4O z)Jphw+qR~gSxU7R$+S9D%d}bm$9vt23l1dUYp?53Fw+J1i0{C;t5F69V9Tc@Ewa8G z;G2jB6pbQ24TMGGg0)|w`ln7He^}|vF-cIs;6Tm&@n8&JV9;>E;aQkJ1+e8?< zuUtS$c3|7Z$hL!!a%B>pLQ=wR{QA6?+v)X`v&7JCGUOxZ9in$|puX$?ujJJ2Kcy-T;7Y?qOdoAcnL%G|-c1$G~j+)RC}L`Q#DQiC@DGw?BPB`SA9qa!MJd* z_=qD(QNEdpDGgOsk|yg`TPyNZ(qgILIHP1_LD-8azME^aYUC2@k*P1Db|gVd*M?^{FBrUHirbQ3 z{;_V(yZhG6IDf*wFtd?3LMvrtZ~b1MaBpz$iEr9<+UeL|3c0PXz3nIhj_W*~%9@9H z!01PD9F0ws+;>?}C9r-^YaH*a+B-XX?peL5%nWkwdgSQAiTU*p`k4E&$sWrm z4_r3cb^O;L+pd4ET_ePyu3(0WwFEL{-@+#jty*;Ld`x9gcz))u$Mz1{@cW1R*N*c&7+zD%)2OsCC1n9# zhN+U33b}!WnV;=l`}f6@Q3d2Ea2Z^WL%2`)3bwHYaR2^u%GhDB$K=S5 zJih+$aAa76Qc5r#VcmA+mic}|xcBwNCcV5$Rw)mbLRw~rCaY7M)v1Hg0WY)R6>y6NTp7hobxS<@{LaPScy0v8 zhH9J*-Y$R zA}&?nMvIYXGDBQfHXD>?1Gog&IT~eB6MaEZ?D*7+0~b&9Sp2icl8J7B>?pQ|_}4UX z2)@$Nnm+;vet_HuedGUZ*V~8S1>P$rTS!1aP z?$oIx0o>uFArkIiP{&!}KKkSSU%Wfy=P}~~z1Qc)a@2BbseAYGm>Xu^Z^OCw;aK4p zyG9{S6fkF($G@H)FrhHAkhI1mr^vjAN^Z-9aMVf#>Zc?&8v;3%uP2;F&V3Ihd>ljX?gNV`^tt|zQoIAAb0Q4548ZjMEUTr40kN9qJ znrL;iOw6ZGg zl~li<3!}n#b;MdL&7S?}V)pcvoh3oRfyl}P`^## zL0$&4mZ1~fF)@Aqh!=es+W9VtjD*U9SD z@ZL~r1;U-Cba+9hjB^NfWot9~eoB9*3pN+XesEDN+Ia{LuSbuQcx}zwL zMeb&_lnUMYgSK>V+iL6R0Op!-@qP6k0JozJ2>;V-0{9M&6L2dN$#-(h9(3_UAf|%= zcHWM43+dSb)8QpxIx_D{6XLtsP$}TK&m9NCZSD_l`=T1Ec;SR`7+4DPP*Kd>^5bl(%vmV+u zZ_cGNmn-sRI<4HGOR6bfeEy{C^wH!@XGC`D?6Gj{=bRrX#KZ{_?jx*lN2{shKBs>B z(TfvDZ}2&oUtI~lwAoxnwM1Ag6V6Hw9dhyDoV3f4%$j-PyDZ$@DJQYZQ`W z^wMG37f0rY?W)f$mI=sGJUfzXZGmAlX;u0b3-AqfI>p4I$fpxdz%i-p&l1TOOYxV%iTmSGZfZ2NP}d z?d>_y-Z6CQfSkVL>~fS@>ifOx(~RE&-+^0~@wIC3HM~1xlJ0`+aBpt0w4fTtd;OH| zwuo99b6ab#&fU`9dS@!1(2fT1KmhTib4LrRFf-Fkkeygu1!PA>?iN!Q#G6e@WZMzh z^(5uZ#tH$)ZPpm~S!3O2j&&XXr48;<*nU7yz&(#qKmG9Tcb3iC6yl#lud{%IHynG? z4o=P*`Mi@!*IwNEJ48HDp+UlZ0krKq=Mr#-rgFoKzipd8#`nN4XS|+TIdj15A1E`w zqs$sjnK_y|do*?4ILdG1D09cb$<8n*GumF^PUK_&*~fnR^!O1w&K%FHDz6dqiv;XM zMqMJKwpuD=Yn63!Q5>i4w~Q-qUGiR>9a$jeTqhxO8*q2no7%@0UgdD9B-c)f^}t*) zcU^VH`*2T1&I8)siJ|nqC@1jFE%A591HPNc5p3 z9|1v4Jw{DCPRTe<$vokbefrtr3+wqMSq3S;SLpleg`uV8{@jvz+al}SA=Wz|jMDW#Pq50x z0qelgxBodAH*9;Evv!~V)lT^83cNmkb#%#W(NyzO};BkMg?&0e2&|+??AXu z{f;0z33nseI4m$D>EQ|A{IqXlVpW;Ms8=+@>~LFWM_YS)OIruAC7LUjj^4I<;JhCx zyXR5&FNSd64dG7RK8Lz>HgyYxJ9Wc!%KB-PHB+doVFP#6=J#jH;t3$AzfYjd9}nR^ z=X(HmDj@z0*nt2RK~5e4ZTp0;27U9*g!248?;1Q4z5Z6_oNeWC@6%xH#G^)jHad>p`;y! z^XJkb+_yHB7Mm5Swr22_(N(t70+b_0voAhb1yUaH=>y|@zRSNtuKHdVYQ$fC z12b!F&tPLKIDq8>&#tb2yd-#V{887?ovwLjhQy!tsmV`lS0??TPWl5PKDkpfO;GSi z`NhE@dmcZ#<;m*H`{gzD>YIY>4Qm;RJL!2>-QLg}-Z|azU}k&kmGI*PzKQKlx3UOC zX>Du0YXe*Ff1w7Gz^k|a*>5&1+G%gqp(c^gvpc{YCTh%tc?b3!`FEI>LD?94n25A{ zwUGPB^s#P}hPzD~;fm5Sk4zeQI4(l0*D_?{HU1}Fn)CCqxCowFF44mtGH~e+ZI4EU z|LqoZzS4}&*y`A1cYmi$av zG=Z}4C(8UEDf4~+aEGg@GhyQa%Cu3G$;10i`0AOd-;Y_fFu5?FuThnWn$lVIsjRwW zCd7IYvmRVlhZFjr&4LjsfDp|-_Ta)o- zM&Rdpp&^`_8aXkxyX$F3-^TpLz(q~dtlbM_u^Rr<4H1LF_PHHh<+^vF>#6l_g?=AJ zo;*;S8*UK9wJSm_;^p$n$E!lAxu*vmTRkvv+u(wL7waObmAy2f{PV)b@ji1hD%-YX zg70!*CeXX%w2OCJI0`Tl#Jw1EE_Rw<~wUwnT`rgauKVLOJzBHdLlW^o>j$Fc1$fPW}~sSm21+T z-X1w;qqq%s6YjkOoqR*B9YE3h0)7B|qQLjd+}3)p z5%A}Va<8AjUtO@rS$mtVg~+)#TgzsMYDh?SHKKesxGPKs1>n2MC^sAFGSSJzs881| z9JX%J{_u-=jrG+$_Qj0k4;Rnc7JQa2YGS|*O+pZCsYsxdqb;=%?k#A7mASRe)-6Zd z40mbKnmD0ZsZTARMp-feX4^q*oC$L>>!wmyPa)yH>=$U;7f+!4{u7y#nG2me+Pm}} z<%iFn{^h%%=*Su-gD#hr3VB&vdM39qgG)az$*yw%bS(zz*KlhsYvKbUp-zqTc1@*U)8l;G2jvd~WMYcbsKow|OsOmN3j zGh9b)MwRboGU1NaQHeT8aHAFO$hm`%gDdEtLXD!1-&7~yHNYLMO-oK4dh53-S+x}` zshA}P&RxouiHexCS^?LB)>nwM3>;4gGc&D*7T7k27!Cy<$k=M(iiB%Ucn_LBnzD8Z z1vqzL+t(tvubfO>{tE~a?!dYKKAtjf9A(ZA(6&$g_K6?AIeF=#RHkZLa%q&>FM`l{!6i_nXLgt{!W(FYCBs&TuO%n?v zAC5b+y)L&DSZN2NZS%;8MC3MCp+Rdv?WT1W3&1^txF5%=9tBw?Li%x)tU@k6)L7g< z&il>GbK$&t>oJDJI*sVS1C!88A`D1K{K>?qg!KDY(6|XExoR}XT$F5ChH>4rw4ihqf?94iV_@$iuDV(fb ztRi1#c|4=Wv9bHm=#x9j(rj?Qp?U8sEd%(%)+kkGQ(OBDmQva}I+SJ;U$3#7K3l0c z^xzU=z0_*w$>vjDw8BraE_|4EetT^WY%S^7s!(M&*VaRYHECn7p(M<*?aY8!HI*j` znR^=Y0n^{gI2Q=}!-#E9iB4`hmD?K7-+b7XD0{^&&35jBvQnecFSul$bPwL)eq@!K z_j0#G%RNANFL(C>u?&QV*K!Z*b@_vjC!TnsL<&$ zDoUq$?R;YHPjLCvo^YQ)S@aWi!H<+_qh6T(^YUX}ndPNoy|zv!ED`dG1RUSm!trUL zPo|t6P~iVjeQYGVHiOfU&T2?u)e)W@AU>E?`7tl$<)*}`O}U%7#c|vQd)F|v{H?1; z_Els$E~wsMdn&p@VBOY7jD70~R@5=Zz|krjNz-jD*CQ!!E6}!=>t%^JCm3ZPWd?tg z9qiRu1QYQN^FX>3o)gXDmDZg*)&d(lwN^@p%|K)E z+l1Yc+a=ds`xw(fhLjvyf}ZV?!Z_+x&;A)F21Xwmcx(-@@a~6Kdibnx2XT0XhtEm? zdLrDtS9k<&b&vGy=ev=zeQpe`ve)fnx84|_%aiXN^wxE6xb!Ajqww&_cBiLhS4J6>Nf zDlhDdyh{h^h011AOFKDi7PeSuL2z$w{(I=&$Afo&boE3es~Vifiu78kQL8o^>bdOD z^yKH}PK0}umi$avG7*A){y6H-Uys?eA~r9(p^3*hSA?B`2P9qJS}}B>I(f|5;^m!JO(^a;B1X;ncAY?QQYV zF(uvG0f2nYL?g^7PW|A}-PDM^{S$oL<9r5Q+~s~^?Z6|eJjht-0T2%^Ppok}x3&M} zJ^lPPP`1x|X8(#x3BT7llN}^e%l(pRwXv+VHmU`ol0#o@(r53wb@@L27GYs z#E$Z`5}Cl%)@p3A7+_k)qHZ>Qmv&*$<-OF4+g*dVzj^hDcV%{=m;)FGLT1#N+bqHH z(LZcm{mJV2|6RRc*50kz6{Xr{BU34>77HrHO_9v%<%RK2#h-9VJ5H&&FsR}3@TLqe zB@F;Ki%SP^PiNI%WL7R|%=&;9`zSl|c|qbRW#LkFRk>QiMp$Q~gnPY0vb7}X$&me( zG9lBUk66!rOtz7G3~R{>9kn478Z^e%J20*t9Qi~BEkjM#&yg(($W~%qb1&AioHzk} zX;sRr=>dSz;kdP=DSH|_IU$=$GW$z@*sPeoBLr}-_dOi2_nP(32bED>de)*9{Z z$h*@)RD}}dI^gnHZT`PP_Ph9Pf^(oEJze6x2gG_0h}b{i%HIB=yIn$e_q((U&ZRoO znzD8FfGu-|o!%W--h2D_E|1c8F8*fSy{$ii}@uu0|o$sliXOg@T-l z(z5zmjX^Il=<48@4q({1K@BC}r(bl9IYi0#rBt4Oksduoke96k_E}xf#L9$qkPRo8 zB|p%T{zkj4iZdEkkwy$#x~tHb4iy}?6iTo7D#mwxQG%^h#?h_Af2zAI zr8-!6IajL$hr^Z*dxF*Rl>C-ze~BM$?Pv$*L*TElmR>mY{)U}TWR12V4_1x?+rFqW z`J>zm-xY?RWmQxY4PopZRBRid$O@S(3GYZ_$0hS! z8^iqp5V5riT6ma$CxSHc)>ONZ;5$mV!xq8Ctj3v#cKvqD6ZV#YaMwwVS|W|EX*OHh z+W_&Cg^Vu~{Qe#0IWsphSIPyyuCaEktWisU$i4hg3LbCGE0H^on=F4^L0ORI3m}&#qE1 zdM0E@!!9R+$hH&d^_D(5RFh*YZ}r7TmmQs*;6;-d^bCipc`JG4He}OnPFy9gS4y%a z+|@PdPoy09T>pqg{IGRPEwN=x0kGcUTrC9t7h$+RNCJpQ@HKXE?l2)^B+~A% znG6v|eKxK3mAMmtJAMGzcEY@ijap!|QE2sT7NWVrIYz}3mpv)L+s0>x!V*lA2_|`r z26J00+o&FscLnt|^n`6}&uvPS97Po)W=D7k&}ZMM-c(bWV%T zU5MTt5xqtyT10dbqC2OYoFGbqM2qP4&MDE4J4B84T%Ld8dGqY+o!On8*_r)(cV^cj z_kp4H3!A>W8RjPzNEA3*lVZcE)Bq0!Wbzy=B3tFEpWSQNn3J`hXY?Crg}O*a#W3et zUosc6zcAg574MAk7O^w^cq3U=mA~L_{m*;>5{V3!ONp(j3z2th^(9u{z7^(!TdDom zjmwf|ZBjMrE6$Bp+5=`bqR^o-J$zvKfewr5Y#N54l4<;-M~<%bP&010Aons}GE>`K z_Q2OeiuQ-B^KjmFd$@+sdkr1IL8(7+p2)C?klE5|SQvu}VfP2_`gPIPs(}Md9?(Sl zD-|}n5o^YVXUtW&-t>fdn3XzBiMz3TFqI*A@3!zfH&P6nKr&_zAM2*VZynekEL4$; z^Bi!ekv?F9+Ff&1*mP9MT_I*C(i1|x>hwl3AIfgYN|iETntDB{*JdKKIi>DjNeZmr z5eOSm%Q+5XA%OVhHw)gqRI@j1i6>Ov;|Ulz39r2yhncG z^K7lkQVJJJ-UjLny?M`#e|RIZwtB`s7o;0}i{KOAQH?_BRNB2wIu-9=L~!jG^O=;s zQ9s~x&gu)#<-P#7T^F!Nc{@{f7UGpfH&&uw_xf4VsRf7@qwi_KYiZcwab2l3eVpn9 zQs;hZ@=)6daHsV)2zA8wSWBsXH= zk{DiXtEVwRtgXkoP`It-hmqz&lK~HM!k&V&vJ#Cjf=38zPsTK-r)mJlDck`GCp1D z5Xuu2YOP#^x3xt>pCV#{&!m{@E&FvhCIY2tJL~m>YWY_eWi9T}9t6~E_xS1Sb-aeh zy==5f+*x{C@^+mz7a-le$e<+i_=-#JGGVC3I$@V@bf_&qP+kqOR%2}mny7#>0;bokYpklXG!mt@Fn98 zNan!!Q_PmV!90eU>anG~_tDveKYwTME75ip!H1i%lr3XAE>Vd&(3C4tx$yIW%^nll zkI}PPiU{K>lO%eMJgesw8R9j%p$Zd>&2W8r@POQy^7*7lIUhP$zzJgegB2ykcL%h$ zl)(1Aw8nu~nC7pRM!#bOo-$bft&M59MT5U^9r3#pN?-h-@rwy!ef)_kqrM>nO_fNE zChCEn2xeJ8lAvx7|4d6@8CzynJ$%0-f{@*(00IqrA&i@NoZNPJGBumgs7C^V`(<~V zn-vaOpCDoo2mmbWRir&;EdlpOyO=-I$dQ}|7b2x#J)!Kw3@0Fo7868zyIzJ_1b+O9 z&ixD^;!-BLT&Dr3`J3G8_Mz_iY2?HYSO=H(8}4jv4uAevK^>GA6Qg7vR8GL(I?h6C zgEr~?T#$Q#Fd&)YM+|jifGNv;j>Mv zNnwAwv|+w_&DpgXo6Y3HBFi?VuOMpykG8ZBkz0Dr@gwabHis;ri>2k<(uqOmCF|Hd zJs2W@V1%uUIt$#RYF_+8IiH{GDAGO{S zl)>U*T83B&xUQ^rO$yit|8qhW*(7_D2(?%c1Gb3oJxwY)!2VnzCscqLIYwe;x-1^( zsXR)7cb6d>F80`(FFkr(?MYsr+gZ-eMUjzAAoD{jDt-(q^aL92K#()7LtIt?-1isv zKhf>BA$t2iG=3J?EE)?uwXzZny@`I2beB#|fJ(dbGv}iT<|Z%f`favJ&}LDBv+8on5eRo8g@NgS5SgJE}3}uQUWBcQ*!GHfW20`4@VAO51PVtPDs%mDbZgEFOi}@yJg~Y188OpXJ{MSu13DV+Vb6ZGQJjC2 zcsU6s!}H%%col^DSvJuu^EuE@lCEa=Q@h|T!;9xDjr9lOMNU~WLcM+hv7wD+ZY~bk zgzq_&PDR0r%C=0PlehWS=vpUElzs?(MuJLMLXNEOSXB4w3AvJ195-W)m~iG^QZ)M~ zB)Gc>fvhi{Z@`wBRR8>e6R;#ydMgG2=n!JYncub-OZLmq&HrWS1Wc;yX&l#-9o<(y zGg*#s_;9sRJBMB6>$@VBwk`cGaeFA=*Sq~ilrC_?lz89P{9ROwLR!7M$W{~u(?D$8cTfkBPFfuRy0w3dS#T>t)_Hw3nL5evvuq~v-BX8C#3oZC$8ZE}@)55vuo zDWPv2N}ESROJYwS6$B|{$?7`%DMP;h@`yL_*`Qa$mFAz8<$FT;Ufa|mS(XrrJs{)X zFOefehw@VuB#idYb{5hJ&<>^!G!=Fbi<^8+52=IX4S8ZXnUA}-=U$Sf_0D9IUW>oF zDr%V|0T`x`PoYqbH8(Pho!=E^l`|zm^lGx5ePT(Rc}LrIDk*brtVm2{8>G_(5q9c? zG9u>m=_(*UriDokpFM}d)`Q&s>O8hv^S;lbPrpueLT@rJB%iW9c6nZCGW%l8Nqzo@XwR+al z`>Rr2@(p%N-oKmGt!sgQ?6}KRD6AC^4qh#QbFbpFY1dEC^CXD{T)7Nqb!KBMw?+zY zq!<0^qCXY~zDPn390Z8@;@w%H4r=YuwV7iiT@|pG**or^zbo(gR*P0>W_gKp9RjJ* z4!8Xpl+Rc&;vU8>U)_FNjnY`~ z%+9X`<&FyY;Q2e^?|_~tN-mMKWMqB+u>1V_dsM28&X zOS6fi#3bu#(T}w)Fq$CV6dx+o#h+yoCpW{I&*IL)!8aEdLSL-dnoe`i{S(=EH!&wO z*(rF$`|R@Ce55ew7bw-rB?hD`W>{=7LIXPQUk~;U^-h6ReX{bu$wh`!(CREbChu>L z(dtchCfQ$Fiz{+@*L!95aiJ~+KdQ154>GO)Lq4M>-)eNcz7ychP0@xD*>rG(|7?sS zS(xXi4}0kI`$!qehL3b3opAI-kpOfQ0+on$xo}}&e+woG0==)%6T2Vkk%SY5+R&4w zHixq^{4m9zn~~|tFX*SSeQeIo`AaF90N7g4%)h;4 zA|WlXOz}X|V9FEe?66}=RCSm~bJ3#MF3h1V{EQ_y0NF@K6S$X2+cXe8GyE4A#0!~6 zm*3;|RC4xEDSe0CC(F7nT2~6yIUWv_lcl-PsDc`KZ1Cpp1RAp8Ev;jxA^P@{v0ob* zNsTsOrR`p_S?T$YAG%F3%mrj2j+rhpXD<(n{o-4BC!NJATY2ZnFCsQ7mZ=kzjXjy#)RwG3R-G09q(CKsI7%T>r_20d-;l$0y z@%PSz1}-R7j6F;^_~-P0S?z)(A}=Fw#1PSa+=A3h@F&YZ0#-ms;WJRtlfS479BmIb zJP`HiGdo1gzO;@LgP3}AjO(p4wVAoec|<&CpE#1dpP;r33fGmE)0?lXoWJLa-l!eK z+kyt5#QYEHIePlxo~GfaX5e6s@ol&>>y9&6(%plLHDwLqIRit+;WgOraliX9eO*vWN6SOgx4< zBGwv+{9vky5|kM>XfL5DY$%B}6Q>^}xpfyy|MjK4)%-k*^M4j_O?>X`{;DnZIwvpS z5tnVaQfFyYbX7K;*U&~(3l2CrP1@H_ zjPnWZ>?lm^IPMqr5ALy%hgL#GYhI;_(#nA9DSGR^Z8}4_RH$TkN88H^Ab$h|SK(B@#YT|6WLtbf=J^pv$HLRIt5I1O1mP%XQ(HrNv=L`1{6y9{2PRrU0 zNHX$pZM>NJE3Mq}kH+_w|J7Kvmm6%H7`#N0;?NI&@hZ-sTb7^O6_7TA#v)~!MNBz$ z%C7(0SvU^@)u0s96%c%+AcXMhhfUg!mqgZw&xlySjb#gbBG;*mh#SiCR|UglkCpxC zf)Bvs^D^h&2XzP~3~XHe^PZyd?{BQ}AYe8#t-*nZ_k^XMXlhTdK&V@LD!~})m4aZS z{A%qu%I>pqN%ic1S=An=F_<+~>r__2SC22gfWao(Mu@u^ZG4)DK}+}6$*Kexx5?$V z+y9tm)=b#MVKLJ`W;}97zcVLsu)hKg+fnjZ3vJ*`g`1de_sFFZjkMwFmOIY%QFAl( zAx9_>3(@GAP_6wO@X4VIulG;RzkuqdCKI&1v-_R7Bv^-UX2}c#RWnp_pZBR5St*&|C9e8FWvSvX~Wu)d{1)-j)d5BGz`@1RBfaG2N8 list: account = self.account specific_property = self.asset.specific default_steps = [ - { + Step({ "step": 1, "value": account.username, "target": specific_property.username_selector, "command": "type" - }, - { + }), + Step({ "step": 2, "value": account.secret, "target": specific_property.password_selector, "command": "type" - }, - { + }), + Step({ "step": 3, "value": "", "target": specific_property.submit_selector, "command": "click" - } + }) ] return default_steps @@ -132,7 +133,8 @@ class WebAPP(object): return True for step in self._steps: - action = StepAction(**step) + action = StepAction(target=step.target, value=step.value, + command=step.command) ret = execute_action(driver, action) if not ret: unblock_input() @@ -180,7 +182,9 @@ class AppletApplication(BaseApplication): self.driver.maximize_window() def wait(self): - msg = "Unable to evaluate script: disconnected: not connected to DevTools\n" + disconnected_msg = "Unable to evaluate script: disconnected: not connected to DevTools\n" + closed_msg = "Unable to evaluate script: no such window: target window already closed" + while True: time.sleep(5) logs = self.driver.get_log('driver') @@ -188,14 +192,16 @@ class AppletApplication(BaseApplication): continue ret = logs[-1] if isinstance(ret, dict): - if ret.get("message") == msg: - print(ret) + message = ret.get('message', '') + if disconnected_msg in message or closed_msg in message: break + print("ret: ", ret) self.close() def close(self): if self.driver: try: - self.driver.close() + # quit 退出全部打开的窗口 + self.driver.quit() except Exception as e: print(e) diff --git a/apps/terminal/applets/chrome/common.py b/apps/terminal/applets/chrome/common.py index 75e59a4c1..4bffa5081 100644 --- a/apps/terminal/applets/chrome/common.py +++ b/apps/terminal/applets/chrome/common.py @@ -1,10 +1,11 @@ import abc +import base64 +import json +import locale +import os import subprocess import sys import time -import os -import json -import base64 from subprocess import CREATE_NO_WINDOW _blockInput = None @@ -38,6 +39,16 @@ def notify_err_message(msg): _messageBox(msg, 'Error') +def decode_content(content: bytes) -> str: + for encoding_name in ['utf-8', 'gbk', 'gb2312']: + try: + return content.decode(encoding_name) + except Exception as e: + print(e) + encoding_name = locale.getpreferredencoding() + return content.decode(encoding_name) + + def check_pid_alive(pid) -> bool: # tasklist /fi "PID eq 508" /fo csv # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' @@ -45,7 +56,7 @@ def check_pid_alive(pid) -> bool: csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], creationflags=CREATE_NO_WINDOW) - content = csv_ret.decode() + content = decode_content(csv_ret) content_list = content.strip().split("\r\n") if len(content_list) != 2: notify_err_message(content) @@ -82,13 +93,20 @@ class User(DictObj): username: str +class Step(DictObj): + step: int + target: str + command: str + value: str + + class Specific(DictObj): # web autofill: str username_selector: str password_selector: str submit_selector: str - script: list + script: list[Step] # database db_name: str diff --git a/apps/terminal/applets/navicat/README.md b/apps/terminal/applets/navicat/README.md new file mode 100644 index 000000000..b46ffb9e5 --- /dev/null +++ b/apps/terminal/applets/navicat/README.md @@ -0,0 +1,5 @@ + +## Navicat Premium + +- 需要先手动导入License激活 + diff --git a/apps/terminal/applets/navicat/app.py b/apps/terminal/applets/navicat/app.py new file mode 100644 index 000000000..33d89d2d2 --- /dev/null +++ b/apps/terminal/applets/navicat/app.py @@ -0,0 +1,216 @@ +import sys +import time + +if sys.platform == 'win32': + import winreg + import win32api + + from pywinauto import Application + from pywinauto.controls.uia_controls import ( + EditWrapper, ComboBoxWrapper, ButtonWrapper + ) +from common import wait_pid, BaseApplication + + +_default_path = r'C:\Program Files\PremiumSoft\Navicat Premium 16\navicat.exe' + + +class AppletApplication(BaseApplication): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.path = _default_path + self.username = self.account.username + self.password = self.account.secret + self.privileged = self.account.privileged + self.host = self.asset.address + self.port = self.asset.get_protocol_port(self.protocol) + self.db = self.asset.specific.db_name + self.name = '%s-%s' % (self.host, self.db) + self.pid = None + self.app = None + + def clean_up(self): + protocol_mapping = { + 'mariadb': 'NavicatMARIADB', 'mongodb': 'NavicatMONGODB', + 'mysql': 'Navicat', 'oracle': 'NavicatORA', + 'sqlserver': 'NavicatMSSQL', 'postgresql': 'NavicatPG' + } + protocol_display = protocol_mapping.get(self.protocol, 'mysql') + sub_key = r'Software\PremiumSoft\%s\Servers' % protocol_display + try: + win32api.RegDeleteTree(winreg.HKEY_CURRENT_USER, sub_key) + except Exception as err: + print('Error: %s' % err) + + @staticmethod + def launch(): + sub_key = r'Software\PremiumSoft\NavicatPremium' + try: + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, sub_key) + # 禁止弹出欢迎页面 + winreg.SetValueEx(key, 'AlreadyShowNavicatV16WelcomeScreen', 0, winreg.REG_DWORD, 1) + # 禁止开启自动检查更新 + winreg.SetValueEx(key, 'AutoCheckUpdate', 0, winreg.REG_DWORD, 0) + winreg.SetValueEx(key, 'ShareUsageData', 0, winreg.REG_DWORD, 0) + except Exception as err: + print('Launch error: %s' % err) + + def _fill_to_mysql(self, app, menu, protocol_display='MySQL'): + menu.item_by_path('File->New Connection->%s' % protocol_display).click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_mariadb(self, app, menu): + self._fill_to_mysql(app, menu, 'MariaDB') + + def _fill_to_mongodb(self, app, menu): + menu.item_by_path('File->New Connection->MongoDB').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + auth_type_ele = conn_window.child_window(best_match='ComboBox2') + ComboBoxWrapper(auth_type_ele.element_info).select('Password') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_postgresql(self, app, menu): + menu.item_by_path('File->New Connection->PostgreSQL').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit1') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_sqlserver(self, app, menu): + menu.item_by_path('File->New Connection->SQL Server').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(host_ele.element_info).set_edit_text('%s,%s' % (self.host, self.port)) + + db_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + def _fill_to_oracle(self, app, menu): + menu.item_by_path('File->New Connection->Oracle').click_input() + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + + name_ele = conn_window.child_window(best_match='Edit6') + EditWrapper(name_ele.element_info).set_edit_text(self.name) + + host_ele = conn_window.child_window(best_match='Edit5') + EditWrapper(host_ele.element_info).set_edit_text(self.host) + + port_ele = conn_window.child_window(best_match='Edit3') + EditWrapper(port_ele.element_info).set_edit_text(self.port) + + db_ele = conn_window.child_window(best_match='Edit2') + EditWrapper(db_ele.element_info).set_edit_text(self.db) + + username_ele = conn_window.child_window(best_match='Edit') + EditWrapper(username_ele.element_info).set_edit_text(self.username) + + password_ele = conn_window.child_window(best_match='Edit4') + EditWrapper(password_ele.element_info).set_edit_text(self.password) + + if self.privileged: + conn_window.child_window(best_match='Advanced', control_type='TabItem').click_input() + role_ele = conn_window.child_window(best_match='ComboBox2') + ComboBoxWrapper(role_ele.element_info).select('SYSDBA') + + def run(self): + self.launch() + app = Application(backend='uia') + app.start(self.path) + self.pid = app.process + + # 检测是否为试用版本 + try: + trial_btn = app.top_window().child_window( + best_match='Trial', control_type='Button' + ) + ButtonWrapper(trial_btn.element_info).click() + time.sleep(0.5) + except Exception: + pass + + menubar = app.window(best_match='Navicat Premium', control_type='Window') \ + .child_window(best_match='Menu', control_type='MenuBar') + + file = menubar.child_window(best_match='File', control_type='MenuItem') + file.click_input() + menubar.item_by_path('File->New Connection').click_input() + + # 根据协议选择动作 + action = getattr(self, '_fill_to_%s' % self.protocol, None) + if action is None: + raise ValueError('This protocol is not supported: %s' % self.protocol) + action(app, menubar) + + conn_window = app.window(best_match='Dialog').child_window(title_re='New Connection') + ok_btn = conn_window.child_window(best_match='OK', control_type='Button') + ok_btn.click() + + file.click_input() + menubar.item_by_path('File->Open Connection').click_input() + self.app = app + + def wait(self): + try: + wait_pid(self.pid) + except Exception: + pass + finally: + self.clean_up() diff --git a/apps/terminal/applets/navicat/common.py b/apps/terminal/applets/navicat/common.py new file mode 100644 index 000000000..f1e6429de --- /dev/null +++ b/apps/terminal/applets/navicat/common.py @@ -0,0 +1,212 @@ +import abc +import subprocess +import locale +import sys +import time +import os +import json +import base64 +from subprocess import CREATE_NO_WINDOW + +_blockInput = None +_messageBox = None +if sys.platform == 'win32': + import ctypes + from ctypes import wintypes + import win32ui + + # import win32con + + _messageBox = win32ui.MessageBox + + _blockInput = ctypes.windll.user32.BlockInput + _blockInput.argtypes = [wintypes.BOOL] + _blockInput.restype = wintypes.BOOL + + +def block_input(): + if _blockInput: + _blockInput(True) + + +def unblock_input(): + if _blockInput: + _blockInput(False) + + +def decode_content(content: bytes) -> str: + for encoding_name in ['utf-8', 'gbk', 'gb2312']: + try: + return content.decode(encoding_name) + except Exception as e: + print(e) + encoding_name = locale.getpreferredencoding() + return content.decode(encoding_name) + + +def notify_err_message(msg): + if _messageBox: + _messageBox(msg, 'Error') + + +def check_pid_alive(pid) -> bool: + # tasklist /fi "PID eq 508" /fo csv + # '"映像名称","PID","会话名 ","会话# ","内存使用 "\r\n"wininit.exe","508","Services","0","6,920 K"\r\n' + try: + + csv_ret = subprocess.check_output(["tasklist", "/fi", f'PID eq {pid}', "/fo", "csv"], + creationflags=CREATE_NO_WINDOW) + content = decode_content(csv_ret) + content_list = content.strip().split("\r\n") + if len(content_list) != 2: + notify_err_message(content) + time.sleep(2) + return False + ret_pid = content_list[1].split(",")[1].strip('"') + return str(pid) == ret_pid + except Exception as e: + notify_err_message(e) + time.sleep(2) + return False + + +def wait_pid(pid): + while 1: + time.sleep(5) + ok = check_pid_alive(pid) + if not ok: + notify_err_message("程序退出") + time.sleep(2) + break + + +class DictObj: + def __init__(self, in_dict: dict): + assert isinstance(in_dict, dict) + for key, val in in_dict.items(): + if isinstance(val, (list, tuple)): + setattr(self, key, [DictObj(x) if isinstance(x, dict) else x for x in val]) + else: + setattr(self, key, DictObj(val) if isinstance(val, dict) else val) + + +class User(DictObj): + id: str + name: str + username: str + + +class Specific(DictObj): + # web + autofill: str + username_selector: str + password_selector: str + submit_selector: str + script: list + + # database + db_name: str + + +class Category(DictObj): + value: str + label: str + + +class Protocol(DictObj): + id: str + name: str + port: int + + +class Asset(DictObj): + id: str + name: str + address: str + protocols: list[Protocol] + category: Category + specific: Specific + + def get_protocol_port(self, protocol): + for item in self.protocols: + if item.name == protocol: + return item.port + return None + + +class LabelValue(DictObj): + label: str + value: str + + +class Account(DictObj): + id: str + name: str + username: str + secret: str + privileged: bool + secret_type: LabelValue + + +class Platform(DictObj): + charset: str + name: str + charset: LabelValue + type: LabelValue + + +class Manifest(DictObj): + name: str + version: str + path: str + exec_type: str + connect_type: str + protocols: list[str] + + +def get_manifest_data() -> dict: + current_dir = os.path.dirname(__file__) + manifest_file = os.path.join(current_dir, 'manifest.json') + try: + with open(manifest_file, "r", encoding='utf8') as f: + return json.load(f) + except Exception as e: + print(e) + return {} + + +def read_app_manifest(app_dir) -> dict: + main_json_file = os.path.join(app_dir, "manifest.json") + if not os.path.exists(main_json_file): + return {} + with open(main_json_file, 'r', encoding='utf8') as f: + return json.load(f) + + +def convert_base64_to_dict(base64_str: str) -> dict: + try: + data_json = base64.decodebytes(base64_str.encode('utf-8')).decode('utf-8') + return json.loads(data_json) + except Exception as e: + print(e) + return {} + + +class BaseApplication(abc.ABC): + + def __init__(self, *args, **kwargs): + self.app_name = kwargs.get('app_name', '') + self.protocol = kwargs.get('protocol', '') + self.manifest = Manifest(kwargs.get('manifest', {})) + self.user = User(kwargs.get('user', {})) + self.asset = Asset(kwargs.get('asset', {})) + self.account = Account(kwargs.get('account', {})) + self.platform = Platform(kwargs.get('platform', {})) + + @abc.abstractmethod + def run(self): + raise NotImplementedError('run') + + @abc.abstractmethod + def wait(self): + raise NotImplementedError('wait') diff --git a/apps/terminal/applets/navicat/i18n.yml b/apps/terminal/applets/navicat/i18n.yml new file mode 100644 index 000000000..ec6427048 --- /dev/null +++ b/apps/terminal/applets/navicat/i18n.yml @@ -0,0 +1,3 @@ +- zh: + display_name: Navicat premium 16 + comment: 数据库管理软件 diff --git a/apps/terminal/applets/navicat/icon.png b/apps/terminal/applets/navicat/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..10b343bf0144f6b88f9d36eeb86db264eca67579 GIT binary patch literal 5132 zcmZ`-XE5B4^Zw|Eh#ECqMDM*u4Wbi4L>JNP;dIX7LPQM_qL+w^C{Zp-bfWiDqD1f9 z>0S8wzWl%V&(7{MyF1U!&c4{$-I(Wk>SQF0Bme-AX=LIl;+RU_KKA;u%;(B%5y1pw3(nySi10W%nLXDH3Y&l^4CldK^S zg9a|)({_rFAZ}$s2D<=~*R*PNd2s#F@9Vazt$g<%MHDRa<3>PO=?!Q;3p&w&3_wV@ z)p}eKf;~GQZRneQ1T*q~VQEL(Svx=LYV6|MusuG%kmH|Cc#}w7pRjWX;17SsNdoF{ zo)2%d;9I%^0YKEp&)LgSj+qnEuovA8PVi0hqGJdFM(1 zR7^l_c0O?=#O!TKiS<~cb5}s*0HP=HAcx0Zf)ZcPdhwKiVF zTcuFRUKOXug_u0ne&0}y3*b^*kh@84jjKD$oV7Hs@AC9uN6ngVlOCXG0bHe8H>=v_ z4kCmgu{h~JB@n25vW|$zT1jqdCi{4S z3tZl4fBcI8e(s3VN^s$dE7dX`Y~>b=Rt(5-awRY%Egz9q&R*2|@xI@}`nb_?K^&0K zl`}b-kF7lgOK^vn!cEacs!>MJ>al8>Nn@3-4viS7X6hh*)u(e>l3_(*PTA9rG>_?r z4?NQQybxTfljVCj!i0{Wf1{*stKVyDJJ_qe1e$A~6agTt` zVnXJHd{!`>+RF@cC+q6<>MVW9(9~QanHg%}Yql#kK)lj?vHx(*7wh${vr@$CxY-oQ zIyv~$GE$Z@Z-rBQdhLlyyESIbroyzoGT%G9d$t**>#U>plfMD=^lDGmO5q13i>vPNap3YV@QOhr)S&8#aJ znqg(}aZ3`E%>?K|7SiuCZ77bFnH!2qo^ou>ig7RJYceZLIA4H@x`|;c09^K>I9CRr9#|>AIQ1IG|m?n_YS`P!m zj-S0njvm9V?~WI+WOIt<45<@$W&qx3<4}yvZ;RDirr*Fk8x9Kkl`I$(KMlbkuu~JH zqKlMT@}Qe_mr|&a^;g1rEs+5xHR%7W=iui7;ZB5mXcs)sJA*7RqM3+PevPuWFA4--BW@HsQhUUU{jeh$)l2ZzxXb%<$Ki*+J?fXM1 zrIycPf+Pnlw>z?PCw9&uCmpfn(m&&JvM*Qxb-uSmfCH*Nwt~chv78Ku-(Y;>M%2RN zLhxr7TTKm?uOm7+yPxoca~Ia~qo-NTnE(M9*%5!;PdArCqBKPVu>0y+oFO3{;;2Mf zOK~Cl-WVfs%PL7?CAFegIObvta7~KM3yO$!9B*1Jv2ERP^}g=#NtTh;l;u&bWOBencUj>LD{xk zIi+-AG+5!HyvHtqLXTmBXVC(M)sbb~dpIq1bwna|pKkDsL{ARedeDe%t~}8HR@Tae z(b4r-U~MdeWtlzsL~<}o^a3^$?(iv1*T5JxTjYl`d<_SLYe>Zsj3$W@5bo-C44FTz z`JJM0=L*4d)rA9(9Vprhhp0f0RpM7zK7WFwjFEfs#ua8=S#n8+z{-mby8RBJ)0a0*9m%cq zb`!IX0h|oMSVqOrIbD6# zC?@O!eA^Nx#JCiNfuFZG*wV%8h6zq@GwGjpROestbkVza#)VaLm|wn8jLp5qcyW=a ztF;yA6Rz_-hXCq8U6Y8?tfZ=cbi*c31vE5lTc@b#Lo`HW^sA(LEqU0%w0eVs-#bwS zZcvt{{6ho(uQ94K=8ZnW#VD*Q)!4?_d*86YN%ixv#Zixt!aZa?T6XXLKKl5$jG>%%Zq1e> zb;9;ERdlL3;&J!noqlT6K9w--?q^{MS(t}azYrr=(yPScDW;YZEwTHu5G~n=y1c>a zyuYs7{4-ES3f`Mw_Dy}GqMCT^%|Xoz)Zh|lOYlhEn`gjG`X_ghW9Tx&x{0DX*hX+qLN`N(X1J% zi(JbmM8j!xD=sSp^sRC@gKOStd&B$z%UFhA3egrBQ%sL$33u4M#W*cir8hseLC(n2 zy5fVcqcOMu)oMljqY}{FKJoT}ae*a@<@S`c=`x<4zEY13@|6&Oc;-lDR9kGFSpg&y ztl?yO><0z^JXyn=?OHqDrfe9=;~Ld!YjO-%-`~Iau%o}56X28hVM_=P=}#Y2H21o8 z_V?42@I9mj8~bx=0yqSCm#YanVfLkb=MhBEoC?-#=HqqglV_gnrDn7?EWVepQp`)M z%Ha-{cr(BB7buZCtF3`L04{`ax3;>&l+K|E*A`hWwZBO0k5U2Nz#*Evk2_SQ z?Xb^w2-$T%)NNc{N1U@dAC382(R)_M2HN;$9Fx$}7BKmLVIGWGG!1YF!f?&YZ_#PXusWyr@_t!}#DtD5uzRL>fWdatfmvR9 zc-e_=lj3r#*&$zVj~;*c1{}W+CTLFw7t)Mx*d)^dW(ERQ`zRn{QOUhxgGK)xPOH0> zZ+Gh=U}^2kBE;_(BI$j3BjEH8iY!{SYQgMY8zGr$Oq#Sf&PUc(zr`X%bx;qc&#@mdH-$?trB zN4GS=6%`&i^NglA@^j!7V*`;!}+Kmy(2KH-DK#V{ae#N3uK@vMJ zW|04a7FYv*OqdUC(9+IM@kM4%U~kr9=n}HyNTdi6KSdId-v?Gdn=5|^?Z0Buw{DG} zUTyUzb?xhtOBR`Zv2{LLJMwyDpPk>=;hF@9>gPE1LA3|-=Y1Kde6!VEtRE}@09l0& zb_0q&m6t__S|-oh?5{HH_ihz>u@G|;0j$rjmmNd_#ijFyP?N4AvN{Fjis?t!K@A0~ zZ~gTLe>~}Yib|{{8;_f>y8daflG`_v7B>Ae;YA;J5we_8KO`w9g(NU*k6NJVDo8vj zmW~p*?Z=7;LF36R7BNF0R%E3n9WIsRF`G!ZMi7~Gxr+?0<-#Y}Y~Tq6BUIE-=@_+r zAtfx2z9Xiu$*8(*XT^$7cvIv0l8(9?2h!YlIUwYdb`z90wtYqW{aeEOsT~Q*?bDj7 znNY?lCxw9co@Tp~ts=c*BdyeWE8BPRl6r5mXc@L+OmuPcCP3kR2coPGOC`V2O9l9( zyni)=jcD`rMyE3=KWv5S z*!qSiiE29iGpUMWcc z4BnttpIc6G^omK4&3J@_j^sJa-8xd$9`>H!$c39)l0Q_m3@t$cKcZiV!BEqdcf;p- zLRv5fLJl-+cOvxkYmV1?bZUg5)@y$af(lqha5L(zTGW1(@2?<$lJN_3WsqhIY|vsQ zD8+AO8EY6Zcx}1;v-ZneJvAJ4*SC(w^yZy*u4Ho=<9wjd;xMVlq>xRAwXTvu`Rb+C zT#tg~Am2p0Hr0bOyVS6_XHgP^IBHlW6bOO>PX#BO({M{=emJlv2} zsKI$HL1pbM^w@ZmSbVg)rpKQkoV4!8MoyVKE~i zZnWOCWXwU7FbHqf|G|l2HS8vj2XUk71apQeXAZhy?7wkF{sy|*jGOEG!j6|#{lISt zU9hZhJpt*$d-v%C&SlY}_DkE1aqFNxwV%Iri19Ep-Q87d&CM54B0Ge6AbuPm!Kxa~_3YihOa;Hf63SNHMel$KEH8yUqo@|OI* zuF|R;e<3hKZ$IF+9N*@^%H5incq&Eg1tSz}(;r?KpmaDkt!8QCu`kbhyfIt-DL1UB zX6)~-e18}X04&O?eSSLj>CAMF_35p!(NSF#y$6W>1ZSphF;2v2_DkVa_4k}Wm?exe zWBwa#5NChB;?`zV!2dpmx~kJ{l{%BAK_cS2VOC_$t$xnG6H`7xH$Uytk|MK~Pe4En z7b3m3IU{|psc^v}jBO!Pn6#jZI`)qWV+8tq z34o{=mG;M+_dGIb7{4_AMsis~ah;2oH0itB65(5|O6)PRb5zwbYG%6-t+x_>TTp}g z0Jpsl?{mkR1q2Xs5uSrqkxYc}ONKtMA!$;vrrdTA<(?nIC>xR~zPSIk*j4QouPM^* z 64: @@ -29,9 +31,8 @@ class InsecureCommandAlertSerializer(SimpleSessionCommandSerializer): pass -class SessionCommandSerializer(SimpleSessionCommandSerializer): +class SessionCommandSerializerMixin(serializers.Serializer): """使用这个类作为基础Command Log Serializer类, 用来序列化""" - id = serializers.UUIDField(read_only=True) # 限制 64 字符,不能直接迁移成 128 字符,命令表数据量会比较大 account = serializers.CharField(label=_("Account ")) @@ -44,3 +45,9 @@ class SessionCommandSerializer(SimpleSessionCommandSerializer): if len(value) > 64: value = pretty_string(value, 64) return value + + +class SessionCommandSerializer(SessionCommandSerializerMixin, SimpleSessionCommandSerializer): + """ 字段排序序列类 """ + pass + diff --git a/apps/terminal/connect_methods.py b/apps/terminal/connect_methods.py index 454abb522..ab7c34fb1 100644 --- a/apps/terminal/connect_methods.py +++ b/apps/terminal/connect_methods.py @@ -30,6 +30,7 @@ class WebMethod(TextChoices): Protocol.sqlserver: [cls.web_cli, cls.web_gui], Protocol.redis: [cls.web_cli], Protocol.mongodb: [cls.web_cli], + Protocol.clickhouse: [cls.web_cli], Protocol.k8s: [cls.web_cli], Protocol.http: [] diff --git a/apps/terminal/const.py b/apps/terminal/const.py index 69f994476..a38d026b4 100644 --- a/apps/terminal/const.py +++ b/apps/terminal/const.py @@ -52,3 +52,10 @@ class TerminalType(TextChoices): @classmethod def types(cls): return set(dict(cls.choices).keys()) + + +class PublishStatus(TextChoices): + pending = 'pending', _('Pending') + success = 'success', _("Success") + failed = 'failed', _("Failed") + mismatch = 'mismatch', _("Mismatch") diff --git a/apps/terminal/migrations/0001_initial.py b/apps/terminal/migrations/0001_initial.py index fe604fc01..83d0fb1bc 100644 --- a/apps/terminal/migrations/0001_initial.py +++ b/apps/terminal/migrations/0001_initial.py @@ -2,12 +2,12 @@ # Generated by Django 1.11 on 2017-12-24 15:21 from __future__ import unicode_literals -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): - initial = True dependencies = [ @@ -38,7 +38,8 @@ class Migration(migrations.Migration): ('user', models.CharField(max_length=128, verbose_name='User')), ('asset', models.CharField(max_length=1024, verbose_name='Asset')), ('system_user', models.CharField(max_length=128, verbose_name='System user')), - ('login_from', models.CharField(choices=[('ST', 'SSH Terminal'), ('WT', 'Web Terminal')], default='ST', max_length=2)), + ('login_from', models.CharField(choices=[('ST', 'SSH Terminal'), ('WT', 'Web Terminal')], default='ST', + max_length=2)), ('is_finished', models.BooleanField(default=False)), ('has_replay', models.BooleanField(default=False, verbose_name='Replay')), ('has_command', models.BooleanField(default=False, verbose_name='Command')), @@ -71,7 +72,8 @@ class Migration(migrations.Migration): name='Task', fields=[ ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.CharField(choices=[('kill_session', 'Kill Session')], max_length=128, verbose_name='Name')), + ('name', + models.CharField(choices=[('kill_session', 'Kill Session')], max_length=128, verbose_name='Name')), ('args', models.CharField(max_length=1024, verbose_name='Args')), ('is_finished', models.BooleanField(default=False)), ('date_created', models.DateTimeField(auto_now_add=True)), @@ -87,8 +89,8 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=32, unique=True, verbose_name='Name')), ('remote_addr', models.CharField(max_length=128, verbose_name='Remote Address')), - ('ssh_port', models.IntegerField(default=2222, verbose_name='SSH Port')), - ('http_port', models.IntegerField(default=5000, verbose_name='HTTP Port')), + ('ssh_port', models.IntegerField(default=2222, verbose_name='SSH port')), + ('http_port', models.IntegerField(default=5000, verbose_name='HTTP port')), ('is_accepted', models.BooleanField(default=False, verbose_name='Is Accepted')), ('is_deleted', models.BooleanField(default=False)), ('date_created', models.DateTimeField(auto_now_add=True)), diff --git a/apps/terminal/migrations/0048_endpoint_endpointrule.py b/apps/terminal/migrations/0048_endpoint_endpointrule.py index 1ea7c3d03..1c1ef2d8c 100644 --- a/apps/terminal/migrations/0048_endpoint_endpointrule.py +++ b/apps/terminal/migrations/0048_endpoint_endpointrule.py @@ -82,13 +82,13 @@ class Migration(migrations.Migration): ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), ('name', models.CharField(max_length=128, unique=True, verbose_name='Name')), ('host', models.CharField(max_length=256, verbose_name='Host', blank=True)), - ('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS Port')), - ('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP Port')), - ('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH Port')), - ('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP Port')), - ('mysql_port', common.db.fields.PortField(default=33060, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL Port')), - ('mariadb_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB Port')), - ('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL Port')), + ('https_port', common.db.fields.PortField(default=443, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTPS port')), + ('http_port', common.db.fields.PortField(default=80, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='HTTP port')), + ('ssh_port', common.db.fields.PortField(default=2222, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='SSH port')), + ('rdp_port', common.db.fields.PortField(default=3389, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='RDP port')), + ('mysql_port', common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL port')), + ('mariadb_port', common.db.fields.PortField(default=33062, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB port')), + ('postgresql_port', common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL port')), ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), ], options={ diff --git a/apps/terminal/migrations/0049_endpoint_redis_port.py b/apps/terminal/migrations/0049_endpoint_redis_port.py index d1b1b38f1..aa4d722bc 100644 --- a/apps/terminal/migrations/0049_endpoint_redis_port.py +++ b/apps/terminal/migrations/0049_endpoint_redis_port.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name='endpoint', name='redis_port', - field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis Port'), + field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis port'), ), ] diff --git a/apps/terminal/migrations/0052_auto_20220713_1417.py b/apps/terminal/migrations/0052_auto_20220713_1417.py index c30032c23..52c5d907f 100644 --- a/apps/terminal/migrations/0052_auto_20220713_1417.py +++ b/apps/terminal/migrations/0052_auto_20220713_1417.py @@ -16,13 +16,13 @@ class Migration(migrations.Migration): name='oracle_11g_port', field=common.db.fields.PortField(default=15211, validators=[ django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g Port'), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 11g port'), ), migrations.AddField( model_name='endpoint', name='oracle_12c_port', field=common.db.fields.PortField(default=15212, validators=[ django.core.validators.MinValueValidator(0), - django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c Port'), + django.core.validators.MaxValueValidator(65535)], verbose_name='Oracle 12c port'), ), ] diff --git a/apps/terminal/migrations/0054_auto_20221027_1125.py b/apps/terminal/migrations/0054_auto_20221027_1125.py index 7f5527b58..4b101aea0 100644 --- a/apps/terminal/migrations/0054_auto_20221027_1125.py +++ b/apps/terminal/migrations/0054_auto_20221027_1125.py @@ -8,7 +8,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('assets', '0107_auto_20221019_1115'), + ('assets', '0101_auto_20220811_1511'), ('terminal', '0053_auto_20221009_1755'), ] @@ -32,84 +32,14 @@ class Migration(migrations.Migration): name='account', field=models.CharField(db_index=True, max_length=128, verbose_name='Account'), ), - migrations.CreateModel( - name='Applet', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), - ('display_name', models.CharField(max_length=128, verbose_name='Display name')), - ('version', models.CharField(max_length=16, verbose_name='Version')), - ('author', models.CharField(max_length=128, verbose_name='Author')), - ('type', - models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, - verbose_name='Type')), - ('is_active', models.BooleanField(default=True, verbose_name='Is active')), - ('protocols', models.JSONField(default=list, verbose_name='Protocol')), - ('tags', models.JSONField(default=list, verbose_name='Tags')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='AppletHost', - fields=[ - ('host_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='assets.host')), - ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), - ('status', models.CharField(max_length=16, verbose_name='Status')), - ], - options={ - 'abstract': False, - }, - bases=('assets.host',), - ), - migrations.CreateModel( - name='AppletPublication', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(default='', max_length=16, verbose_name='Status')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applet', - verbose_name='Applet')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='terminal.applethost', - verbose_name='Host')), - ], - options={ - 'unique_together': {('applet', 'host')}, - }, - ), - migrations.CreateModel( - name='AppletHostDeployment', - fields=[ - ('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')), - ('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')), - ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), - ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), - ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), - ('status', models.CharField(max_length=16, default='', verbose_name='Status')), - ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), - ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', - verbose_name='Hosting')), - ], - options={ - 'abstract': False, - }, + migrations.AddField( + model_name='session', + name='comment', + field=models.TextField(blank=True, null=True, verbose_name='Comment'), ), migrations.AddField( - model_name='applethost', - name='applets', - field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', - verbose_name='Applet'), + model_name='session', + name='type', + field=models.CharField(db_index=True, default='normal', max_length=16), ), ] diff --git a/apps/terminal/migrations/0055_auto_20221031_1848.py b/apps/terminal/migrations/0055_auto_20221031_1848.py deleted file mode 100644 index e36ed5a9b..000000000 --- a/apps/terminal/migrations/0055_auto_20221031_1848.py +++ /dev/null @@ -1,49 +0,0 @@ -# Generated by Django 3.2.14 on 2022-10-31 10:48 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0054_auto_20221027_1125'), - ] - - operations = [ - migrations.AddField( - model_name='applet', - name='hosts', - field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', verbose_name='Hosts'), - ), - migrations.AddField( - model_name='applethost', - name='date_inited', - field=models.DateTimeField(blank=True, null=True, verbose_name='Date inited'), - ), - migrations.AddField( - model_name='applethost', - name='inited', - field=models.BooleanField(default=False, verbose_name='Inited'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='date_finished', - field=models.DateTimeField(null=True, verbose_name='Date finished'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='date_start', - field=models.DateTimeField(db_index=True, null=True, verbose_name='Date start'), - ), - migrations.AlterField( - model_name='appletpublication', - name='applet', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applet', verbose_name='Applet'), - ), - migrations.AlterField( - model_name='appletpublication', - name='host', - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', to='terminal.applethost', verbose_name='Host'), - ), - ] diff --git a/apps/terminal/migrations/0064_auto_20221220_1956.py b/apps/terminal/migrations/0055_auto_20221228_1804.py similarity index 81% rename from apps/terminal/migrations/0064_auto_20221220_1956.py rename to apps/terminal/migrations/0055_auto_20221228_1804.py index 93b47dedc..126cbdbbf 100644 --- a/apps/terminal/migrations/0064_auto_20221220_1956.py +++ b/apps/terminal/migrations/0055_auto_20221228_1804.py @@ -1,15 +1,43 @@ -# Generated by Django 3.2.14 on 2022-12-20 11:56 +# Generated by Django 3.2.14 on 2022-12-28 10:04 from django.db import migrations, models +import django.db.models.deletion +import uuid class Migration(migrations.Migration): - dependencies = [ - ('terminal', '0063_applet_builtin'), + ('assets', '0105_auto_20221220_1956'), + ('terminal', '0054_auto_20221027_1125'), ] operations = [ + migrations.AlterModelOptions( + name='terminal', + options={'permissions': (('view_terminalconfig', 'Can view terminal config'),), 'verbose_name': 'Terminal'}, + ), + migrations.RenameField( + model_name='command', + old_name='system_user', + new_name='account', + ), + migrations.AlterField( + model_name='command', + name='account', + field=models.CharField(db_index=True, max_length=64, verbose_name='Account'), + ), + migrations.RemoveField( + model_name='terminal', + name='http_port', + ), + migrations.RemoveField( + model_name='terminal', + name='is_accepted', + ), + migrations.RemoveField( + model_name='terminal', + name='ssh_port', + ), migrations.AddField( model_name='commandstorage', name='updated_by', @@ -85,36 +113,6 @@ class Migration(migrations.Migration): name='updated_by', field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), ), - migrations.AlterField( - model_name='applet', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='applet', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='applethostdeployment', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='applethostdeployment', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), - migrations.AlterField( - model_name='appletpublication', - name='created_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by'), - ), - migrations.AlterField( - model_name='appletpublication', - name='updated_by', - field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by'), - ), migrations.AlterField( model_name='commandstorage', name='created_by', diff --git a/apps/terminal/migrations/0056_auto_20221101_1353.py b/apps/terminal/migrations/0056_auto_20221101_1353.py deleted file mode 100644 index 798420e2c..000000000 --- a/apps/terminal/migrations/0056_auto_20221101_1353.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-01 05:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0055_auto_20221031_1848'), - ] - - operations = [ - migrations.AddField( - model_name='applethost', - name='deploy_options', - field=models.JSONField(default=dict, verbose_name='Deploy options'), - ), - migrations.AddField( - model_name='applethostdeployment', - name='initial', - field=models.BooleanField(default=False, verbose_name='Initial'), - ), - ] diff --git a/apps/terminal/migrations/0056_auto_20221228_1808.py b/apps/terminal/migrations/0056_auto_20221228_1808.py new file mode 100644 index 000000000..c4b5b8a14 --- /dev/null +++ b/apps/terminal/migrations/0056_auto_20221228_1808.py @@ -0,0 +1,125 @@ +# Generated by Django 3.2.14 on 2022-12-28 10:08 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('assets', '0105_auto_20221220_1956'), + ('terminal', '0055_auto_20221228_1804'), + ] + + operations = [ + migrations.CreateModel( + name='Applet', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('name', models.SlugField(max_length=128, unique=True, verbose_name='Name')), + ('display_name', models.CharField(max_length=128, verbose_name='Display name')), + ('version', models.CharField(max_length=16, verbose_name='Version')), + ('author', models.CharField(max_length=128, verbose_name='Author')), + ('type', + models.CharField(choices=[('general', 'General'), ('web', 'Web')], default='general', max_length=16, + verbose_name='Type')), + ('is_active', models.BooleanField(default=True, verbose_name='Is active')), + ('builtin', models.BooleanField(default=False, verbose_name='Builtin')), + ('protocols', models.JSONField(default=list, verbose_name='Protocol')), + ('tags', models.JSONField(default=list, verbose_name='Tags')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ], + options={ + 'verbose_name': 'Applet', + }, + ), + migrations.CreateModel( + name='AppletHost', + fields=[ + ('host_ptr', + models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, + primary_key=True, serialize=False, to='assets.host')), + ('deploy_options', models.JSONField(default=dict, verbose_name='Deploy options')), + ('inited', models.BooleanField(default=False, verbose_name='Inited')), + ('date_inited', models.DateTimeField(blank=True, null=True, verbose_name='Date inited')), + ('date_synced', models.DateTimeField(blank=True, null=True, verbose_name='Date synced')), + ], + options={ + 'verbose_name': 'Applet host', + }, + bases=('assets.host',), + ), + migrations.CreateModel( + name='AppletPublication', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('applet', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', + to='terminal.applet', verbose_name='Applet')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='publications', + to='terminal.applethost', verbose_name='Host')), + ], + options={ + 'unique_together': {('applet', 'host')}, + }, + ), + migrations.CreateModel( + name='AppletHostDeployment', + fields=[ + ('created_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Created by')), + ('updated_by', models.CharField(blank=True, max_length=128, null=True, verbose_name='Updated by')), + ('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')), + ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), + ('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('initial', models.BooleanField(default=False, verbose_name='Initial')), + ('status', models.CharField(default='pending', max_length=16, verbose_name='Status')), + ('date_start', models.DateTimeField(db_index=True, null=True, verbose_name='Date start')), + ('date_finished', models.DateTimeField(null=True, verbose_name='Date finished')), + ('comment', models.TextField(blank=True, default='', verbose_name='Comment')), + ('task', models.UUIDField(null=True, verbose_name='Task')), + ('host', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='terminal.applethost', + verbose_name='Hosting')), + ], + options={ + 'ordering': ('-date_start',), + }, + ), + migrations.AddField( + model_name='applethost', + name='applets', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.Applet', + verbose_name='Applet'), + ), + migrations.AddField( + model_name='applethost', + name='terminal', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='applet_host', to='terminal.terminal', verbose_name='Terminal'), + ), + migrations.AddField( + model_name='applet', + name='hosts', + field=models.ManyToManyField(through='terminal.AppletPublication', to='terminal.AppletHost', + verbose_name='Hosts'), + ), + migrations.AlterField( + model_name='appletpublication', + name='applet', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applet', verbose_name='Applet'), + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applethost', verbose_name='Host'), + ), + ] diff --git a/apps/terminal/migrations/0057_auto_20221102_1941.py b/apps/terminal/migrations/0057_auto_20221102_1941.py deleted file mode 100644 index 56f1e699a..000000000 --- a/apps/terminal/migrations/0057_auto_20221102_1941.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-02 11:41 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0056_auto_20221101_1353'), - ] - - operations = [ - migrations.AddField( - model_name='applethost', - name='terminal', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='applet_host', to='terminal.terminal', verbose_name='Terminal'), - ), - migrations.AlterField( - model_name='appletpublication', - name='status', - field=models.CharField(default='ready', max_length=16, verbose_name='Status'), - ), - ] diff --git a/apps/terminal/migrations/0057_auto_20230109_1447.py b/apps/terminal/migrations/0057_auto_20230109_1447.py new file mode 100644 index 000000000..6bb719828 --- /dev/null +++ b/apps/terminal/migrations/0057_auto_20230109_1447.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.14 on 2023-01-09 06:47 + +import common.db.fields +import django.core.validators +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0056_auto_20221228_1808'), + ] + + operations = [ + migrations.AddField( + model_name='endpoint', + name='mariadb_port', + field=common.db.fields.PortField(default=33062, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MariaDB port'), + ), + migrations.AddField( + model_name='endpoint', + name='mysql_port', + field=common.db.fields.PortField(default=33061, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='MySQL port'), + ), + migrations.AddField( + model_name='endpoint', + name='oracle_port_range', + field=common.db.fields.PortRangeField(default='1-65535', max_length=16, verbose_name='Oracle port range'), + ), + migrations.AddField( + model_name='endpoint', + name='postgresql_port', + field=common.db.fields.PortField(default=54320, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='PostgreSQL port'), + ), + migrations.AddField( + model_name='endpoint', + name='redis_port', + field=common.db.fields.PortField(default=63790, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(65535)], verbose_name='Redis port'), + ), + ] diff --git a/apps/terminal/migrations/0058_auto_20221103_1624.py b/apps/terminal/migrations/0058_auto_20221103_1624.py deleted file mode 100644 index 0c8091e5c..000000000 --- a/apps/terminal/migrations/0058_auto_20221103_1624.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-03 08:24 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0057_auto_20221102_1941'), - ] - - operations = [ - migrations.AlterModelOptions( - name='terminal', - options={'permissions': (('view_terminalconfig', 'Can view terminal config'),), 'verbose_name': 'Terminal'}, - ), - migrations.RemoveField( - model_name='terminal', - name='http_port', - ), - migrations.RemoveField( - model_name='terminal', - name='is_accepted', - ), - migrations.RemoveField( - model_name='terminal', - name='ssh_port', - ), - migrations.RemoveField( - model_name='applethost', - name='status', - ), - ] diff --git a/apps/terminal/migrations/0058_auto_20230110_1445.py b/apps/terminal/migrations/0058_auto_20230110_1445.py new file mode 100644 index 000000000..1c6d87fbf --- /dev/null +++ b/apps/terminal/migrations/0058_auto_20230110_1445.py @@ -0,0 +1,27 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0057_auto_20230109_1447'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applethost', + options={'verbose_name': 'Hosting'}, + ), + migrations.RemoveField( + model_name='endpoint', + name='oracle_port_range', + ), + migrations.AlterField( + model_name='appletpublication', + name='host', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='publications', to='terminal.applethost', verbose_name='Hosting'), + ), + ] diff --git a/apps/terminal/migrations/0059_applethostdeployment_task.py b/apps/terminal/migrations/0059_applethostdeployment_task.py deleted file mode 100644 index 5f455c9c6..000000000 --- a/apps/terminal/migrations/0059_applethostdeployment_task.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-15 05:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0058_auto_20221103_1624'), - ] - - operations = [ - migrations.AddField( - model_name='applethostdeployment', - name='task', - field=models.UUIDField(null=True, verbose_name='Task'), - ), - ] diff --git a/apps/terminal/migrations/0060_alter_applethostdeployment_options.py b/apps/terminal/migrations/0060_alter_applethostdeployment_options.py deleted file mode 100644 index c38e2ba29..000000000 --- a/apps/terminal/migrations/0060_alter_applethostdeployment_options.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.2.14 on 2022-11-18 02:55 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0059_applethostdeployment_task'), - ] - - operations = [ - migrations.AlterModelOptions( - name='applethostdeployment', - options={'ordering': ('-date_start',)}, - ), - ] diff --git a/apps/terminal/migrations/0061_rename_system_user_command_account.py b/apps/terminal/migrations/0061_rename_system_user_command_account.py deleted file mode 100644 index ab7ee1f32..000000000 --- a/apps/terminal/migrations/0061_rename_system_user_command_account.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-05 05:16 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0060_alter_applethostdeployment_options'), - ] - - operations = [ - migrations.RenameField( - model_name='command', - old_name='system_user', - new_name='account', - ), - migrations.AlterField( - model_name='command', - name='account', - field=models.CharField(db_index=True, max_length=64, verbose_name='Account'), - ), - ] diff --git a/apps/terminal/migrations/0062_auto_20221216_1529.py b/apps/terminal/migrations/0062_auto_20221216_1529.py deleted file mode 100644 index 295cc8b55..000000000 --- a/apps/terminal/migrations/0062_auto_20221216_1529.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.14 on 2022-12-16 07:29 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0061_rename_system_user_command_account'), - ] - - operations = [ - migrations.AddField( - model_name='session', - name='comment', - field=models.TextField(blank=True, null=True, verbose_name='Comment'), - ), - migrations.AddField( - model_name='session', - name='type', - field=models.CharField(db_index=True, default='normal', max_length=16), - ), - ] diff --git a/apps/terminal/migrations/0065_auto_20221223_1536.py b/apps/terminal/migrations/0065_auto_20221223_1536.py deleted file mode 100644 index 23b070be8..000000000 --- a/apps/terminal/migrations/0065_auto_20221223_1536.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.16 on 2022-12-23 07:36 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('terminal', '0064_auto_20221220_1956'), - ] - - operations = [ - migrations.AlterModelOptions( - name='applet', - options={'verbose_name': 'Applet'}, - ), - migrations.AlterModelOptions( - name='applethost', - options={'verbose_name': 'Applet host'}, - ), - ] diff --git a/apps/terminal/models/applet/applet.py b/apps/terminal/models/applet/applet.py index b898cf2ed..6d0d0d3bf 100644 --- a/apps/terminal/models/applet/applet.py +++ b/apps/terminal/models/applet/applet.py @@ -1,5 +1,6 @@ import os.path import random +import shutil import yaml from django.conf import settings @@ -87,6 +88,11 @@ class Applet(JMSBaseModel): serializer = AppletSerializer(instance=instance, data=manifest) serializer.is_valid() serializer.save(builtin=True) + pkg_path = default_storage.path('applets/{}'.format(name)) + + if os.path.exists(pkg_path): + shutil.rmtree(pkg_path) + shutil.copytree(path, pkg_path) return instance def select_host_account(self): @@ -123,11 +129,11 @@ class Applet(JMSBaseModel): class AppletPublication(JMSBaseModel): - applet = models.ForeignKey('Applet', on_delete=models.PROTECT, related_name='publications', + applet = models.ForeignKey('Applet', on_delete=models.CASCADE, related_name='publications', verbose_name=_('Applet')) - host = models.ForeignKey('AppletHost', on_delete=models.PROTECT, related_name='publications', - verbose_name=_('Host')) - status = models.CharField(max_length=16, default='ready', verbose_name=_('Status')) + host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, related_name='publications', + verbose_name=_('Hosting')) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) class Meta: diff --git a/apps/terminal/models/applet/host.py b/apps/terminal/models/applet/host.py index 311fbe61a..37d15e64d 100644 --- a/apps/terminal/models/applet/host.py +++ b/apps/terminal/models/applet/host.py @@ -30,7 +30,7 @@ class AppletHost(Host): LOCKING_ORG = '00000000-0000-0000-0000-000000000004' class Meta: - verbose_name = _("Applet host") + verbose_name = _('Hosting') def __str__(self): return self.name @@ -104,7 +104,7 @@ class AppletHost(Host): class AppletHostDeployment(JMSBaseModel): host = models.ForeignKey('AppletHost', on_delete=models.CASCADE, verbose_name=_('Hosting')) initial = models.BooleanField(default=False, verbose_name=_('Initial')) - status = models.CharField(max_length=16, default='', verbose_name=_('Status')) + status = models.CharField(max_length=16, default='pending', verbose_name=_('Status')) date_start = models.DateTimeField(null=True, verbose_name=_('Date start'), db_index=True) date_finished = models.DateTimeField(null=True, verbose_name=_("Date finished")) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/terminal/models/component/endpoint.py b/apps/terminal/models/component/endpoint.py index 61d5e06d7..47d7a7894 100644 --- a/apps/terminal/models/component/endpoint.py +++ b/apps/terminal/models/component/endpoint.py @@ -12,10 +12,14 @@ class Endpoint(JMSBaseModel): name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) host = models.CharField(max_length=256, blank=True, verbose_name=_('Host')) # value=0 表示 disabled - https_port = PortField(default=443, verbose_name=_('HTTPS Port')) - http_port = PortField(default=80, verbose_name=_('HTTP Port')) - ssh_port = PortField(default=2222, verbose_name=_('SSH Port')) - rdp_port = PortField(default=3389, verbose_name=_('RDP Port')) + https_port = PortField(default=443, verbose_name=_('HTTPS port')) + http_port = PortField(default=80, verbose_name=_('HTTP port')) + ssh_port = PortField(default=2222, verbose_name=_('SSH port')) + rdp_port = PortField(default=3389, verbose_name=_('RDP port')) + mysql_port = PortField(default=33061, verbose_name=_('MySQL port')) + mariadb_port = PortField(default=33062, verbose_name=_('MariaDB port')) + postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL port')) + redis_port = PortField(default=63790, verbose_name=_('Redis port')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) @@ -30,12 +34,12 @@ class Endpoint(JMSBaseModel): def get_port(self, target_instance, protocol): from terminal.utils import db_port_manager - if protocol in ['https', 'http', 'ssh', 'rdp']: - port = getattr(self, f'{protocol}_port', 0) - elif isinstance(target_instance, Asset) and target_instance.category == 'dabase': + from assets.const import DatabaseTypes + if isinstance(target_instance, Asset) and \ + target_instance.is_type(DatabaseTypes.ORACLE): port = db_port_manager.get_port_by_db(target_instance) else: - port = 0 + port = getattr(self, f'{protocol}_port', 0) return port def is_default(self): diff --git a/apps/terminal/serializers/applet.py b/apps/terminal/serializers/applet.py index 35af7e07b..89fdf19b5 100644 --- a/apps/terminal/serializers/applet.py +++ b/apps/terminal/serializers/applet.py @@ -1,25 +1,20 @@ -from rest_framework import serializers from django.utils.translation import gettext_lazy as _ -from django.db import models +from rest_framework import serializers -from common.drf.fields import ObjectRelatedField, LabeledChoiceField +from common.const.choices import Status +from common.serializers.fields import ObjectRelatedField, LabeledChoiceField +from terminal.const import PublishStatus from ..models import Applet, AppletPublication, AppletHost - __all__ = [ 'AppletSerializer', 'AppletPublicationSerializer', ] class AppletPublicationSerializer(serializers.ModelSerializer): - class Status(models.TextChoices): - PUBLISHED = 'published', _('Published') - UNPUBLISHED = 'unpublished', _('Unpublished') - NOT_MATCH = 'not_match', _('Not match') - applet = ObjectRelatedField(attrs=('id', 'name', 'display_name', 'icon', 'version'), queryset=Applet.objects.all()) host = ObjectRelatedField(queryset=AppletHost.objects.all()) - status = LabeledChoiceField(choices=Status.choices, label=_("Status")) + status = LabeledChoiceField(choices=PublishStatus.choices, label=_("Status"), default=Status.pending) class Meta: model = AppletPublication diff --git a/apps/terminal/serializers/applet_host.py b/apps/terminal/serializers/applet_host.py index c81258892..9ec4b8104 100644 --- a/apps/terminal/serializers/applet_host.py +++ b/apps/terminal/serializers/applet_host.py @@ -1,9 +1,11 @@ from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from assets.models import Platform, Account +from accounts.models import Account +from assets.models import Platform from assets.serializers import HostSerializer -from common.drf.fields import LabeledChoiceField +from common.const.choices import Status +from common.serializers.fields import LabeledChoiceField from common.validators import ProjectUniqueValidator from .applet import AppletSerializer from .. import const @@ -46,6 +48,7 @@ class AppletHostSerializer(HostSerializer): 'load', 'date_synced', 'deploy_options' ] extra_kwargs = { + **HostSerializer.Meta.extra_kwargs, 'date_synced': {'read_only': True} } @@ -84,6 +87,8 @@ class HostAppletSerializer(AppletSerializer): class AppletHostDeploymentSerializer(serializers.ModelSerializer): + status = LabeledChoiceField(choices=Status.choices, label=_('Status'), default=Status.pending) + class Meta: model = AppletHostDeployment fields_mini = ['id', 'host', 'status', 'task'] diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index ce45ffa3d..57439c48d 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -1,22 +1,22 @@ -from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.drf.serializers import BulkModelSerializer -from acls.serializers.rules import ip_group_child_validator, ip_group_help_text -from ..utils import db_port_manager -from ..models import Endpoint, EndpointRule +from rest_framework import serializers +from acls.serializers.rules import ip_group_child_validator, ip_group_help_text +from common.serializers import BulkModelSerializer +from ..models import Endpoint, EndpointRule +from ..utils import db_port_manager __all__ = ['EndpointSerializer', 'EndpointRuleSerializer'] class EndpointSerializer(BulkModelSerializer): # 解决 luna 处理繁琐的问题, 返回 magnus 监听的当前 db 的 port - magnus_listen_db_port = serializers.SerializerMethodField(label=_('Magnus listen db port')) - magnus_listen_port_range = serializers.CharField( - max_length=128, default=db_port_manager.magnus_listen_port_range, read_only=True, - label=_('Magnus Listen port range'), + oracle_port = serializers.SerializerMethodField(label=_('Oracle port')) + oracle_port_range = serializers.CharField( + max_length=128, default=db_port_manager.oracle_port_range, read_only=True, + label=_('Oracle port range'), help_text=_( - 'The range of ports that Magnus listens on is modified in the configuration file' + 'Oracle proxy server listen port is dynamic, Each additional Oracle database instance adds a port listener' ) ) @@ -24,26 +24,33 @@ class EndpointSerializer(BulkModelSerializer): model = Endpoint fields_mini = ['id', 'name'] fields_small = [ - 'host', - 'https_port', 'http_port', 'ssh_port', 'rdp_port', - 'magnus_listen_db_port', 'magnus_listen_port_range', + 'host', 'https_port', 'http_port', 'ssh_port', 'rdp_port', + 'mysql_port', 'mariadb_port', 'postgresql_port', 'redis_port', + 'oracle_port_range', 'oracle_port', ] fields = fields_mini + fields_small + [ 'comment', 'date_created', 'date_updated', 'created_by' ] extra_kwargs = { - 'https_port': {'default': 443}, - 'http_port': {'default': 80}, - 'ssh_port': {'default': 2222}, - 'rdp_port': {'default': 3389}, + 'host': {'help_text': 'Visit IP/host, if empty, use the current request instead'}, } - def get_magnus_listen_db_port(self, obj: Endpoint): + def get_oracle_port(self, obj: Endpoint): view = self.context.get('view') if not view or view.action not in ['smart']: return 0 return obj.get_port(view.target_instance, view.target_protocol) + def get_extra_kwargs(self): + extra_kwargs = super().get_extra_kwargs() + model_fields = self.Meta.model._meta.fields + for field in model_fields: + if field.name.endswith('_port'): + kwargs = extra_kwargs.get(field.name, {}) + kwargs = {'default': field.default, **kwargs} + extra_kwargs[field.name] = kwargs + return extra_kwargs + class EndpointRuleSerializer(BulkModelSerializer): _ip_group_help_text = '{}
{}'.format( diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 184262ec4..b0494f42a 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from assets.const import Protocol -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Session @@ -22,7 +22,12 @@ class SessionType(models.TextChoices): class SessionSerializer(BulkOrgResourceModelSerializer): org_id = serializers.CharField(allow_blank=True) protocol = serializers.ChoiceField(choices=Protocol.choices, label=_("Protocol")) - type = LabeledChoiceField(choices=SessionType.choices, label=_("Type"), default=SessionType.normal) + type = LabeledChoiceField( + choices=SessionType.choices, label=_("Type"), default=SessionType.normal + ) + can_replay = serializers.BooleanField(read_only=True, label=_("Can replay")) + can_join = serializers.BooleanField(read_only=True, label=_("Can join")) + can_terminate = serializers.BooleanField(read_only=True, label=_("Can terminate")) class Meta: model = Session diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index cc4478c2d..8bc66fd5a 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -5,9 +5,9 @@ from rest_framework.validators import UniqueValidator from urllib.parse import urlparse from django.utils.translation import ugettext_lazy as _ from django.db.models import TextChoices - -from common.drf.serializers import MethodSerializer -from common.drf.fields import ReadableHiddenField, EncryptedField +from common.serializers.fields import LabeledChoiceField +from common.serializers import MethodSerializer +from common.serializers.fields import ReadableHiddenField, EncryptedField from ..models import ReplayStorage, CommandStorage from .. import const @@ -178,6 +178,7 @@ command_storage_type_serializer_classes_mapping = { # BaseStorageSerializer class BaseStorageSerializer(serializers.ModelSerializer): + type = LabeledChoiceField(choices=const.ReplayStorageType.choices, label=_('Type')) storage_type_serializer_classes_mapping = {} meta = MethodSerializer() diff --git a/apps/terminal/serializers/terminal.py b/apps/terminal/serializers/terminal.py index 340a0b0f7..83ab1605b 100644 --- a/apps/terminal/serializers/terminal.py +++ b/apps/terminal/serializers/terminal.py @@ -1,8 +1,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField -from common.drf.serializers import BulkModelSerializer +from common.serializers.fields import LabeledChoiceField +from common.serializers import BulkModelSerializer from common.utils import get_request_ip, pretty_string, is_uuid from users.serializers import ServiceAccountSerializer from .. import const diff --git a/apps/terminal/signal_handlers/__init__.py b/apps/terminal/signal_handlers/__init__.py new file mode 100644 index 000000000..265f6416d --- /dev/null +++ b/apps/terminal/signal_handlers/__init__.py @@ -0,0 +1,3 @@ +from .applet import * +from .db_port import * +from .terminal import * diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers/applet.py similarity index 64% rename from apps/terminal/signal_handlers.py rename to apps/terminal/signal_handlers/applet.py index 046c1b122..18595fc7c 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers/applet.py @@ -1,21 +1,15 @@ -# -*- coding: utf-8 -*- -# - from django.db.models.signals import post_save, post_delete -from django.db.utils import ProgrammingError from django.dispatch import receiver from django.utils.functional import LazyObject -from assets.models import Asset from common.signals import django_ready from common.utils import get_logger from common.utils.connection import RedisPubSub from orgs.utils import tmp_to_builtin_org -from .models import Applet, AppletHost -from .utils import db_port_manager, DBPortManager +from ..models import Applet, AppletHost +from ..utils import DBPortManager db_port_manager: DBPortManager - logger = get_logger(__file__) @@ -31,6 +25,11 @@ def on_applet_host_create(sender, instance, created=False, **kwargs): applet_host_change_pub_sub.publish(True) +@receiver(post_delete, sender=AppletHost) +def on_applet_host_delete(sender, instance, **kwargs): + applet_host_change_pub_sub.publish(True) + + @receiver(post_save, sender=Applet) def on_applet_create(sender, instance, created=False, **kwargs): if not created: @@ -41,29 +40,9 @@ def on_applet_create(sender, instance, created=False, **kwargs): applet_host_change_pub_sub.publish(True) -@receiver(django_ready) -def check_db_port_mapper(sender, **kwargs): - logger.info('Init db port mapper') - try: - db_port_manager.check() - except (ProgrammingError,) as e: - pass - - -@receiver(post_save, sender=Asset) -def on_db_app_created(sender, instance: Asset, created, **kwargs): - if not instance.category != 'database': - return - if not created: - return - db_port_manager.add(instance) - - -@receiver(post_delete, sender=Asset) -def on_db_app_delete(sender, instance, **kwargs): - if not instance.category != 'database': - return - db_port_manager.pop(instance) +@receiver(post_delete, sender=Applet) +def on_applet_delete(sender, instance, **kwargs): + applet_host_change_pub_sub.publish(True) class AppletHostPubSub(LazyObject): diff --git a/apps/terminal/signal_handlers/db_port.py b/apps/terminal/signal_handlers/db_port.py new file mode 100644 index 000000000..9ee60257c --- /dev/null +++ b/apps/terminal/signal_handlers/db_port.py @@ -0,0 +1,39 @@ +from django.db.models.signals import post_save, post_delete +from django.dispatch import receiver + +from assets.models import Asset +from common.decorator import on_transaction_commit +from common.signals import django_ready +from common.utils import get_logger +from ..utils import db_port_manager + +logger = get_logger(__file__) + + +@receiver(django_ready) +def check_db_port_mapper(sender, **kwargs): + logger.info('Check oracle ports') + try: + db_port_manager.check() + except Exception as e: + pass + + +@receiver(post_save, sender=Asset) +@on_transaction_commit +def on_db_created(sender, instance: Asset, created, **kwargs): + if instance.type != 'oracle': + return + if not created: + return + logger.info("Oracle create signal recv: {} {}".format(instance, instance.type)) + db_port_manager.check() + + +@receiver(post_delete, sender=Asset) +@on_transaction_commit +def on_db_delete(sender, instance, **kwargs): + if instance.type != 'oracle': + return + logger.info("Oracle delete signal recv: {}".format(instance)) + db_port_manager.check() diff --git a/apps/terminal/signal_handlers/terminal.py b/apps/terminal/signal_handlers/terminal.py new file mode 100644 index 000000000..10d87b989 --- /dev/null +++ b/apps/terminal/signal_handlers/terminal.py @@ -0,0 +1,35 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.functional import LazyObject + +from common.decorator import on_transaction_commit +from common.utils import get_logger +from common.utils.connection import RedisPubSub +from ..models import Task +from ..utils import DBPortManager + +db_port_manager: DBPortManager + +logger = get_logger(__file__) + + +class ComponentEventChan(LazyObject): + def _setup(self): + self._wrapped = RedisPubSub('fm.component_event_chan') + + +component_event_chan = ComponentEventChan() + + +@receiver(post_save, sender=Task) +@on_transaction_commit +def on_task_created(sender, instance: Task, created, **kwargs): + if not created: + return + event = { + "type": instance.name, + "payload": { + "id": str(instance.id), + }, + } + component_event_chan.publish(event) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 5d3c373d2..c13892346 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -53,8 +53,4 @@ urlpatterns = [ path('components/connect-methods/', api.ConnectMethodListApi.as_view(), name='connect-methods'), ] -old_version_urlpatterns = [ - re_path('(?Pterminal|command)/.*', capi.redirect_plural_name_api) -] - -urlpatterns += router.urls + old_version_urlpatterns +urlpatterns += router.urls diff --git a/apps/terminal/urls/ws_urls.py b/apps/terminal/urls/ws_urls.py new file mode 100644 index 000000000..e779534ec --- /dev/null +++ b/apps/terminal/urls/ws_urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .. import ws + +app_name = 'terminal' + +urlpatterns = [ + path('ws/terminal-task/', ws.TerminalTaskWebsocket.as_asgi(), name='terminal-task-ws'), +] diff --git a/apps/terminal/utils/db_port_mapper.py b/apps/terminal/utils/db_port_mapper.py index 4ca8cebec..6f8457d22 100644 --- a/apps/terminal/utils/db_port_mapper.py +++ b/apps/terminal/utils/db_port_mapper.py @@ -2,8 +2,8 @@ from django.conf import settings from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ -from assets.const import Category -from assets.models import Asset +from assets.const import DatabaseTypes +from assets.models import Database from common.decorator import Singleton from common.exceptions import JMSException from common.utils import get_logger, get_object_or_none @@ -15,57 +15,64 @@ logger = get_logger(__file__) @Singleton class DBPortManager(object): """ 管理端口-数据库ID的映射, Magnus 要使用 """ - CACHE_KEY = 'PORT_DB_MAPPER' def __init__(self): + oracle_ports = self.oracle_port_range try: - port_start, port_end = settings.MAGNUS_PORTS.split('-') + port_start, port_end = oracle_ports.split('-') port_start, port_end = int(port_start), int(port_end) except Exception as e: - logger.error('MAGNUS_PORTS config error: {}'.format(e)) + logger.error('MAGNUS_ORACLE_PORTS config error: {}'.format(e)) port_start, port_end = 30000, 30100 self.port_start, self.port_end = port_start, port_end # 可以使用的端口列表 - self.all_available_ports = list(range(self.port_start, self.port_end + 1)) + self.all_avail_ports = list(range(self.port_start, self.port_end + 1)) @property - def magnus_listen_port_range(self): - return settings.MAGNUS_PORTS + def oracle_port_range(self): + oracle_ports = settings.MAGNUS_ORACLE_PORTS + if not oracle_ports and settings.MAGNUS_PORTS: + oracle_ports = settings.MAGNUS_PORTS + return oracle_ports @staticmethod def fetch_dbs(): with tmp_to_root_org(): - dbs = Asset.objects.filter(platform__category=Category.DATABASE).order_by('id') + dbs = Database.objects.filter(platform__type=DatabaseTypes.ORACLE).order_by('id') return dbs def check(self): dbs = self.fetch_dbs() - for db in dbs: - port = self.get_port_by_db(db, raise_exception=False) - if not port: - self.add(db) + mapper = self.get_mapper() + db_ids = [str(db.id) for db in dbs] + db_ids_to_add = list(set(db_ids) - set(mapper.values())) + mapper = self.bulk_add(db_ids_to_add, mapper) + + db_ids_to_pop = set(mapper.values()) - set(db_ids) + mapper = self.bulk_pop(db_ids_to_pop, mapper) + self.set_mapper(mapper) + + if settings.DEBUG: + logger.debug("Oracle listen ports: {}".format(len(mapper.keys()))) def init(self): dbs = self.fetch_dbs() db_ids = dbs.values_list('id', flat=True) db_ids = [str(i) for i in db_ids] - mapper = dict(zip(self.all_available_ports, list(db_ids))) + mapper = dict(zip(self.all_avail_ports, list(db_ids))) self.set_mapper(mapper) - def add(self, db: Asset): - mapper = self.get_mapper() - available_port = self.get_next_available_port() - mapper.update({available_port: str(db.id)}) - self.set_mapper(mapper) - return True + def bulk_add(self, db_ids, mapper): + for db_id in db_ids: + avail_port = self.get_next_avail_port(mapper) + mapper[avail_port] = str(db_id) + return mapper - def pop(self, db: Asset): - mapper = self.get_mapper() - to_delete_port = self.get_port_by_db(db, raise_exception=False) - mapper.pop(to_delete_port, None) - self.set_mapper(mapper) + def bulk_pop(self, db_ids, mapper): + new_mapper = {port: str(db_id) for port, db_id in mapper.items() if db_id not in db_ids} + return new_mapper def get_port_by_db(self, db, raise_exception=True): mapper = self.get_mapper() @@ -91,29 +98,31 @@ class DBPortManager(object): if not db_id: raise JMSException('Database not in port-db mapper, port: {}'.format(port)) with tmp_to_root_org(): - db = get_object_or_none(Asset, id=db_id) + db = get_object_or_none(Database, id=db_id) if not db: raise JMSException('Database not exists, db id: {}'.format(db_id)) return db - def get_next_available_port(self): - already_use_ports = self.get_already_use_ports() - available_ports = sorted(list(set(self.all_available_ports) - set(already_use_ports))) - if len(available_ports) <= 0: + def get_next_avail_port(self, mapper=None): + if mapper is None: + mapper = self.get_mapper() + already_use_ports = [int(i) for i in mapper.keys()] + avail_ports = sorted(list(set(self.all_avail_ports) - set(already_use_ports))) + if len(avail_ports) <= 0: msg = _('No ports can be used, check and modify the limit on the number ' 'of ports that Magnus listens on in the configuration file.') tips = _('All available port count: {}, Already use port count: {}').format( - len(self.all_available_ports), len(already_use_ports) + len(self.all_avail_ports), len(already_use_ports) ) error = msg + tips raise JMSException(error) - port = available_ports[0] + port = avail_ports[0] logger.debug('Get next available port: {}'.format(port)) return port def get_already_use_ports(self): mapper = self.get_mapper() - return sorted(list(mapper.keys())) + return sorted([int(i) for i in mapper.keys()]) def get_mapper(self): mapper = cache.get(self.CACHE_KEY, {}) diff --git a/apps/terminal/ws.py b/apps/terminal/ws.py new file mode 100644 index 000000000..c414ecfcf --- /dev/null +++ b/apps/terminal/ws.py @@ -0,0 +1,79 @@ +import datetime + +from channels.generic.websocket import JsonWebsocketConsumer +from django.utils import timezone +from rest_framework.renderers import JSONRenderer + +from common.db.utils import safe_db_connection +from common.utils import get_logger +from common.utils.connection import Subscription +from terminal.models import Terminal +from terminal.models import Session +from terminal.serializers import TaskSerializer, StatSerializer +from .signal_handlers import component_event_chan + +logger = get_logger(__name__) + + +class TerminalTaskWebsocket(JsonWebsocketConsumer): + sub: Subscription = None + terminal: Terminal = None + + def connect(self): + user = self.scope["user"] + if user.is_authenticated and user.terminal: + self.accept() + self.terminal = user.terminal + self.sub = self.watch_component_event() + else: + self.close() + + def receive_json(self, content, **kwargs): + req_type = content.get('type') + if req_type == "status": + payload = content.get('payload') + self.handle_status(payload) + + def handle_status(self, content): + serializer = StatSerializer(data=content) + if not serializer.is_valid(): + logger.error('Invalid status data: {}'.format(serializer.errors)) + return + serializer.validated_data["terminal"] = self.terminal + session_ids = serializer.validated_data.pop('sessions', []) + Session.set_sessions_active(session_ids) + with safe_db_connection(): + serializer.save() + + def send_kill_tasks_msg(self, task_id=None): + content = self.get_terminal_tasks(task_id) + self.send(bytes_data=content) + + def get_terminal_tasks(self, task_id=None): + with safe_db_connection(): + critical_time = timezone.now() - datetime.timedelta(minutes=10) + tasks = self.terminal.task_set.filter(is_finished=False, date_created__gte=critical_time) + if task_id: + tasks = tasks.filter(id=task_id) + serializer = TaskSerializer(tasks, many=True) + return JSONRenderer().render(serializer.data) + + def watch_component_event(self): + # 先发一次已有的任务 + self.send_kill_tasks_msg() + + ws = self + + def handle_task_msg_recv(msg): + logger.debug('New component task msg recv: {}'.format(msg)) + msg_type = msg.get('type') + payload = msg.get('payload') + if msg_type == "kill_session": + ws.send_kill_tasks_msg(payload.get('id')) + + return component_event_chan.subscribe(handle_task_msg_recv) + + def disconnect(self, code): + if self.sub is None: + return + self.sub.unsubscribe() diff --git a/apps/tickets/api/flow.py b/apps/tickets/api/flow.py index 303af6d3f..b11b1bb70 100644 --- a/apps/tickets/api/flow.py +++ b/apps/tickets/api/flow.py @@ -2,7 +2,7 @@ from rest_framework.exceptions import MethodNotAllowed from tickets import serializers from tickets.models import TicketFlow -from common.drf.api import JMSBulkModelViewSet +from common.api import JMSBulkModelViewSet __all__ = ['TicketFlowViewSet'] diff --git a/apps/tickets/api/relation.py b/apps/tickets/api/relation.py index c442c04ab..2f12437cb 100644 --- a/apps/tickets/api/relation.py +++ b/apps/tickets/api/relation.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from rest_framework.mixins import CreateModelMixin from orgs.utils import tmp_to_root_org -from common.drf.api import JMSGenericViewSet +from common.api import JMSGenericViewSet from terminal.serializers import SessionSerializer from tickets.models import TicketSession from tickets.serializers import TicketSessionRelationSerializer diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index a402e23a1..19bffa952 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -7,7 +7,7 @@ from rest_framework.exceptions import MethodNotAllowed from orgs.utils import tmp_to_root_org from rbac.permissions import RBACPermission -from common.mixins.api import CommonApiMixin +from common.api import CommonApiMixin from common.const.http import POST, PUT, PATCH from tickets import filters from tickets import serializers @@ -86,7 +86,7 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): instance.reject(processor=request.user) return Response('ok') - @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ]) + @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) def close(self, request, *args, **kwargs): instance = self.get_object() instance.close() diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py index 40fb314e3..e71d4297a 100644 --- a/apps/tickets/handlers/base.py +++ b/apps/tickets/handlers/base.py @@ -98,7 +98,7 @@ class BaseHandler: approve_info = _('{} {} the ticket').format(user_display, state_display) context = self._diff_prev_approve_context(state) context.update({'approve_info': approve_info}) - body = self.reject_html_script( + body = self.safe_html_script( render_to_string('tickets/ticket_approve_diff.html', context) ) data = { @@ -111,6 +111,6 @@ class BaseHandler: return self.ticket.comments.create(**data) @staticmethod - def reject_html_script(unsafe_html): + def safe_html_script(unsafe_html): safe_html = escape(unsafe_html) return safe_html diff --git a/apps/tickets/handlers/login_asset_confirm.py b/apps/tickets/handlers/login_asset_confirm.py index 16f156d4d..fa989e8a0 100644 --- a/apps/tickets/handlers/login_asset_confirm.py +++ b/apps/tickets/handlers/login_asset_confirm.py @@ -4,3 +4,9 @@ from .base import BaseHandler class Handler(BaseHandler): ticket: ApplyLoginAssetTicket + + def _on_step_approved(self, step): + is_finished = super()._on_step_approved(step) + if is_finished: + self.ticket.activate_connection_token_if_need() + return is_finished diff --git a/apps/tickets/migrations/0028_remove_app_tickets.py b/apps/tickets/migrations/0028_remove_app_tickets.py new file mode 100644 index 000000000..b396d6c2a --- /dev/null +++ b/apps/tickets/migrations/0028_remove_app_tickets.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-12-28 09:46 + +from django.db import migrations + + +def migrate_remove_app_tickets(apps, *args): + model = apps.get_model('tickets', 'Ticket') + model.objects.filter(type='apply_application').delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('tickets', '0027_alter_applycommandticket_apply_run_account'), + ] + + operations = [ + migrations.RunPython(migrate_remove_app_tickets) + ] diff --git a/apps/tickets/migrations/0029_auto_20230110_1445.py b/apps/tickets/migrations/0029_auto_20230110_1445.py new file mode 100644 index 000000000..a6757b534 --- /dev/null +++ b/apps/tickets/migrations/0029_auto_20230110_1445.py @@ -0,0 +1,28 @@ +# Generated by Django 3.2.16 on 2023-01-10 06:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0110_alter_favoriteasset_options'), + ('tickets', '0028_remove_app_tickets'), + ] + + operations = [ + migrations.AlterModelOptions( + name='applyassetticket', + options={'verbose_name': 'Apply Asset Ticket'}, + ), + migrations.AlterField( + model_name='applyassetticket', + name='apply_assets', + field=models.ManyToManyField(to='assets.Asset', verbose_name='Asset'), + ), + migrations.AlterField( + model_name='applyassetticket', + name='apply_nodes', + field=models.ManyToManyField(to='assets.Node', verbose_name='Node'), + ), + ] diff --git a/apps/tickets/models/comment.py b/apps/tickets/models/comment.py index 638ecdc4d..6577b65e2 100644 --- a/apps/tickets/models/comment.py +++ b/apps/tickets/models/comment.py @@ -33,3 +33,6 @@ class Comment(JMSBaseModel): def set_display_fields(self): self.user_display = str(self.user) + + def __str__(self): + return str(self.ticket) diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py index 2fde56125..ef29c607f 100644 --- a/apps/tickets/models/ticket/apply_asset.py +++ b/apps/tickets/models/ticket/apply_asset.py @@ -11,9 +11,9 @@ asset_or_node_help_text = _("Select at least one asset or node") class ApplyAssetTicket(Ticket): apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name')) - apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes')) + apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Node')) # 申请信息 - apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) + apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Asset')) apply_accounts = models.JSONField(default=list, verbose_name=_('Apply accounts')) apply_actions = models.IntegerField(verbose_name=_('Actions'), default=ActionChoices.all()) apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True) @@ -21,3 +21,6 @@ class ApplyAssetTicket(Ticket): def get_apply_actions_display(self): return ActionChoices.display(self.apply_actions) + + class Meta: + verbose_name = _('Apply Asset Ticket') diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index 863a1508f..11a1db9dc 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -14,6 +14,7 @@ from common.db.encoder import ModelJSONFieldEncoder from common.db.models import JMSBaseModel from common.exceptions import JMSException from common.utils.timezone import as_current_tz +from common.utils import reverse from orgs.models import Organization from orgs.utils import tmp_to_org from tickets.const import ( @@ -250,9 +251,8 @@ class StatusMixin: @property def processor(self): - processor = self.current_step.ticket_assignees \ - .exclude(state=StepState.pending).first() - return processor.assignee if processor else None + """ 返回最后一步的处理人 """ + return self.current_step.processor def has_current_assignee(self, assignee): return self.ticket_steps.filter( @@ -418,6 +418,39 @@ class Ticket(StatusMixin, JMSBaseModel): snapshot[field.verbose_name] = value return snapshot + def get_extra_info_of_review(self, user=None): + if user and user.is_service_account: + url_ticket_status = reverse( + view_name='api-tickets:super-ticket-status', kwargs={'pk': str(self.id)} + ) + check_ticket_api = {'method': 'GET', 'url': url_ticket_status} + close_ticket_api = {'method': 'DELETE', 'url': url_ticket_status} + else: + url_ticket_status = reverse( + view_name='api-tickets:ticket-detail', kwargs={'pk': str(self.id)} + ) + url_ticket_close = reverse( + view_name='api-tickets:ticket-close', kwargs={'pk': str(self.id)} + ) + check_ticket_api = {'method': 'GET', 'url': url_ticket_status} + close_ticket_api = {'method': 'PUT', 'url': url_ticket_close} + + url_ticket_detail_external = reverse( + view_name='api-tickets:ticket-detail', + kwargs={'pk': str(self.id)}, + external=True, + api_to_ui=True + ) + ticket_assignees = self.current_step.ticket_assignees.all() + return { + 'check_ticket_api': check_ticket_api, + 'close_ticket_api': close_ticket_api, + 'ticket_detail_page_url': '{url}?type={type}'.format( + url=url_ticket_detail_external, type=self.type + ), + 'assignees': [str(ticket_assignee.assignee) for ticket_assignee in ticket_assignees] + } + class SuperTicket(Ticket): class Meta: diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py index 8761bc7fe..ffcbf869e 100644 --- a/apps/tickets/models/ticket/login_asset_confirm.py +++ b/apps/tickets/models/ticket/login_asset_confirm.py @@ -16,3 +16,9 @@ class ApplyLoginAssetTicket(Ticket): apply_login_account = models.CharField( max_length=128, default='', verbose_name=_('Login account') ) + + def activate_connection_token_if_need(self): + if not self.connection_token: + return + self.connection_token.is_active = True + self.connection_token.save() diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 6fa9fa567..09ce04232 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from common.drf.fields import ReadableHiddenField +from common.serializers.fields import ReadableHiddenField from ..models import Comment __all__ = ['CommentSerializer'] @@ -24,7 +24,7 @@ class CommentSerializer(serializers.ModelSerializer): model = Comment fields_mini = ['id'] fields_small = fields_mini + [ - 'body', 'user_display', 'date_created', 'date_updated' + 'body', 'user_display', 'date_created', 'date_updated' ] fields_fk = ['ticket', 'user', ] fields = fields_small + fields_fk diff --git a/apps/tickets/serializers/flow.py b/apps/tickets/serializers/flow.py index f48e16501..f3b9c0215 100644 --- a/apps/tickets/serializers/flow.py +++ b/apps/tickets/serializers/flow.py @@ -2,11 +2,10 @@ from rest_framework import serializers from django.db.transaction import atomic from django.utils.translation import ugettext_lazy as _ - from orgs.models import Organization from orgs.utils import get_current_org_id from orgs.mixins.serializers import OrgResourceModelSerializerMixin -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from tickets.models import TicketFlow, ApprovalRule from tickets.const import TicketApprovalStrategy, TicketType diff --git a/apps/tickets/serializers/super_ticket.py b/apps/tickets/serializers/super_ticket.py index 9200c3f28..12bb911b0 100644 --- a/apps/tickets/serializers/super_ticket.py +++ b/apps/tickets/serializers/super_ticket.py @@ -1,5 +1,7 @@ from rest_framework import serializers from django.utils.translation import gettext_lazy as _ +from common.serializers.fields import LabeledChoiceField +from tickets.const import TicketStatus, TicketState from ..models import SuperTicket @@ -8,6 +10,8 @@ __all__ = ['SuperTicketSerializer'] class SuperTicketSerializer(serializers.ModelSerializer): + status = LabeledChoiceField(choices=TicketStatus.choices, read_only=True, label=_('Status')) + state = LabeledChoiceField(choices=TicketState.choices, read_only=True, label=_("State")) processor = serializers.SerializerMethodField(label=_("Processor")) class Meta: @@ -16,6 +20,4 @@ class SuperTicketSerializer(serializers.ModelSerializer): @staticmethod def get_processor(instance): - if not instance.processor: - return '' - return str(instance.processor) + return str(instance.processor) if instance.processor else '' diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index 8bf7b8563..97c42f331 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import Asset, Node from perms.models import AssetPermission from perms.serializers.permission import ActionChoicesField -from common.drf.fields import ObjectRelatedField +from common.serializers.fields import ObjectRelatedField from tickets.models import ApplyAssetTicket from .common import BaseApplyAssetSerializer from .ticket import TicketApplySerializer diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index d21c44ec0..334031e5e 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import LabeledChoiceField +from common.serializers.fields import LabeledChoiceField from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.models import Organization from tickets.const import TicketType, TicketStatus, TicketState @@ -29,9 +29,7 @@ class TicketSerializer(OrgResourceModelSerializerMixin): 'status', 'date_created', 'date_updated', 'org_name', 'rel_snapshot' ] fields = fields_small + read_only_fields - extra_kwargs = { - 'type': {'required': True} - } + extra_kwargs = {} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -85,15 +83,10 @@ class TicketApplySerializer(TicketSerializer): ticket_type = attrs.get('type') org_id = attrs.get('org_id') - flow = TicketFlow.get_org_related_flows(org_id=org_id) \ - .filter(type=ticket_type).first() - - if flow: - attrs['flow'] = flow - return attrs - else: + flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() + if not flow: error = _('The ticket flow `{}` does not exist'.format(ticket_type)) raise serializers.ValidationError(error) - + attrs['flow'] = flow attrs['applicant'] = self.get_applicant(attrs.get('applicant')) return attrs diff --git a/apps/users/api/relation.py b/apps/users/api/relation.py index 23260a5ae..5af72633c 100644 --- a/apps/users/api/relation.py +++ b/apps/users/api/relation.py @@ -3,7 +3,7 @@ from django.db.models import F -from common.drf.api import JMSBulkRelationModelViewSet +from common.api import JMSBulkRelationModelViewSet from .. import serializers from ..models import User, UserGroup diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 70939f51f..c882719f6 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -7,9 +7,9 @@ from rest_framework import generics from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet -from common.mixins import CommonApiMixin +from common.api import CommonApiMixin from common.utils import get_logger -from common.mixins.api import SuggestionMixin +from common.api import SuggestionMixin from orgs.utils import current_org, tmp_to_root_org from rbac.models import Role, RoleBinding from users.utils import LoginBlockUtil, MFABlockUtils diff --git a/apps/users/serializers/profile.py b/apps/users/serializers/profile.py index c8dfb7782..287454146 100644 --- a/apps/users/serializers/profile.py +++ b/apps/users/serializers/profile.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.utils import validate_ssh_public_key -from common.drf.fields import EncryptedField +from common.serializers.fields import EncryptedField from ..models import User from .user import UserSerializer diff --git a/apps/users/serializers/user.py b/apps/users/serializers/user.py index 1d99be5ac..7baa4117b 100644 --- a/apps/users/serializers/user.py +++ b/apps/users/serializers/user.py @@ -5,8 +5,8 @@ from functools import partial from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField -from common.drf.serializers import CommonBulkSerializerMixin +from common.serializers.fields import EncryptedField, ObjectRelatedField, LabeledChoiceField +from common.serializers import CommonBulkSerializerMixin from common.utils import pretty_string, get_logger from common.validators import PhoneValidator from rbac.builtin import BuiltinRole diff --git a/apps/users/views/profile/mfa.py b/apps/users/views/profile/mfa.py index 432dba5c8..10f289ab8 100644 --- a/apps/users/views/profile/mfa.py +++ b/apps/users/views/profile/mfa.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals from django.views.generic.base import TemplateView from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from users.models import User __all__ = ['MFASettingView'] @@ -21,4 +21,3 @@ class MFASettingView(PermissionsMixin, TemplateView): 'mfa_backends': mfa_backends, }) return context - diff --git a/apps/users/views/profile/otp.py b/apps/users/views/profile/otp.py index fec3055e6..5b4333d37 100644 --- a/apps/users/views/profile/otp.py +++ b/apps/users/views/profile/otp.py @@ -13,7 +13,7 @@ from authentication.mixins import AuthMixin from authentication.mfa import MFAOtp, otp_failed_msg from authentication.errors import SessionEmptyError from common.utils import get_logger, FlashMessageUtil -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin from common.permissions import IsValidUser from .password import UserVerifyPasswordView from ... import forms @@ -168,5 +168,3 @@ class UserOtpDisableView(PermissionsMixin, FormView): } url = FlashMessageUtil.gen_message_url(message_data) return url - - diff --git a/apps/users/views/profile/pubkey.py b/apps/users/views/profile/pubkey.py index 7408889e1..f8a038e50 100644 --- a/apps/users/views/profile/pubkey.py +++ b/apps/users/views/profile/pubkey.py @@ -5,7 +5,7 @@ from django.views import View from common.utils import get_logger, ssh_key_gen from common.permissions import IsValidUser -from common.mixins.views import PermissionsMixin +from common.views.mixins import PermissionsMixin __all__ = ['UserPublicKeyGenerateView'] diff --git a/generateV3Data.py b/generateV3Data.py deleted file mode 100644 index 673037e99..000000000 --- a/generateV3Data.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python -# - -# >>> Django 环境配置 -import django -import os -import sys - -if os.path.exists('../apps'): - sys.path.insert(0, '../apps') -elif os.path.exists('./apps'): - sys.path.insert(0, './apps') - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -APPS_DIR = os.path.join(BASE_DIR, 'apps') -sys.path.insert(0, APPS_DIR) - -os.environ.setdefault('PYTHONOPTIMIZE', '1') -if os.getuid() == 0: - os.environ.setdefault('C_FORCE_ROOT', '1') - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings") -django.setup() - -# <<< - - -class Generator(object): - - def generate(self): - pass - - def generate_assets(self): - pass - - -if __name__ == '__main__': - pass