def _shell_command(self, command): """Execute an underlying shell command The _shell_command() is an internal API that is invoked as part of the public command() API should the user context be that of an underlying shell (e.g. non-ArcOS CLI). Args: command: A string value representing a single shell command. Returns: A Reply() object indicating success/failure along with a message. If the reply is successful, the appropriate data payload is returned in the message field. If the reply is a failure, an appropriate error code is set along with a message description indicating the failure. """ if self.session.local is False: stdin, stdout, stderr = self.session.exec_command(command) response = Reply() if stdout.channel.recv_exit_status() != 0: response.error = Error.SHELL_ERROR response.message = stderr.read().rstrip() else: response.error = None response.message = stdout.read() return response else: self.child = pexpect.spawn(command, encoding='UTF-8') self.child.expect(pexpect.EOF, timeout=self.timeout) result = self.child.before response = Reply() response.error = None response.message = result return response
def _check_expect_response(self, command, match, timeout=None, close_child=True): """Check a command response against a match string Alternative function to _wait which uses expect to match an argument against the return buffer as a result of a previous sendline. Args: command: A string representing the command previously sent that corresponds to the buffer returned. match: A string indicating a word to match in the buffer response. timeout: A integer respresenting the time pexpect waits for command to return. If the caller doesn't set this value the timeout value will be equal to the object level timeout close_child: Boolean value that instructs if the child should be terminated Returns: A tuple indicating success (True) or failure (False) along with a Reply() object. If the reply is successful, the appropriate data payload is returned in the message field. If the reply is a failure, an appropriate error code is set along with a message description indicating the failure. """ response = Reply() timeout = self.timeout if timeout is None else timeout try: self.child.expect(match, timeout) return (True, None) except pexpect.EOF: # we caught a pexpect.EOF response.error = Error.PROCESS_UNAVAILABLE response.message = 'Management daemon not available' if close_child: if self.child.isalive(): self.child.close(force=True) return (False, response) except pexpect.TIMEOUT: # we caught a pexpect.TIMEOUT response.error = Error.PROCESS_TIMEOUT response.message = 'Command [{}] timed out.'.format(command) if close_child: if self.child.isalive(): self.child.close(force=True) return (False, response)
def _validate_buffer(self, command, buf): """Validate the contents of command responses Given an input buffer message as a response to a previously sent command, validate the contents of the message and pack an appropriate error and message within a Reply() object. Args: command: A string representing the command issued buf: A string representing the return buffer Returns: A Reply() object indicating success/failure along with a message. If the reply is successful, the appropriate data payload is returned in the message field. If the reply is a failure, an appropriate error code is set along with a message description indicating the failure. """ error = None message = '' if '----^' in buf: message = 'Syntax Error: [{}]'.format(command.strip()) error = Error.INVALID_COMMAND elif 'Commit complete' in buf: message = 'Commit Successful' elif 'No modifications to commit' in buf: message = 'No modifications to commit' error = Error.COMMIT_NO_MODIFICATIONS elif 'Aborted' in buf: message = '{} [{}]'.format(self._cleanse_buffer(buf), command) error = Error.OPERATION_ABORTED elif 'Error' in buf: message = '{}'.format(self._cleanse_buffer(buf)) error = Error.OPERATION_ERROR elif 'Subsystem started' in buf: message = '{}'.format(self._cleanse_buffer(buf)) error = Error.OPERATION_ERROR elif 'Validation complete' in buf: message = 'Validation Successful' else: message = buf response = Reply() response.error = error response.message = message return response
def _interactive_command(self, commands, **kwargs): """Execute a set of interactive CLI commands Internal only API that is front-ended by all public facing APIs. This code path is executed for configuration or operational commands should the context of the user session be within the ArcOS CLI. Args: commands: A list of commands to be executed. kwargs: A dict of keyword arguments passed to respective functions. Returns: A Reply() object indicating success/failure along with a message. If the reply is successful, the appropriate data payload is returned in the message field. If the reply is a failure, an appropriate error code is set along with a message description indicating the failure. """ response = Reply() if 'timeout' in kwargs: self.timeout = kwargs['timeout'] if 'cli' in kwargs: if kwargs['cli'] is not None: command = 'arcos_cli' if self.session.local is True: self.child = pexpect.spawnu(command) self.arcos_shell = True else: self._send_command(command) self._wait(self.shell_prompt) self.arcos_shell = True if 'shell' in kwargs: if kwargs['shell']: commands = ['bash ' + command for command in commands] self.arcos_shell = True if self.arcos_shell: prompts = { 'prompt1': self.oper_prompt, 'prompt2': self.conf_prompt } if self.session.local is True: retry = 0 for prompt in prompts: success = False retry = 0 ## adding a retry to deal with odd timing issues after arcos_cli ## is initially spawned while retry < 3 and not success: command = '%s %s' % (prompt, prompts[prompt]) self.child.sendline(command) (success, response) = self._check_expect_response( command, self.oper_prompt, timeout=5, close_child=False) if not success: retry += 1 if not success: return response response = self._validate_buffer(command, self.child.before) if response.error is not None: return response command = 'paginate false' self.child.sendline(command) (success, response) = self._check_expect_response( command, self.oper_prompt) if not success: return response response = self._validate_buffer(command, self.child.before) if response.error is not None: return response else: for prompt in prompts: command = '%s %s' % (prompt, prompts[prompt]) self._send_command(command) buf = self._wait(self.oper_prompt) if isinstance(buf, Reply): response = buf else: response = self._validate_buffer(command, buf) if response.error is not None: return response command = 'paginate false' self._send_command(command) buf = self._wait(self.oper_prompt) if isinstance(buf, Reply): response = buf else: response = self._validate_buffer(command, buf) if response.error is not None: return response commit = False commit_response = None exit_commands = ['commit', 'show configuration diff', 'validate'] if any(x in commands for x in exit_commands): commit_response = self._commit_handler(commands) commit = True if commit is False: for command in commands: if 'encoding' in kwargs: encoding = kwargs['encoding'] if encoding is not None: command = command + ' | display %s' % (encoding) if self.arcos_shell: if self.session.local is True: try: self.child.sendline(command) self.child.expect(self.oper_prompt, timeout=self.timeout) response = self._validate_buffer( command, self.child.before) if response.error is not None: return response buf = response.message except pexpect.TIMEOUT: return Reply( Error.PROCESS_TIMEOUT, 'Command [{}] timed out'.format(command)) else: self._send_command(command) buf = self._wait(self.oper_prompt) else: return self._shell_command(command) ## For now, each call to _interactive_command() spawns a ## new shell. When this is the case, ensure there is some ## cleanup to ensure that locks are not being held for ## future callers ## ## TODO: Move session closure for handling at the manager ## (session) level if self.arcos_shell: if self.session.local is True: self.child.close() if commit: response.error = commit_response.error response.message = commit_response.message else: response.error = None response.message = self._cleanse_buffer(buf) return response
def _noninteractive_command(self, commands, **kwargs): """Execute a set of noninteractive CLI commands Internal only API that is front-ended by all public facing APIs. This code path is executed for configuration or operational commands should the context of the user session be outside of the ArcOS CLI. Non-interactive commands are written to a temporary file and piped directly into the ArcOS CLI shell vs. an interactive expect based mechanism. Args: commands: A list of commands to be executed. kwargs: A dict of keyword arguments passed to respective functions. Returns: A Reply() object indicating success/failure along with a message. If the reply is successful, the appropriate data payload is returned in the message field. If the reply is a failure, an appropriate error code is set along with a message description indicating the failure. """ response = Reply() # will handle delete outside the scope of tempfile tf = tempfile.NamedTemporaryFile(delete=False) for command in commands: tf.write('{}\n'.format(command)) tf.flush() tf.close() try: command_status = run_cmdfile(tf.name) os.remove(tf.name) except subprocess.CalledProcessError as e: respone.error = Error.OPERATION_ERROR response.message = json.dumps({ 'rc': 255, 'message': command_status[1] }) os.remove(tf.name) return response # returncode non-zero if command_status[0] != 0: # spawning confd_cli with -s so when a command/commit/validate fails # it should return a non-zero exit code val_buf = self._validate_buffer('', command_status[1]) response.error = val_buf.error response.message = json.dumps({ 'rc': command_status[0], 'message': val_buf.message }) return response else: # should only hit this when a valid command is sent through val_buf = self._validate_buffer('', command_status[1]) response.error = val_buf.error response.message = json.dumps({ 'rc': command_status[0], 'message': val_buf.message }) return response