diff --git a/src/command_modules/azure-cli-appservice/HISTORY.rst b/src/command_modules/azure-cli-appservice/HISTORY.rst index 69c1adcb6c4..cfbaa56ccc9 100644 --- a/src/command_modules/azure-cli-appservice/HISTORY.rst +++ b/src/command_modules/azure-cli-appservice/HISTORY.rst @@ -3,6 +3,7 @@ Release History =============== * functionapp: add support for app insights on functionapp create +* webapp: bugfixes for webapp ssh 0.2.11 ++++++ diff --git a/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/custom.py b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/custom.py index 1b735573dc3..d55cdd95f9c 100644 --- a/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/custom.py +++ b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/custom.py @@ -4,7 +4,6 @@ # -------------------------------------------------------------------------------------------- from __future__ import print_function -import subprocess import threading import time @@ -21,6 +20,9 @@ import sys import OpenSSL.crypto +from fabric import Connection + + from knack.prompting import prompt_pass, NoTTYException from knack.util import CLIError from knack.log import get_logger @@ -2253,11 +2255,11 @@ def _ping_scm_site(cmd, resource_group, name): requests.get(scm_url + '/api/settings', headers=authorization) -def _check_for_ready_tunnel(tunnel_server): - return tunnel_server.is_port_set_to_default() +def is_webapp_up(tunnel_server): + return tunnel_server.is_webapp_up() -def create_tunnel(cmd, resource_group_name, name, port=None, slot=None): +def create_tunnel_and_session(cmd, resource_group_name, name, port=None, slot=None): webapp = show_webapp(cmd, resource_group_name, name, slot) is_linux = webapp.reserved if not is_linux: @@ -2281,15 +2283,14 @@ def create_tunnel(cmd, resource_group_name, name, port=None, slot=None): tunnel_server = TunnelServer('', port, host_name, profile_user_name, profile_user_password) _ping_scm_site(cmd, resource_group_name, name) + _wait_for_webapp(tunnel_server) + t = threading.Thread(target=_start_tunnel, args=(tunnel_server,)) t.daemon = True t.start() - _wait_for_tunnel(tunnel_server, False) - logger.warning("SSH is available ( username: %s, password: %s )", ssh_user_name, ssh_user_password) - - s = threading.Thread(target=_start_ssh, - args=('localhost', tunnel_server.get_port(), ssh_user_name)) + s = threading.Thread(target=_start_ssh_session, + args=('localhost', tunnel_server.get_port(), ssh_user_name, ssh_user_password)) s.daemon = True s.start() @@ -2297,25 +2298,50 @@ def create_tunnel(cmd, resource_group_name, name, port=None, slot=None): time.sleep(5) -def _wait_for_tunnel(tunnel_server, print_warnings): - if not _check_for_ready_tunnel(tunnel_server): - if print_warnings: - logger.warning('Tunnel is not ready yet, please wait (may take up to 1 minute)') - while True: - time.sleep(1) - if print_warnings: - logger.warning('.') - if _check_for_ready_tunnel(tunnel_server): - break +def _wait_for_webapp(tunnel_server): + tries = 0 + while True: + if is_webapp_up(tunnel_server): + break + if tries == 0: + logger.warning('Connection is not ready yet, please wait') + if tries == 60: + raise CLIError("Timeout Error, Unable to establish a connection") + tries = tries + 1 + logger.warning('.') + time.sleep(1) def _start_tunnel(tunnel_server): - _wait_for_tunnel(tunnel_server, True) tunnel_server.start_server() -def _start_ssh(host_name, port, user_name): - subprocess.call("ssh -o StrictHostKeyChecking=no {}@{} -p {}".format(user_name, host_name, port), shell=True) +def _start_ssh_session(hostname, port, username, password): + tries = 0 + while True: + try: + c = Connection(host=hostname, + port=port, + user=username, + # connect_timeout=60*10, + connect_kwargs={"password": password}) + break + except Exception as ex: # pylint: disable=broad-except + logger.info(ex) + if tries == 0: + logger.warning('Connection is not ready yet, please wait') + if tries == 60: + raise CLIError("Timeout Error, Unable to establish a connection") + tries = tries + 1 + logger.warning('.') + time.sleep(1) + try: + c.run('cat /etc/motd', pty=True) + c.run('source /etc/profile; /bin/ash', pty=True) + except Exception as ex: # pylint: disable=broad-except + logger.info(ex) + finally: + c.close() def ssh_webapp(cmd, resource_group_name, name, slot=None): # pylint: disable=too-many-statements @@ -2323,4 +2349,4 @@ def ssh_webapp(cmd, resource_group_name, name, slot=None): # pylint: disable=to if platform.system() == "Windows": raise CLIError('webapp ssh is only supported on linux and mac') else: - create_tunnel(cmd, resource_group_name, name, port=None, slot=slot) + create_tunnel_and_session(cmd, resource_group_name, name, port=None, slot=slot) diff --git a/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/fabric_license b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/fabric_license new file mode 100644 index 00000000000..aa3e5a4f739 --- /dev/null +++ b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/fabric_license @@ -0,0 +1,22 @@ +Copyright (c) 2018 Jeff Forcier. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/tunnel.py b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/tunnel.py index 15c663d2f11..ff136dbba3b 100644 --- a/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/tunnel.py +++ b/src/command_modules/azure-cli-appservice/azure/cli/command_modules/appservice/tunnel.py @@ -54,7 +54,7 @@ def __init__(self, local_addr, local_port, remote_addr, remote_user_name, remote self.sock.bind((self.local_addr, self.local_port)) if self.local_port == 0: self.local_port = self.sock.getsockname()[1] - logger.warning('Auto-selecting port: %s', self.local_port) + logger.info('Auto-selecting port: %s', self.local_port) logger.info('Finished initialization') def create_basic_auth(self): @@ -69,11 +69,11 @@ def is_port_open(self): if sock.connect_ex(('', self.local_port)) == 0: logger.info('Port %s is NOT open', self.local_port) else: - logger.warning('Port %s is open', self.local_port) + logger.info('Port %s is open', self.local_port) is_port_open = True return is_port_open - def is_port_set_to_default(self): + def is_webapp_up(self): import certifi import urllib3 try: @@ -97,7 +97,8 @@ def is_port_set_to_default(self): msg = r.read().decode('utf-8') logger.info('Status response message: %s', msg) if 'FAIL' in msg.upper(): - logger.warning('WARNING - Remote debugging may not be setup properly. Reponse content: %s', msg) + logger.info('WARNING - Remote debugging may not be setup properly. Reponse content: %s', msg) + return False if '2222' in msg: return True return False @@ -108,7 +109,7 @@ def _listen(self): basic_auth_string = self.create_basic_auth() while True: self.client, _address = self.sock.accept() - self.client.settimeout(1800) + self.client.settimeout(60 * 20) host = 'wss://{}{}'.format(self.remote_addr, '.scm.azurewebsites.net/AppServiceTunnel/Tunnel.ashx') basic_auth_header = 'Authorization: Basic {}'.format(basic_auth_string) cli_logger = get_logger() # get CLI logger which has the level set through command lines @@ -117,13 +118,14 @@ def _listen(self): logger.info('Websocket tracing enabled') websocket.enableTrace(True) else: - logger.warning('Websocket tracing disabled, use --verbose flag to enable') + logger.info('Websocket tracing disabled, use --verbose flag to enable') websocket.enableTrace(False) self.ws = create_connection(host, sockopt=((socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),), class_=TunnelWebSocket, header=[basic_auth_header], sslopt={'cert_reqs': ssl.CERT_NONE}, + timeout=60 * 20, enable_multithread=True) logger.info('Websocket, connected status: %s', self.ws.connected) index = index + 1 @@ -133,15 +135,15 @@ def _listen(self): debugger_thread.start() web_socket_thread.start() logger.info('Both debugger and websocket threads started...') - logger.warning('Successfully connected to local server..') + logger.info('Successfully connected to local server..') debugger_thread.join() web_socket_thread.join() logger.info('Both debugger and websocket threads stopped...') - logger.warning('Stopped local server..') + logger.info('Stopped local server..') def _listen_to_web_socket(self, client, ws_socket, index): - while True: - try: + try: + while True: logger.info('Waiting for websocket data, connection status: %s, index: %s', ws_socket.connected, index) data = ws_socket.recv() logger.info('Received websocket data: %s, index: %s', data, index) @@ -152,17 +154,17 @@ def _listen_to_web_socket(self, client, ws_socket, index): client.sendall(response) logger.info('Done sending to debugger, index: %s', index) else: - logger.info('Client disconnected!, index: %s', index) - client.close() - ws_socket.close() break - except: - client.close() - ws_socket.close() + except Exception as ex: # pylint: disable=broad-except + logger.info(ex) + finally: + logger.info('Client disconnected!, index: %s', index) + client.close() + ws_socket.close() def _listen_to_client(self, client, ws_socket, index): - while True: - try: + try: + while True: logger.info('Waiting for debugger data, index: %s', index) buf = bytearray(4096) nbytes = client.recv_into(buf, 4096) @@ -173,14 +175,14 @@ def _listen_to_client(self, client, ws_socket, index): ws_socket.send_binary(responseData) logger.info('Done sending to websocket, index: %s', index) else: - logger.warning('Client disconnected %s', index) - client.close() - ws_socket.close() break - except: - traceback.print_exc(file=sys.stdout) - client.close() - ws_socket.close() + except Exception as ex: # pylint: disable=broad-except + logger.info(ex) + logger.warning("Connection Timed Out") + finally: + logger.info('Client disconnected %s', index) + client.close() + ws_socket.close() def start_server(self): self._listen() diff --git a/src/command_modules/azure-cli-appservice/setup.py b/src/command_modules/azure-cli-appservice/setup.py index 12f63b84352..d8fe38f697b 100644 --- a/src/command_modules/azure-cli-appservice/setup.py +++ b/src/command_modules/azure-cli-appservice/setup.py @@ -38,6 +38,8 @@ # v1.17 breaks on wildcard cert https://github.com/shazow/urllib3/issues/981 'urllib3[secure]>=1.18', 'xmltodict', + 'fabric>=2.4', + 'cryptography<2.5', 'pyOpenSSL', 'six', 'vsts-cd-manager<1.1.0',