def push_config(request): c = SSHClient() if request.is_ajax(): rst = {} regions = Region.objects.all() action = request.GET.get('action') rst = {} for i in regions: region = i.region roster = '' roster_path = './media/salt/region_sls' if not os.path.exists(roster_path): os.makedirs(roster_path) host_list = SaltHost.objects.filter(region=i) for h in host_list: for u in h.user.all(): roster = roster + '''%s-%s: host: %s port: %s user: %s passwd: %s thin_dir: /home/%s/.salt-thin timeout: 30\n''' % (h.ip, u.username, h.ip, h.port, u.username, u.password, u.username) with open('%s/roster_%s' % (roster_path, region), 'w') as f: f.write(roster) if action == 'push': r = {} ret = c.cmd(tgt='*', fun='state.sls', roster_file='%s/roster_%s' % (roster_path, region), arg=['grains.%s' % region]) rst = dict(rst, **ret) elif action == 'refresh': ## refresh grains ret = c.cmd(tgt='*', fun='saltutil.sync_grains', roster_file='%s/roster_%s' % (roster_path, region)) rst = dict(rst, **ret) else: raise Http404 return HttpResponse(json.dumps(rst))
def get_grains(self): client = SSHClient() output = client.cmd(tgt='*', fun='grains.item host \ hwaddr_interfaces ip4_interfaces') olist = sorted(output.keys()) data = {} for o in olist: if output[o]['retcode'] == 0: out = output[o]['return'] data[out['host']] = output[o]['return'] return data
def get_ev_files(vpn_ip, path): pre_dir = os.path.dirname(path) my_capital = os.path.join(pre_dir, "my_capital.config") cli = SSHClient() try: ret = cli.cmd(vpn_ip, "cmd.run", ["cat %s" % my_capital]) if ret.get(vpn_ip).get("retcode") == 0: capital_content = ret.get(vpn_ip).get("return") sts = get_config_strategy(capital_content) print "sts: ", sts return strategy_map_ev(sts) except Exception, ex: traceback.print_exc() Log.error("get ev files error! %s" % ex)
def salt_ssh(target, command): from salt.client.ssh.client import SSHClient client = SSHClient(c_path='/etc/salt/master') client.opts['ssh_skip_roster'] = True client.opts['raw_shell'] = True command = client.cmd(tgt=target, fun=command) server = next(iter(command)) retcode = command[server]['retcode'] stdout = command[server]['stdout'] stderr = command[server]['stderr'] if retcode == 0: return stdout,retcode else: return stderr,retcode
def get_bonding_opts(self, host, bond): # maybe tell it where the python lib is? client = SSHClient() # can't do hosts without a roster file, gotta build that # or figure something out mcmd = [ "grep BONDING_OPTS \ /etc/sysconfig/network-scripts/ifcfg-%s" % bond ] output = client.cmd(tgt=host, fun='cmd.run', arg=mcmd) olist = sorted(output.keys()) for o in olist: if output[o]['retcode'] == 0: out = output[o]['return'].lower() return out
def get_real_macs(self, host, bond): # maybe tell it where the python lib is? client = SSHClient() # can't do hosts without a roster file, gotta build that # or figure something out mcmd = ["cat /proc/net/bonding/%s | egrep 'addr|Slave I'" % bond] output = client.cmd(tgt=host, fun='cmd.run', arg=mcmd) olist = sorted(output.keys()) realnic = {} for o in olist: if output[o]['retcode'] == 0: out = output[o]['return'].split('\n') l = [x.split()[-1] for x in out] realmacs = dict(zip(*[iter(l)] * 2)) return realmacs
def job_exec_nginx(host_list, dest_file, bid_list, port, sls, roster_file, desc): c = SSHClient() data = {'dest_file': dest_file, 'backends': bid_list, 'port': port} result_source = c.cmd(tgt=host_list, fun='state.sls', roster_file=roster_file, arg=[sls, 'pillar=%s' % json.dumps(data)], expr_form='list') result = [] keys = result_source.keys() keys.sort() for i in keys: value = result_source[i] t_upstream = {} t_reload = {} ret = 0 if value['retcode'] != 0: ret = 103 for k, v in value['return'].items(): keys = k.split('|') if keys[-1] == '-replace': t_upstream[keys[1]] = {'comment': v['comment'], 'result': v['result']} if keys[1] == '-nginx-reload_': if v['changes']: if v['changes']['retcode'] != 0: ret = 102 # test failed t_reload = {'comment': v['comment'], 'result': v['result'], 'retcode': v['changes']['retcode'], 'stderr': v['changes']['stderr']} else: t_reload = {'comment': v['comment'], 'result': v['result']} if v['comment'] == 'onlyif execution failed': ret = 99 r = {'host': i, 'backends': t_upstream, 'reload': t_reload, 'retcode': value['retcode'], 'ret': ret} result.append(r) if value['retcode'] == 0 and ret == 0: temp = '成功' elif ret == 99: temp = '失败:配置无任何变更' elif ret == 102: temp = '失败:配置测试不通过' else: temp = '失败:未知异常' logger.info('{0}nginx服务器{1}后端{2}:{3}{4},返回信息:{5}'.format(desc, i, bid_list, port, temp, r)) return {'result': result, 'source': result_source}
def log_tail(request): if request.is_ajax(): pid = request.POST.get('pid') host = request.POST.get('host') project = Project.objects.get(pk=pid) roster_file = os.path.join( BASE_DIR, 'media/salt/project_roster/roster_hostgroup_%s' % (project.host_group.id)) data = {'puser': project.host_group.user, 'dpath': project.path} sls = 'log_tail' c = SSHClient() r = c.cmd(tgt=host, fun='state.sls', roster_file=roster_file, arg=[sls, 'pillar=%s' % json.dumps(data)], expr_form='list') ret = '' for _, v in r.items(): for _, v1 in v['return'].items(): ret = v1['changes']['stdout'] return JsonResponse({'retcode': 0, 'result': ret})
def create_host_files(self): client = SSHClient() try: output = client.cmd(tgt='*', fun='grains.item fqdn_ip4 fqdn host') except: raise CommandError(self, "Host target is incorrect.") f = open('/etc/salt/roster', 'w') h = open('/etc/hosts', 'a') for o in output: if output[o]['retcode'] == 0: name = output[o]['return']['host'] ip = output[o]['return']['fqdn_ip4'][0] fqdn = output[o]['return']['fqdn'] f.write("%s: %s\n" % (name, ip)) h.write("%s %s %s\n" % (ip, fqdn, name)) else: msg = "Unable to access %s. " % o msg += " via ssh. It will not be included." print msg f.close() h.close()
class SaltSSHClient(SaltClientBase): c_path: str = attr.ib( default='/etc/salt/master', metadata={inputs.METADATA_ARGPARSER: { 'help': "config path" }}) roster_file: str = attr.ib( default=config.SALT_ROSTER_DEFAULT, metadata={inputs.METADATA_ARGPARSER: { 'help': "path to roster file" }}) ssh_options: Optional[Dict] = attr.ib(default=attr.Factory( lambda: ['UserKnownHostsFile=/dev/null', 'StrictHostKeyChecking=no']), metadata={ inputs.METADATA_ARGPARSER: { 'help': "ssh connection options", 'metavar': 'KEY=VALUE', 'nargs': '+', 'action': KeyValueListAction } }) # targets: str = RunArgs.targets targets: str = attr.ib(default=config.ALL_MINIONS, metadata={ inputs.METADATA_ARGPARSER: { 'help': "command's host targets" } }) _client: SSHClient = attr.ib(init=False, default=None) _def_roster_data: Dict = attr.ib(init=False, default=attr.Factory(dict)) def __attrs_post_init__(self): """Do post init.""" self._client = SSHClient(c_path=str(self.c_path)) # if self.roster_file is None: # path = USER_SHARED_PILLAR.all_hosts_path( # 'roster.sls' # ) # if not path.exists(): # path = GLUSTERFS_VOLUME_PILLAR_DIR.all_hosts_path( # 'roster.sls' # ) # if path.exists(): # self.roster_file = path if self.roster_file: logger.debug(f'default roster is set to {self.roster_file}') self._def_roster_data = load_yaml(self.roster_file) @property def _cmd_args_t(self) -> Type[SaltArgsBase]: return SaltSSHArgs @property def _salt_client_res_t(self) -> Type[SaltClientResultBase]: return SaltSSHClientResult def roster_data(self, roster_file=None): if roster_file: return load_yaml(roster_file) else: return self._def_roster_data def roster_targets(self, roster_file=None): return list(self.roster_data(roster_file)) def _run(self, cmd_args: SaltSSHArgs): salt_logger = logging.getLogger('salt.client.ssh') salt_log_level = None if cmd_args.secure and salt_logger.isEnabledFor(logging.DEBUG): salt_log_level = salt_logger.getEffectiveLevel() salt_logger.setLevel(logging.INFO) try: return self._client.cmd(*cmd_args.args, **cmd_args.kwargs) finally: if salt_log_level is not None: salt_logger.setLevel(salt_log_level) def run(self, fun: str, fun_args: Union[Tuple, None] = None, fun_kwargs: Union[Dict, None] = None, secure=False, **kwargs): for arg in ('roster_file', 'ssh_options', 'targets'): arg_v = kwargs.pop(arg, getattr(self, arg)) if arg_v: kwargs[arg] = (str(arg_v) if arg == 'roster_file' else arg_v) return super().run(fun, fun_args, fun_kwargs, secure, **kwargs) # TODO TEST EOS-8473 def ensure_access(self, targets: List, bootstrap_roster_file=None): for target in targets: try: # try to reach using default (class-level) settings self.run('uname', targets=target, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ('Permission denied' in reason): roster = load_yaml(roster_file) if bootstrap_roster_file: # NOTE assumptions: # - python3 is already there since it can be # installed using the bootstrap key # (so salt modules might be used now) # - bootstrap key is availble in salt-ssh file roots self.run('state.single', fun_args=['file.directory'], fun_kwargs=dict(name='~/.ssh', mode=700), targets=target, roster_file=bootstrap_roster_file) # inject production access public key # FIXME hardcoded 'root' self.run( 'state.single', fun_args=['ssh_auth.present'], fun_kwargs=dict( name=None, user=roster.get(target, {}).get('user', 'root'), # FIXME hardcoded path to production pub key source= "salt://provisioner/files/minions/all/id_rsa_prvsnr.pub" # noqa: E501 ), targets=target, roster_file=bootstrap_roster_file) else: copy_id(host=roster.get(target, {}).get('host'), user=roster.get(target, {}).get('user'), port=roster.get(target, {}).get('port'), priv_key_path=roster.get(target, {}).get('priv'), ssh_options=exc.cmd_args.get('kw').get( 'ssh_options'), force=True) else: raise # TODO TEST EOS-8473 def ensure_python3(self, targets: List, roster_file=None): for target in targets: try: self.run('python3 --version', targets=target, roster_file=roster_file, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) # TODO IMPROVE EOS-8473 better search string / regex roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ("not found" in reason): self.run( 'yum install -y python3', targets=target, roster_file=roster_file, ssh_options=exc.cmd_args.get('kw').get('ssh_options'), raw_shell=True) else: raise # TODO TEST EOS-8473 def ensure_ready( self, targets: List, bootstrap_roster_file: Optional[Path] = None, ): if bootstrap_roster_file: self.ensure_python3(targets, roster_file=bootstrap_roster_file) self.ensure_access(targets, bootstrap_roster_file=bootstrap_roster_file) if not bootstrap_roster_file: self.ensure_python3(targets)
class SaltSSHClient(SaltClientBase): roster_file: str = None ssh_options: Optional[Dict] = None _client: SSHClient = attr.ib(init=False, default=None) def __attrs_post_init__(self): self._client = SSHClient(c_path=str(self.c_path)) @property def _cmd_args_t(self) -> Type[SaltClientArgsBase]: return SaltSSHArgs @property def _salt_client_res_t(self) -> Type[SaltClientResult]: return SaltSSHClientResult def _run(self, cmd_args: SaltSSHArgs): salt_logger = logging.getLogger('salt.client.ssh') salt_log_level = None if cmd_args.secure and salt_logger.isEnabledFor(logging.DEBUG): salt_log_level = salt_logger.getEffectiveLevel() salt_logger.setLevel(logging.INFO) try: return self._client.cmd(*cmd_args.args, **cmd_args.kwargs) finally: if salt_log_level is not None: salt_logger.setLevel(salt_log_level) # TODO TEST EOS-8473 def ensure_access(self, targets: List, bootstrap_roster_file=None): for target in targets: try: # try to reach using default (class-level) settings self.run('uname', targets=target, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ('Permission denied' in reason): roster = load_yaml(roster_file) if bootstrap_roster_file: # NOTE assumptions: # - python3 is already there since it can be # installed using the bootstrap key # (so salt modules might be used now) # - bootstrap key is availble in salt-ssh file roots self.run('state.single', fun_args=['file.directory'], fun_kwargs=dict(name='~/.ssh', mode=700), targets=target, roster_file=bootstrap_roster_file) # inject production access public key # FIXME hardcoded 'root' self.run( 'state.single', fun_args=['ssh_auth.present'], fun_kwargs=dict( name=None, user=roster.get(target, {}).get('user', 'root'), # FIXME hardcoded path to production pub key source= "salt://provisioner/files/minions/all/id_rsa_prvsnr.pub" # noqa: E501 ), targets=target, roster_file=bootstrap_roster_file) else: copy_id(host=roster.get(target, {}).get('host'), user=roster.get(target, {}).get('user'), port=roster.get(target, {}).get('port'), priv_key_path=roster.get(target, {}).get('priv'), ssh_options=exc.cmd_args.get('kw').get( 'ssh_options'), force=True) else: raise # TODO TEST EOS-8473 def ensure_python3(self, targets: List, roster_file=None): for target in targets: try: self.run('python3 --version', targets=target, roster_file=roster_file, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) # TODO IMPROVE EOS-8473 better search string / regex roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ("not found" in reason): self.run( 'yum install -y python3', targets=target, roster_file=roster_file, ssh_options=exc.cmd_args.get('kw').get('ssh_options'), raw_shell=True) else: raise # TODO TEST EOS-8473 def ensure_ready( self, targets: List, bootstrap_roster_file: Optional[Path] = None, ): if bootstrap_roster_file: self.ensure_python3(targets, roster_file=bootstrap_roster_file) self.ensure_access(targets, bootstrap_roster_file=bootstrap_roster_file) if not bootstrap_roster_file: self.ensure_python3(targets) # FIXME issues: # 1. not properly processed error, e.g.: # # self.run( # 'state.single', # fun_args=['ssh_auth.present'], # fun_kwargs=dict( # user=roster.get(target, {}).get( # 'user', 'root' # ), # source=f"{priv_file}.pub" # ), # targets=target, # roster_file=bootstrap_roster_file # ) # # will return with success but salt actually error takes place: # # TypeError encountered executing state.single: single() missing 1 # required positional argument: 'name' # TODO TYPE EOS-8473 def run(self, *args, roster_file=None, ssh_options=None, **kwargs): if roster_file is None: roster_file = self.roster_file if roster_file: kwargs['roster_file'] = str(roster_file) if ssh_options is None: ssh_options = self.ssh_options if ssh_options: kwargs['ssh_options'] = ssh_options return super().run(*args, **kwargs)
def process_info(project, hosts): roster_file = os.path.join( BASE_DIR, 'media/salt/project_roster/roster_hostgroup_%s' % (project.host_group.id)) c = SSHClient() if project.container == 0: ## 显示长用户名 rst = c.cmd( hosts, 'cmd.run', [ 'ps -o ruser=LONGUSERNAME12 -eo pid,ppid,pcpu,pmem,rss,lstart,etime,cmd|grep config.file=/home/%s/%s/conf|grep -v grep|awk \'{print $1,$2,$3,$4,$5,$6,$8,$9,$10,$12}\'' % (project.host_group.user, project.path) ], roster_file=roster_file, expr_form='list') else: logger.info('Process Collect') rst = c.cmd( hosts, 'cmd.run', [ 'if [ -f /home/%s/%s/RUNNING_PID ];then ps -o ruser=LONGUSERNAME12 -eo pid,ppid,pcpu,pmem,rss,lstart,etime,cmd|grep `cat /home/%s/%s/RUNNING_PID`|grep -v grep|awk \'{print $1,$2,$3,$4,$5,$6,$8,$9,$10,$12}\';else exit 1;fi' % (project.host_group.user, project.path, project.host_group.user, project.path) ], roster_file=roster_file, expr_form='list') logger.info('Result: %s' % rst) result = {} for k, v in rst.items(): try: plist = ProcessList.objects.get(tag='%s-%s' % (project.id, k)) except: plist = ProcessList() plist.tag = '%s-%s' % (project.id, k) plist.project = project plist.host = SaltHost.objects.get(ip=k) if v['retcode'] == 0 and v['return']: rlist = v['return'].split(' ') t = 0 if "-" in rlist[9]: t = t + int(rlist[9].split("-")[0]) * 24 * 3600 d = rlist[9].split("-")[1] else: d = rlist[9].split("-")[0] d = d.split(":") if len(d) == 3: t = t + int(d[0]) * 3600 + int(d[1]) * 60 + int(d[2]) else: t = t + int(d[0]) * 60 + int(d[1]) plist.process_user = rlist[0] plist.process_pid = rlist[1] plist.process_ppid = rlist[2] plist.process_cpu_per = rlist[3] plist.process_mem_per = rlist[4] plist.process_rmem = rlist[5] plist.process_start = '%s%s-%s' % (rlist[6], rlist[7], rlist[8]) plist.process_etime = t r = "%s %s %s %s %s %s %s%s-%s %s" % ( rlist[0], rlist[1], rlist[2], rlist[3], rlist[4], rlist[5], rlist[6], rlist[7], rlist[8], t) else: r = "None None None None None None None None" plist.process_user = None plist.process_pid = None plist.process_ppid = None plist.process_cpu_per = None plist.process_mem_per = None plist.process_rmem = None plist.process_start = None plist.process_etime = None plist.save() plist = ProcessList.objects.get(tag='%s-%s' % (project.id, k)) result[plist.pk] = r return result
def project_config(request, template_name, pid=None): page_name = u'配置文件' content_sls = '' content_config = '' project = Project.objects.get(pk=pid) config_path = './media/salt/config/%s-%s' % (project.id, project.path) config_list = [ i['name'] for i in ConfigList.objects.filter(project=project).values('name') ] project_id = (project.path).replace('.', '-') roster_file = './media/salt/project_roster/roster_hostgroup_%s' % ( project.host_group.id) # 过滤禁用主机 host_list = project.host_group.hosts.filter(status=True) regions = Region.objects.all() rst = {'retcode': 0} if request.is_ajax(): if request.method == 'POST': action = request.POST.get('action') if action == 'update': filename = request.POST.get('config') content_config = request.POST.get('content_config') content_sls = request.POST.get('content_sls') config_path = './media/salt/config/%s-%s' % (project.id, project.path) file = filename.split('.')[0] ## 备份原文件 shutil.copy('%s/%s.jinja' % (config_path, filename), '%s/%s.jinja.bakup' % (config_path, filename)) shutil.copy('%s/%s.ini' % (config_path, filename), '%s/%s.ini.bakup' % (config_path, filename)) try: with open('%s/%s.ini' % (config_path, filename), 'w') as f: f.write(content_sls) except: content_config = 'File %s/%s not exists.' % (config_path, filename) try: with open('%s/%s.jinja' % (config_path, filename), 'w') as f: f.write(content_config) except: content_config = 'File %s/%s not exists.' % (config_path, filename) return HttpResponse(json.dumps('ok')) if action == 'release': hosts = request.POST.get('hosts') filename = request.POST.get('config') #### test #### path = datetime.datetime.now().strftime('%Y%m%d%H%M%S') # 重命名备份文件 shutil.copy('%s/%s.jinja.bakup' % (config_path, filename), '%s/%s.jinja.%s' % (config_path, filename, path)) shutil.copy('%s/%s.ini.bakup' % (config_path, filename), '%s/%s.ini.%s' % (config_path, filename, path)) c = SSHClient() cfg = SomsParse() cfg.read(os.path.join(config_path, '{}.ini'.format(filename))) data = cfg.as_dict() data['puser'] = project.host_group.user data['dpath'] = project.path data['dtime'] = path data['pub_path'] = project.path data['filename'] = filename data['ctype'] = project.container data['project'] = project.id rst_source = c.cmd( tgt=hosts, fun='state.sls', roster_file=roster_file, arg=['job_config', 'pillar=%s' % json.dumps(data)], expr_form='list') #### end ##### rst = result_handle_config(rst_source) # 创建更新列表 cbak = ConfigBackup() cbak.name = ConfigList.objects.filter(project=project).get( name=filename) cbak.path = path cbak.content = rst cbak.save() # 记录操作日志 ConfigLog.objects.create(user=username_auth(request), project=project.name, config=filename, action_ip=user_ip(request), content=rst, source_content=rst_source, msg_type=False) return HttpResponse(json.dumps(rst)) if action == 'rollback': hosts = request.POST.get('hosts') filename = request.POST.get('config') version = request.POST.get('config_ver') # 还原对应版本文件 shutil.copy( '%s/%s.jinja.%s' % (config_path, filename, version), '%s/%s.jinja' % (config_path, filename)) c = SSHClient() data = { 'puser': project.host_group.user, 'project': project.id, 'dpath': project.path, 'dtime': version, 'pub_path': project.path, 'filename': filename, 'ctype': project.container } rst_source = c.cmd(tgt=hosts, fun='state.sls', roster_file=roster_file, arg=[ 'job_config_rollback', 'pillar=%s' % json.dumps(data) ], expr_form='list') #### end ##### rst = result_handle_config(rst_source) ConfigLog.objects.create(user=username_auth(request), project=project.name, config=filename, action_ip=user_ip(request), content=rst, source_content=rst_source, msg_type=True) return HttpResponse(json.dumps(rst)) filename = request.GET.get('config') config_path = os.path.join( BASE_DIR, './media/salt/config/%s-%s' % (project.id, project.path)) if not os.path.exists(config_path): os.makedirs(config_path) file = filename.split('.')[0] action = request.GET.get('action') if action == 'get': try: with open('%s/%s.ini' % (config_path, filename), 'r') as f: content_sls = f.read() except: content_sls = 'File %s/%s not exists, created?' % (config_path, filename) rst['retcode'] = 1 try: with open('%s/%s.jinja' % (config_path, filename), 'r') as f: content_config = f.read() except: content_config = 'File %s/%s not exists, created?' % ( config_path, filename) rst['retcode'] = 1 else: config = ConfigParser.RawConfigParser() region_query = Region.objects.all() if len(region_query) == 0: rst['retcode'] = 3 return JsonResponse(rst) for i in region_query: config.add_section(i.region) config.set(i.region, 'name', i.name) config.set(i.region, 'region', i.region) with open(os.path.join(config_path, '%s.ini' % filename), 'w') as f: config.write(f) try: with open('%s/%s.jinja' % (config_path, filename), 'r') as f: content_config = f.read() except: with open('%s/%s.jinja' % (config_path, filename), 'w') as f: f.write( "{% set region = grains['region'] %}\n#key不存在时报错:pillar[region]['key']\n" + "#key不存在时使用默认值:salt['pillar.get'](region + ':key', 'default')" ) # 记录文件 clist = ConfigList() clist.name = filename clist.project = project clist.tag = '%s-%s' % (filename, project.id) clist.save() rst['retcode'] = 2 rst['sls'] = content_sls rst['config'] = content_config return HttpResponse(json.dumps(rst)) return render( request, template_name, { 'page_name': page_name, 'pid': pid, 'all_hosts': host_list, 'all_regions': regions, 'project': project, 'project_id': project_id, 'config_list': config_list, 'nav_tag': 'project_list' })
class SaltSSHClient(SaltClientBase): roster_file: str = None ssh_options: Optional[Dict] = None _client: SSHClient = attr.ib(init=False, default=None) def __attrs_post_init__(self): self._client = SSHClient(c_path=str(self.c_path)) @property def _cmd_args_t(self) -> Type[SaltClientArgsBase]: return SaltSSHArgs @property def _salt_client_res_t(self) -> Type[SaltClientResult]: return SaltSSHClientResult def _run(self, cmd_args: SaltSSHArgs): return self._client.cmd(*cmd_args.args, **cmd_args.kwargs) # TODO TEST EOS-8473 def ensure_access(self, targets: List, roster_file=None, ssh_options=None): for target in targets: try: self.run('uname', targets=target, roster_file=roster_file, ssh_options=ssh_options, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ('Permission denied' in reason): roster = load_yaml(roster_file) copy_id( host=roster.get(target, {}).get('host'), user=roster.get(target, {}).get('user'), port=roster.get(target, {}).get('port'), priv_key_path=roster.get(target, {}).get('priv'), ssh_options=exc.cmd_args.get('kw').get('ssh_options'), force=True) else: raise # TODO TEST EOS-8473 def ensure_python3(self, targets: List, roster_file=None, ssh_options=None): for target in targets: try: self.run('python3 --version', targets=target, roster_file=roster_file, ssh_options=ssh_options, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) # TODO IMPROVE EOS-8473 better search string / regex roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ("not found" in reason): self.run( 'yum install -y python3', targets=target, roster_file=exc.cmd_args.get('kw').get('roster_file'), ssh_options=exc.cmd_args.get('kw').get('ssh_options'), raw_shell=True) else: raise # TODO TEST EOS-8473 def ensure_ready(self, targets: List, roster_file=None, ssh_options=None): self.ensure_access(targets, roster_file=roster_file, ssh_options=ssh_options) self.ensure_python3(targets, roster_file=roster_file, ssh_options=ssh_options) # TODO TYPE EOS-8473 def run(self, *args, roster_file=None, ssh_options=None, **kwargs): if roster_file is None: roster_file = self.roster_file if roster_file: kwargs['roster_file'] = str(roster_file) if ssh_options is None: ssh_options = self.ssh_options if ssh_options: kwargs['ssh_options'] = ssh_options return super().run(*args, **kwargs)
class SaltSSHClient(SaltClientBase): c_path: str = attr.ib( default='/etc/salt/master', metadata={inputs.METADATA_ARGPARSER: { 'help': "config path" }}, converter=utils.converter_path_resolved, ) roster_file: str = attr.ib( default=config.SALT_ROSTER_DEFAULT, metadata={inputs.METADATA_ARGPARSER: { 'help': "path to roster file" }}, converter=utils.converter_path_resolved, ) profile: str = attr.ib( default=None, metadata={ inputs.METADATA_ARGPARSER: { 'help': ("path to ssh profile, if specified" "'--c-path', '--roster-file', '--pillar-path'" "and '--fileroot-path' options would be set " "automatically") } }, converter=utils.converter_path_resolved) profile_name: str = attr.ib(default=None, metadata={ inputs.METADATA_ARGPARSER: { 'help': ("name of the ssh profile. Ignored if" " '--profile' is specified") } }) ssh_options: Optional[Dict] = attr.ib(default=attr.Factory( lambda: ['UserKnownHostsFile=/dev/null', 'StrictHostKeyChecking=no']), metadata={ inputs.METADATA_ARGPARSER: { 'help': "ssh connection options", 'metavar': 'KEY=VALUE', 'nargs': '+', 'action': KeyValueListAction } }) # targets: str = RunArgs.targets targets: str = attr.ib(default=config.ALL_MINIONS, metadata={ inputs.METADATA_ARGPARSER: { 'help': "command's host targets" } }) re_config: bool = False _client: SSHClient = attr.ib(init=False, default=None) _def_roster_data: Dict = attr.ib(init=False, default=attr.Factory(dict)) def __attrs_post_init__(self): """Do post init.""" if not self.profile and self.profile_name: self.profile = (config.profile_base_dir().parent / self.profile_name) if self.profile: paths = config.profile_paths( config.profile_base_dir(profile=self.profile)) self.c_path = paths['salt_master_file'] self.roster_file = paths['salt_roster_file'] self.fileroot_path = converter__fileroot_path( paths['salt_fileroot_dir']) self.pillar_path = converter__pillar_path(paths['salt_pillar_dir']) self._client_init() # if self.roster_file is None: # path = USER_SHARED_PILLAR.all_hosts_path( # 'roster.sls' # ) # if not path.exists(): # path = GLUSTERFS_VOLUME_PILLAR_DIR.all_hosts_path( # 'roster.sls' # ) # if path.exists(): # self.roster_file = path if self.roster_file: logger.debug(f'default roster is set to {self.roster_file}') self._def_roster_data = utils.load_yaml(self.roster_file) def _client_init(self): self._client = SSHClient(c_path=str(self.c_path)) @property def _cmd_args_t(self) -> Type[SaltArgsBase]: return SaltSSHArgs @property def _salt_client_res_t(self) -> Type[SaltClientResultBase]: return SaltSSHClientResult def _add_file_roots(self, roots: List[Path]): config = utils.load_yaml(self.c_path) for root in roots: if str(root) not in config['file_roots']['base']: config['file_roots']['base'].append(str(root)) utils.dump_yaml(self.c_path, config) self._client_init() def add_file_roots(self, roots: List[Path]): if not self.re_config: raise RuntimeError('re-configuration is not allowed') return self._add_file_roots(roots) def roster_data(self, roster_file=None): if roster_file: return utils.load_yaml(roster_file) else: return self._def_roster_data def roster_targets(self, roster_file=None): return list(self.roster_data(roster_file)) def roster_nodes(self, roster_file=None): return [ Node(target, params['host'], params['user'], params['post']) for target, params in self.roster_data(roster_file).items() ] def _run(self, cmd_args: SaltSSHArgs): salt_logger = logging.getLogger('salt.client.ssh') salt_log_level = None if cmd_args.secure and salt_logger.isEnabledFor(logging.DEBUG): salt_log_level = salt_logger.getEffectiveLevel() salt_logger.setLevel(logging.INFO) try: return self._client.cmd(*cmd_args.args, **cmd_args.kwargs) finally: if salt_log_level is not None: salt_logger.setLevel(salt_log_level) def run(self, fun: str, fun_args: Union[Tuple, None] = None, fun_kwargs: Union[Dict, None] = None, secure=False, **kwargs): for arg in ('roster_file', 'ssh_options', 'targets'): arg_v = kwargs.pop(arg, None) if arg_v is None: arg_v = getattr(self, arg) if arg_v: kwargs[arg] = (str(arg_v) if arg == 'roster_file' else arg_v) return super().run(fun, fun_args, fun_kwargs, secure, **kwargs) # TODO TEST EOS-8473 def ensure_access(self, targets: Optional[List] = None, bootstrap_roster_file=None): if not targets: targets = self.roster_targets() for target in targets: try: # try to reach using default (class-level) settings logger.debug(f"Checking access to '{target}'") self.run('uname', targets=target, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ('Permission denied' in reason): roster = utils.load_yaml(roster_file) priv_key = Path(roster.get(target, {}).get('priv')) self._add_file_roots([priv_key.parent]) logger.debug(f"Copying access key to '{target}'") if bootstrap_roster_file: # NOTE assumptions: # - python3 is already there since it can be # installed using the bootstrap key # (so salt modules might be used now) # - bootstrap key is availble in salt-ssh file roots # - access key is availble in salt-ssh file roots self.run('state.single', fun_args=['file.directory'], fun_kwargs=dict(name='~/.ssh', mode=700), targets=target, roster_file=bootstrap_roster_file) # inject production access public key # FIXME hardcoded 'root' self.run('state.single', fun_args=['ssh_auth.present'], fun_kwargs=dict( name=None, user=roster.get(target, {}).get('user', 'root'), source=f"salt://{priv_key.name}.pub"), targets=target, roster_file=bootstrap_roster_file) else: copy_id(host=roster.get(target, {}).get('host'), user=roster.get(target, {}).get('user'), port=roster.get(target, {}).get('port'), priv_key_path=priv_key, ssh_options=exc.cmd_args.get('kw').get( 'ssh_options'), force=True, target=target) else: raise # TODO TEST EOS-8473 def ensure_python3(self, targets: Optional[List] = None, roster_file=None): if not targets: targets = self.roster_targets() for target in targets: logger.debug(f"Ensuring python3 is installed on '{target}'") try: self.run('python3 --version', targets=target, roster_file=roster_file, raw_shell=True) except SaltCmdResultError as exc: reason = exc.reason.get(target) # TODO IMPROVE EOS-8473 better search string / regex roster_file = exc.cmd_args.get('kw').get('roster_file') if roster_file and ("not found" in reason): logger.debug(f"Installing python3 on '{target}'") self.run( 'yum install -y python3', targets=target, roster_file=roster_file, ssh_options=exc.cmd_args.get('kw').get('ssh_options'), raw_shell=True) else: raise @staticmethod def build_roster(nodes: List[Node], priv_key, roster_path): roster = { node.minion_id: { 'host': node.host, 'user': node.user, 'port': node.port, 'priv': str(priv_key) } for node in nodes } utils.dump_yaml(roster_path, roster) # TODO TEST EOS-8473 def ensure_ready( self, targets: Optional[List] = None, bootstrap_roster_file: Optional[Path] = None, ): if not targets: targets = self.roster_targets() if bootstrap_roster_file: self.ensure_python3(targets, roster_file=bootstrap_roster_file) self.ensure_access(targets, bootstrap_roster_file=bootstrap_roster_file) if not bootstrap_roster_file: self.ensure_python3(targets)