From 83c5344307090883023b3e7da67337ac6de3ba71 Mon Sep 17 00:00:00 2001 From: feng626 <1304903146@qq.com> Date: Wed, 19 Jan 2022 17:48:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=8E=B7=E5=8F=96=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A7=84=E5=88=99=E8=BF=87=E6=BB=A4=E4=B8=8D=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/assets/api/system_user.py | 34 ++------- apps/assets/models/cmd_filter.py | 49 ++++++++++-- apps/locale/zh/LC_MESSAGES/django.mo | 4 +- apps/locale/zh/LC_MESSAGES/django.po | 110 ++++++++++++++------------- apps/ops/models/command.py | 50 ++++++++++-- 5 files changed, 155 insertions(+), 92 deletions(-) diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index cf61be658..9e5ca0f19 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -196,41 +196,21 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView): return CommandFilterRuleSerializer def get_queryset(self): - user_groups = [] user_id = self.request.query_params.get('user_id') - user = get_object_or_none(User, pk=user_id) - if user: - user_groups.extend(list(user.groups.all())) user_group_id = self.request.query_params.get('user_group_id') - user_group = get_object_or_none(UserGroup, pk=user_group_id) - if user_group: - user_groups.append(user_group) system_user_id = self.kwargs.get('pk', None) system_user = get_object_or_none(SystemUser, pk=system_user_id) if not system_user: system_user_id = self.request.query_params.get('system_user_id') - system_user = get_object_or_none(SystemUser, pk=system_user_id) asset_id = self.request.query_params.get('asset_id') - asset = get_object_or_none(Asset, pk=asset_id) application_id = self.request.query_params.get('application_id') - application = get_object_or_none(Application, pk=application_id) - q = Q() - if user: - q |= Q(users=user) - if user_groups: - q |= Q(user_groups__in=set(user_groups)) - if system_user: - q |= Q(system_users=system_user) - if asset: - q |= Q(assets=asset) - if application: - q |= Q(applications=application) - if q: - cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) - rule_ids = cmd_filters.values_list('rules', flat=True) - rules = CommandFilterRule.objects.filter(id__in=rule_ids) - else: - rules = CommandFilterRule.objects.none() + rules = CommandFilterRule.get_queryset( + user_id=user_id, + user_group_id=user_group_id, + system_user_id=system_user_id, + asset_id=asset_id, + application_id=application_id + ) return rules diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index 9de4859b5..6a2f030fa 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -4,15 +4,19 @@ import uuid import re from django.db import models +from django.db.models import Q from django.core.validators import MinValueValidator, MaxValueValidator from django.utils.translation import ugettext_lazy as _ -from common.utils import lazyproperty, get_logger +from users.models import User, UserGroup +from applications.models import Application +from ..models import SystemUser, Asset + +from common.utils import lazyproperty, get_logger, get_object_or_none from orgs.mixins.models import OrgModelMixin logger = get_logger(__file__) - __all__ = [ 'CommandFilter', 'CommandFilterRule' ] @@ -72,10 +76,14 @@ class CommandFilterRule(OrgModelMixin): confirm = 2, _('Reconfirm') id = models.UUIDField(default=uuid.uuid4, primary_key=True) - filter = models.ForeignKey('CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules') + filter = models.ForeignKey( + 'CommandFilter', on_delete=models.CASCADE, verbose_name=_("Filter"), related_name='rules' + ) type = models.CharField(max_length=16, default=TYPE_COMMAND, choices=TYPE_CHOICES, verbose_name=_("Type")) - priority = models.IntegerField(default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), - validators=[MinValueValidator(1), MaxValueValidator(100)]) + priority = models.IntegerField( + default=50, verbose_name=_("Priority"), help_text=_("1-100, the lower the value will be match first"), + validators=[MinValueValidator(1), MaxValueValidator(100)] + ) content = models.TextField(verbose_name=_("Content"), help_text=_("One line one command")) action = models.IntegerField(default=ActionChoices.deny, choices=ActionChoices.choices, verbose_name=_("Action")) # 动作: 附加字段 @@ -172,3 +180,34 @@ class CommandFilterRule(OrgModelMixin): ticket.create_process_map_and_node(self.reviewers.all()) ticket.open(applicant=session.user_obj) return ticket + + @classmethod + def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, asset_id=None, application_id=None): + user_groups = [] + user = get_object_or_none(User, pk=user_id) + if user: + user_groups.extend(list(user.groups.all())) + user_group = get_object_or_none(UserGroup, pk=user_group_id) + if user_group: + user_groups.append(user_group) + system_user = get_object_or_none(SystemUser, pk=system_user_id) + asset = get_object_or_none(Asset, pk=asset_id) + application = get_object_or_none(Application, pk=application_id) + q = Q() + if user: + q |= Q(users=user) + if user_groups: + q |= Q(user_groups__in=set(user_groups)) + if system_user: + q |= Q(system_users=system_user) + if asset: + q |= Q(assets=asset) + if application: + q |= Q(applications=application) + if q: + cmd_filters = CommandFilter.objects.filter(q).filter(is_active=True) + rule_ids = cmd_filters.values_list('rules', flat=True) + rules = cls.objects.filter(id__in=rule_ids) + else: + rules = cls.objects.none() + return rules diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index c22f63f2d..f9af64832 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:bc12c498651ddc7b1750389f65c4fe6d48d9dbe0fad3f63b7066cb82ef7e223a -size 97032 +oid sha256:07244b630278a5574b97c46218ae453de71d01a1ea6682b88baa741a99cf8c22 +size 97436 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 44c3f384c..5998da265 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-01-18 18:31+0800\n" +"POT-Creation-Date: 2022-01-19 19:02+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -20,7 +20,7 @@ msgstr "" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 #: applications/models/application.py:202 assets/models/asset.py:139 #: assets/models/base.py:175 assets/models/cluster.py:18 -#: assets/models/cmd_filter.py:23 assets/models/domain.py:24 +#: assets/models/cmd_filter.py:27 assets/models/domain.py:24 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:24 perms/models/base.py:83 settings/models.py:29 #: settings/serializers/sms.py:6 terminal/models/storage.py:23 @@ -34,12 +34,12 @@ msgstr "" msgid "Name" msgstr "名称" -#: acls/models/base.py:27 assets/models/cmd_filter.py:77 +#: acls/models/base.py:27 assets/models/cmd_filter.py:84 #: assets/models/user.py:211 msgid "Priority" msgstr "优先级" -#: acls/models/base.py:28 assets/models/cmd_filter.py:77 +#: acls/models/base.py:28 assets/models/cmd_filter.py:84 #: assets/models/user.py:211 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -54,8 +54,8 @@ msgstr "激活中" #: acls/models/base.py:32 applications/models/application.py:215 #: assets/models/asset.py:144 assets/models/asset.py:232 #: assets/models/backup.py:54 assets/models/base.py:180 -#: assets/models/cluster.py:29 assets/models/cmd_filter.py:44 -#: assets/models/cmd_filter.py:87 assets/models/domain.py:25 +#: assets/models/cluster.py:29 assets/models/cmd_filter.py:48 +#: assets/models/cmd_filter.py:95 assets/models/domain.py:25 #: assets/models/domain.py:65 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:27 #: perms/models/base.py:93 settings/models.py:34 terminal/models/storage.py:26 @@ -71,7 +71,7 @@ msgstr "备注" msgid "Reject" msgstr "拒绝" -#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:71 +#: acls/models/login_acl.py:19 assets/models/cmd_filter.py:75 msgid "Allow" msgstr "允许" @@ -81,8 +81,8 @@ msgid "Login confirm" msgstr "登录复核" #: acls/models/login_acl.py:24 acls/models/login_asset_acl.py:20 -#: assets/models/cmd_filter.py:26 assets/models/label.py:15 audits/models.py:37 -#: audits/models.py:57 audits/models.py:79 audits/serializers.py:97 +#: assets/models/cmd_filter.py:30 assets/models/label.py:15 audits/models.py:37 +#: audits/models.py:57 audits/models.py:79 audits/serializers.py:100 #: authentication/models.py:47 orgs/models.py:19 orgs/models.py:433 #: perms/models/base.py:84 templates/index.html:78 #: terminal/backends/command/models.py:19 @@ -104,7 +104,7 @@ msgstr "规则" #: acls/models/login_acl.py:31 acls/models/login_asset_acl.py:26 #: acls/serializers/login_acl.py:17 acls/serializers/login_asset_acl.py:75 -#: assets/models/cmd_filter.py:80 audits/models.py:58 +#: assets/models/cmd_filter.py:88 audits/models.py:58 audits/serializers.py:51 #: authentication/templates/authentication/_access_key_modal.html:34 #: users/templates/users/_granted_assets.html:29 #: users/templates/users/user_asset_permission.html:44 @@ -114,7 +114,7 @@ msgid "Action" msgstr "动作" #: acls/models/login_acl.py:35 acls/models/login_asset_acl.py:32 -#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:85 +#: acls/serializers/login_acl.py:16 assets/models/cmd_filter.py:93 msgid "Reviewers" msgstr "审批人" @@ -131,7 +131,7 @@ msgstr "系统用户" #: acls/models/login_asset_acl.py:22 #: applications/serializers/attrs/application_category/remote_app.py:36 #: assets/models/asset.py:356 assets/models/authbook.py:19 -#: assets/models/backup.py:31 assets/models/cmd_filter.py:34 +#: assets/models/backup.py:31 assets/models/cmd_filter.py:38 #: assets/models/gathered_user.py:14 assets/serializers/system_user.py:264 #: audits/models.py:39 perms/models/asset_permission.py:24 #: templates/index.html:82 terminal/backends/command/models.py:20 @@ -248,7 +248,7 @@ msgstr "时段" msgid "My applications" msgstr "我的应用" -#: applications/const.py:8 applications/models/account.py:12 +#: applications/const.py:8 #: applications/serializers/attrs/application_category/db.py:14 #: applications/serializers/attrs/application_type/mysql_workbench.py:25 #: xpack/plugins/change_auth_plan/models/app.py:32 @@ -263,8 +263,14 @@ msgstr "远程应用" msgid "Custom" msgstr "自定义" +#: applications/models/account.py:12 applications/models/application.py:219 +#: assets/models/backup.py:32 assets/models/cmd_filter.py:45 +#: perms/models/application_permission.py:27 users/models/user.py:170 +msgid "Application" +msgstr "应用程序" + #: applications/models/account.py:15 assets/models/authbook.py:20 -#: assets/models/cmd_filter.py:38 assets/models/user.py:302 audits/models.py:40 +#: assets/models/cmd_filter.py:42 assets/models/user.py:302 audits/models.py:40 #: perms/models/application_permission.py:32 #: perms/models/asset_permission.py:26 templates/_nav.html:45 #: terminal/backends/command/models.py:21 @@ -305,7 +311,7 @@ msgstr "类别" #: applications/models/application.py:207 #: applications/serializers/application.py:101 assets/models/backup.py:49 -#: assets/models/cmd_filter.py:76 assets/models/user.py:210 +#: assets/models/cmd_filter.py:82 assets/models/user.py:210 #: perms/models/application_permission.py:23 #: perms/serializers/application/user_permission.py:34 #: terminal/models/storage.py:55 terminal/models/storage.py:116 @@ -325,12 +331,6 @@ msgstr "网域" msgid "Attrs" msgstr "属性" -#: applications/models/application.py:219 assets/models/backup.py:32 -#: assets/models/cmd_filter.py:41 perms/models/application_permission.py:27 -#: users/models/user.py:170 -msgid "Application" -msgstr "应用程序" - #: applications/serializers/application.py:70 #: applications/serializers/application.py:100 assets/serializers/label.py:13 #: perms/serializers/application/permission.py:18 @@ -353,7 +353,7 @@ msgstr "类型名称" #: assets/models/domain.py:27 assets/models/gathered_user.py:19 #: assets/models/group.py:22 assets/models/label.py:25 #: assets/serializers/account.py:17 common/db/models.py:113 -#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:29 +#: common/mixins/models.py:50 ops/models/adhoc.py:39 ops/models/command.py:30 #: orgs/models.py:26 orgs/models.py:435 perms/models/base.py:92 #: users/models/group.py:18 users/models/user.py:783 #: xpack/plugins/cloud/models.py:122 @@ -571,7 +571,7 @@ msgstr "协议组" msgid "Nodes" msgstr "节点" -#: assets/models/asset.py:220 assets/models/cmd_filter.py:43 +#: assets/models/asset.py:220 assets/models/cmd_filter.py:47 #: assets/models/domain.py:66 assets/models/label.py:22 msgid "Is active" msgstr "激活" @@ -594,8 +594,8 @@ msgid "Labels" msgstr "标签管理" #: assets/models/asset.py:230 assets/models/base.py:183 -#: assets/models/cluster.py:28 assets/models/cmd_filter.py:48 -#: assets/models/cmd_filter.py:90 assets/models/group.py:21 +#: assets/models/cluster.py:28 assets/models/cmd_filter.py:52 +#: assets/models/cmd_filter.py:98 assets/models/group.py:21 #: common/db/models.py:111 common/mixins/models.py:49 orgs/models.py:25 #: orgs/models.py:437 perms/models/base.py:91 users/models/user.py:593 #: users/serializers/group.py:33 @@ -634,7 +634,7 @@ msgstr "手动触发" msgid "Timing trigger" msgstr "定时触发" -#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:30 +#: assets/models/backup.py:105 audits/models.py:44 ops/models/command.py:31 #: perms/models/base.py:89 terminal/models/session.py:54 #: tickets/serializers/ticket/meta/ticket_type/apply_application.py:55 #: tickets/serializers/ticket/meta/ticket_type/apply_asset.py:57 @@ -670,8 +670,8 @@ msgstr "触发模式" msgid "Reason" msgstr "原因" -#: assets/models/backup.py:121 audits/serializers.py:76 -#: audits/serializers.py:91 ops/models/adhoc.py:254 +#: assets/models/backup.py:121 audits/serializers.py:82 +#: audits/serializers.py:97 ops/models/adhoc.py:254 #: terminal/serializers/session.py:35 #: xpack/plugins/change_auth_plan/models/base.py:202 msgid "Is success" @@ -771,7 +771,7 @@ msgstr "系统" msgid "Default Cluster" msgstr "默认Cluster" -#: assets/models/cmd_filter.py:30 perms/models/base.py:86 +#: assets/models/cmd_filter.py:34 perms/models/base.py:86 #: templates/_nav.html:21 users/models/group.py:31 users/models/user.py:555 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_asset_permission.html:39 @@ -781,51 +781,51 @@ msgstr "默认Cluster" msgid "User group" msgstr "用户组" -#: assets/models/cmd_filter.py:56 assets/serializers/system_user.py:54 +#: assets/models/cmd_filter.py:60 assets/serializers/system_user.py:54 msgid "Command filter" msgstr "命令过滤器" -#: assets/models/cmd_filter.py:63 +#: assets/models/cmd_filter.py:67 msgid "Regex" msgstr "正则表达式" -#: assets/models/cmd_filter.py:64 ops/models/command.py:25 +#: assets/models/cmd_filter.py:68 ops/models/command.py:26 #: terminal/backends/command/serializers.py:15 terminal/models/session.py:51 #: terminal/templates/terminal/_msg_command_alert.html:12 #: terminal/templates/terminal/_msg_command_execute_alert.html:10 msgid "Command" msgstr "命令" -#: assets/models/cmd_filter.py:70 +#: assets/models/cmd_filter.py:74 msgid "Deny" msgstr "拒绝" -#: assets/models/cmd_filter.py:72 +#: assets/models/cmd_filter.py:76 msgid "Reconfirm" msgstr "复核" -#: assets/models/cmd_filter.py:75 +#: assets/models/cmd_filter.py:80 msgid "Filter" msgstr "过滤器" -#: assets/models/cmd_filter.py:79 settings/serializers/basic.py:10 +#: assets/models/cmd_filter.py:87 settings/serializers/basic.py:10 #: xpack/plugins/license/models.py:29 msgid "Content" msgstr "内容" -#: assets/models/cmd_filter.py:79 +#: assets/models/cmd_filter.py:87 msgid "One line one command" msgstr "每行一个命令" -#: assets/models/cmd_filter.py:94 +#: assets/models/cmd_filter.py:102 msgid "Command filter rule" msgstr "命令过滤规则" -#: assets/models/cmd_filter.py:132 +#: assets/models/cmd_filter.py:140 msgid "The generated regular expression is incorrect: {}" msgstr "生成的正则表达式有误" -#: assets/models/cmd_filter.py:158 tickets/const.py:13 +#: assets/models/cmd_filter.py:166 tickets/const.py:13 msgid "Command confirm" msgstr "命令复核" @@ -1056,7 +1056,7 @@ msgstr "当前只支持邮件发送" msgid "Key password" msgstr "密钥密码" -#: assets/serializers/base.py:48 +#: assets/serializers/base.py:49 msgid "private key invalid or passphrase error" msgstr "密钥不合法或密钥密码错误" @@ -1336,7 +1336,7 @@ msgstr "创建" msgid "Update" msgstr "更新" -#: audits/models.py:59 audits/serializers.py:60 +#: audits/models.py:59 audits/serializers.py:63 msgid "Resource Type" msgstr "资源类型" @@ -1419,28 +1419,28 @@ msgstr "MFA名称" msgid "Reason display" msgstr "原因描述" -#: audits/serializers.py:81 +#: audits/serializers.py:84 msgid "Hosts display" msgstr "主机名称" -#: audits/serializers.py:93 ops/models/command.py:26 +#: audits/serializers.py:96 ops/models/command.py:27 #: xpack/plugins/cloud/models.py:170 msgid "Result" msgstr "结果" -#: audits/serializers.py:95 terminal/serializers/storage.py:151 +#: audits/serializers.py:98 terminal/serializers/storage.py:151 msgid "Hosts" msgstr "主机" -#: audits/serializers.py:96 +#: audits/serializers.py:99 msgid "Run as" msgstr "运行用户" -#: audits/serializers.py:98 +#: audits/serializers.py:101 msgid "Run as display" msgstr "运行用户名称" -#: audits/serializers.py:99 +#: audits/serializers.py:102 msgid "User display" msgstr "用户名称" @@ -2636,7 +2636,7 @@ msgstr "开始时间" msgid "End time" msgstr "完成时间" -#: ops/models/adhoc.py:253 ops/models/command.py:28 +#: ops/models/adhoc.py:253 ops/models/command.py:29 #: terminal/serializers/session.py:39 msgid "Is finished" msgstr "是否完成" @@ -2649,19 +2649,23 @@ msgstr "结果" msgid "Adhoc result summary" msgstr "汇总" -#: ops/models/command.py:31 +#: ops/models/command.py:32 msgid "Date finished" msgstr "结束日期" -#: ops/models/command.py:78 +#: ops/models/command.py:108 msgid "Task start" msgstr "任务开始" -#: ops/models/command.py:102 +#: ops/models/command.py:120 +msgid "There are currently no assets that can be executed" +msgstr "当前没有匹配到可允许执行的资产" + +#: ops/models/command.py:142 msgid "Command `{}` is forbidden ........" msgstr "命令 `{}` 不允许被执行 ......." -#: ops/models/command.py:115 +#: ops/models/command.py:155 msgid "Task end" msgstr "任务结束" diff --git a/apps/ops/models/command.py b/apps/ops/models/command.py index 819acc3d5..61504d782 100644 --- a/apps/ops/models/command.py +++ b/apps/ops/models/command.py @@ -10,10 +10,11 @@ from django.utils.translation import ugettext from django.db import models from terminal.notifications import CommandExecutionAlert +from assets.models import Asset from common.utils import lazyproperty from orgs.models import Organization from orgs.mixins.models import OrgModelMixin -from orgs.utils import current_org, tmp_to_org +from orgs.utils import tmp_to_org from ..ansible.runner import CommandRunner from ..inventory import JMSInventory @@ -37,13 +38,12 @@ class CommandExecution(OrgModelMixin): with tmp_to_org(self.run_as.org_id): super().save(*args, **kwargs) - @property def inventory(self): if self.run_as.username_same_with_user: username = self.user.username else: username = self.run_as.username - inv = JMSInventory(self.hosts.all(), run_as=username, system_user=self.run_as) + inv = JMSInventory(self.allow_assets, run_as=username, system_user=self.run_as) return inv @lazyproperty @@ -74,16 +74,56 @@ class CommandExecution(OrgModelMixin): def get_hosts_names(self): return ','.join(self.hosts.all().values_list('hostname', flat=True)) + def cmd_filter_rules(self, asset_id=None): + from assets.models import CommandFilterRule + user_id = self.user.id + system_user_id = self.run_as.id + rules = CommandFilterRule.get_queryset( + user_id=user_id, + system_user_id=system_user_id, + asset_id=asset_id, + ) + return rules + + def is_command_can_run(self, command, asset_id=None): + for rule in self.cmd_filter_rules(asset_id=asset_id): + action, matched_cmd = rule.match(command) + if action == rule.ActionChoices.allow: + return True, None + elif action == rule.ActionChoices.deny: + return False, matched_cmd + return True, None + + @property + def allow_assets(self): + allow_asset_ids = [] + for asset in self.hosts.all(): + ok, __ = self.is_command_can_run(self.command, asset_id=asset.id) + if ok: + allow_asset_ids.append(asset.id) + allow_assets = Asset.objects.filter(id__in=allow_asset_ids) + return allow_assets + def run(self): print('-'*10 + ' ' + ugettext('Task start') + ' ' + '-'*10) org = Organization.get_instance(self.run_as.org_id) org.change_to() self.date_start = timezone.now() - ok, msg = self.run_as.is_command_can_run(self.command) + ok, msg = self.is_command_can_run(self.command) if ok: + allow_assets = self.allow_assets + deny_assets = set(list(self.hosts.all())) - set(list(allow_assets)) + for asset in deny_assets: + print(f'资产{asset}: 命令{self.command}不允许执行') + if not allow_assets: + self.result = { + "error": _('There are currently no assets that can be executed') + } + self.save() + return self.result runner = CommandRunner(self.inventory) try: - host = self.hosts.first() + host = allow_assets.first() if host and host.is_windows(): shell = 'win_shell' elif host and host.is_unixlike():