From e96e7dcab712e8c9f57df5afb0c10d64f2fe6788 Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Fri, 18 Jan 2019 18:30:09 -0800 Subject: [PATCH 01/63] Remove login role This replaces references to the login role with the login property, a load_balancer role, or the shadow role where appropriate. --- appscale/tools/appscale_stats.py | 12 +-- appscale/tools/appscale_tools.py | 100 +++++++++++++++-------- appscale/tools/local_state.py | 21 +---- appscale/tools/node_layout.py | 33 +++----- appscale/tools/remote_helper.py | 25 ++++-- test/test_appscale_add_instances.py | 2 +- test/test_appscale_describe_instances.py | 9 +- test/test_appscale_gather_logs.py | 4 +- test/test_appscale_get_property.py | 2 +- test/test_appscale_relocate_app.py | 4 +- test/test_appscale_remove_app.py | 4 +- test/test_appscale_reset_pwd.py | 4 +- test/test_appscale_run_instances.py | 18 ++-- test/test_appscale_set_property.py | 2 +- test/test_appscale_upload_app.py | 12 +-- test/test_ip_layouts.py | 3 - test/test_local_state.py | 3 +- test/test_node_layout.py | 38 +-------- test/test_remote_helper.py | 14 ++-- test/test_stats.py | 15 ++-- 20 files changed, 157 insertions(+), 168 deletions(-) diff --git a/appscale/tools/appscale_stats.py b/appscale/tools/appscale_stats.py index 890dfcd8..8a558e19 100644 --- a/appscale/tools/appscale_stats.py +++ b/appscale/tools/appscale_stats.py @@ -58,7 +58,7 @@ def _get_stats(keyname, stats_kind, include_lists): A dict of statistics. A dict of failures. """ - login_host = LocalState.get_login_host(keyname=keyname) + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') secret = LocalState.get_secret_key(keyname=keyname) administration_port = "17441" stats_path = "/stats/cluster/{stats_kind}".format(stats_kind=stats_kind) @@ -66,7 +66,7 @@ def _get_stats(keyname, stats_kind, include_lists): headers = {'Appscale-Secret': secret} data = {'include_lists': include_lists} url = "https://{ip}:{port}{path}".format( - ip=login_host, + ip=load_balancer_ip, port=administration_port, path=stats_path ) @@ -341,12 +341,12 @@ def get_roles(keyname): Returns: A dict in which each key is an ip and value is a role list. """ - login_host = LocalState.get_login_host(keyname=keyname) - login_acc = AppControllerClient( - host=login_host, + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') + acc = AppControllerClient( + host=load_balancer_ip, secret=LocalState.get_secret_key(keyname) ) - cluster_stats = login_acc.get_cluster_stats() + cluster_stats = acc.get_cluster_stats() roles_data = { node["private_ip"]: (node["roles"] if len(node["roles"]) > 0 else ["?"]) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 98215835..fbd4fea5 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -154,8 +154,9 @@ def add_instances(cls, options): # Finally, find an AppController and send it a message to add # the given nodes with the new roles. AppScaleLogger.log("Sending request to add instances") - login_ip = LocalState.get_login_host(options.keyname) - acc = AppControllerClient(login_ip, LocalState.get_secret_key( + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') + acc = AppControllerClient(load_balancer_ip, LocalState.get_secret_key( options.keyname)) acc.start_roles_on_nodes(json.dumps(options.ips)) @@ -231,11 +232,12 @@ def print_cluster_status(cls, options): passed in via the command-line interface. """ try: - login_host = LocalState.get_login_host(options.keyname) - login_acc = AppControllerClient(login_host, - LocalState.get_secret_key(options.keyname)) - all_private_ips = login_acc.get_all_private_ips() - cluster_stats = login_acc.get_cluster_stats() + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') + acc = AppControllerClient( + load_balancer_ip, LocalState.get_secret_key(options.keyname)) + all_private_ips = acc.get_all_private_ips() + cluster_stats = acc.get_cluster_stats() except (faultType, AppControllerException, BadConfigurationException): AppScaleLogger.warn("AppScale deployment is probably down") raise @@ -262,6 +264,11 @@ def print_cluster_status(cls, options): cls._print_services(services) cls._print_status_alerts(nodes) + try: + login_host = acc.get_property('login')['login'] + except KeyError: + raise AppControllerException('login property not found') + dashboard = next( (service for service in services if service.http == RemoteHelper.APP_DASHBOARD_PORT), None) @@ -445,9 +452,10 @@ def gather_logs(cls, options): raise AppScaleException("Can't gather logs, as the location you " + \ "specified, {}, already exists.".format(location)) - login_host = LocalState.get_login_host(options.keyname) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') secret = LocalState.get_secret_key(options.keyname) - acc = AppControllerClient(login_host, secret) + acc = AppControllerClient(load_balancer_ip, secret) try: all_ips = acc.get_all_public_ips() @@ -566,9 +574,10 @@ def relocate_app(cls, options): if the AppController rejects the request to relocate the application (in which case it includes the reason why the rejection occurred). """ - login_host = LocalState.get_login_host(options.keyname) - acc = AppControllerClient(login_host, LocalState.get_secret_key( - options.keyname)) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') + acc = AppControllerClient( + load_balancer_ip, LocalState.get_secret_key(options.keyname)) version_key = '_'.join([options.appname, DEFAULT_SERVICE, DEFAULT_VERSION]) app_info_map = acc.get_app_info_map() @@ -577,6 +586,11 @@ def relocate_app(cls, options): "running in this AppScale cloud, so we can't move it to a different " \ "port.".format(options.appname)) + try: + login_host = acc.get_property('login')['login'] + except KeyError: + raise AppControllerException('login property not found') + acc.relocate_version(version_key, options.http_port, options.https_port) AppScaleLogger.success( 'Successfully issued request to move {0} to ports {1} and {2}'.format( @@ -605,9 +619,10 @@ def remove_app(cls, options): if response.lower() not in ['y', 'yes']: raise AppScaleException("Cancelled application removal.") - login_host = LocalState.get_login_host(options.keyname) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') secret = LocalState.get_secret_key(options.keyname) - admin_client = AdminClient(login_host, secret) + admin_client = AdminClient(load_balancer_ip, secret) for service_id in admin_client.list_services(options.project_id): AppScaleLogger.log('Deleting service: {}'.format(service_id)) @@ -657,9 +672,10 @@ def remove_service(cls, options): if response.lower() not in ['y', 'yes']: raise AppScaleException("Cancelled service removal.") - login_host = LocalState.get_login_host(options.keyname) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') secret = LocalState.get_secret_key(options.keyname) - admin_client = AdminClient(login_host, secret) + admin_client = AdminClient(load_balancer_ip, secret) cls._remove_service(admin_client, options.project_id, options.service_id) AppScaleLogger.success('Done shutting down service {} for {}.'.format( options.project_id, options.service_id)) @@ -673,11 +689,12 @@ def reset_password(cls, options): passed in via the command-line interface. """ secret = LocalState.get_secret_key(options.keyname) - login_host = LocalState.get_login_host(options.keyname) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') username, password = LocalState.get_credentials(is_admin=False) encrypted_password = LocalState.encrypt_password(username, password) - acc = AppControllerClient(login_host,secret) + acc = AppControllerClient(load_balancer_ip, secret) try: acc.reset_password(username, encrypted_password) @@ -702,13 +719,15 @@ def create_user(cls, options, is_admin): the AppController crashed. """ secret = LocalState.get_secret_key(options.keyname) - login_host = LocalState.get_login_host(options.keyname) + load_balancer_ip = LocalState.get_host_with_role( + options.keyname, 'load_balancer') username, password = LocalState.get_credentials(is_admin) - acc = AppControllerClient(login_host, secret) + acc = AppControllerClient(load_balancer_ip, secret) - RemoteHelper.create_user_accounts(username, password, login_host, options.keyname) + RemoteHelper.create_user_accounts( + username, password, load_balancer_ip, options.keyname) try: if is_admin: @@ -824,13 +843,19 @@ def run_instances(cls, options): # Wait for machines to finish loading and AppScale Dashboard to be deployed. RemoteHelper.wait_for_machines_to_finish_loading(head_node, options.keyname) - RemoteHelper.sleep_until_port_is_open(LocalState.get_login_host( - options.keyname), RemoteHelper.APP_DASHBOARD_PORT, options.verbose) + + try: + login_host = acc.get_property('login')['login'] + except KeyError: + raise AppControllerException('login property not found') + + RemoteHelper.sleep_until_port_is_open( + login_host, RemoteHelper.APP_DASHBOARD_PORT, options.verbose) AppScaleLogger.success("AppScale successfully started!") - AppScaleLogger.success("View status information about your AppScale " + \ - "deployment at http://{0}:{1}".format(LocalState.get_login_host( - options.keyname), RemoteHelper.APP_DASHBOARD_PORT)) + AppScaleLogger.success( + 'View status information about your AppScale deployment at ' + 'http://{}:{}'.format(login_host, RemoteHelper.APP_DASHBOARD_PORT)) AppScaleLogger.remote_log_tools_state(options, my_id, "finished", APPSCALE_VERSION) @@ -985,9 +1010,10 @@ def upload_app(cls, options): 'current supported SDK version is ' '{}.'.format(AppEngineHelper.SUPPORTED_SDK_VERSION)) - login_host = LocalState.get_login_host(options.keyname) + head_node_public_ip = LocalState.get_host_with_role( + options.keyname, 'shadow') secret_key = LocalState.get_secret_key(options.keyname) - admin_client = AdminClient(login_host, secret_key) + admin_client = AdminClient(head_node_public_ip, secret_key) remote_file_path = RemoteHelper.copy_app_to_host( file_location, version.project_id, options.keyname, options.verbose, @@ -1022,8 +1048,10 @@ def upload_app(cls, options): if created_dir: shutil.rmtree(file_location) - http_port = int(version_url.split(':')[-1]) - return (login_host, http_port) + match = re.match('http://(.+):(\d+)', version_url) + login_host = match.group(1) + http_port = int(match.group(2)) + return login_host, http_port @classmethod def update_cron(cls, source_location, keyname, project_id): @@ -1066,9 +1094,9 @@ def update_cron(cls, source_location, keyname, project_id): cron_jobs = yaml.safe_load(cron_config) AppScaleLogger.log('Updating cron jobs') - login_host = LocalState.get_login_host(keyname) + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') secret_key = LocalState.get_secret_key(keyname) - admin_client = AdminClient(login_host, secret_key) + admin_client = AdminClient(load_balancer_ip, secret_key) admin_client.update_cron(version.project_id, cron_jobs) @classmethod @@ -1106,9 +1134,9 @@ def update_indexes(cls, source_location, keyname, project_id): return AppScaleLogger.log('Updating indexes') - login_host = LocalState.get_login_host(keyname) + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') secret_key = LocalState.get_secret_key(keyname) - admin_client = AdminClient(login_host, secret_key) + admin_client = AdminClient(load_balancer_ip, secret_key) admin_client.update_indexes(version.project_id, indexes) @classmethod @@ -1152,9 +1180,9 @@ def update_queues(cls, source_location, keyname, project_id): queues = yaml.safe_load(queue_config) AppScaleLogger.log('Updating queues') - login_host = LocalState.get_login_host(keyname) + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') secret_key = LocalState.get_secret_key(keyname) - admin_client = AdminClient(login_host, secret_key) + admin_client = AdminClient(load_balancer_ip, secret_key) admin_client.update_queues(version.project_id, queues) @classmethod diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 0fe51909..769a8e59 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -98,18 +98,19 @@ def ensure_appscale_isnt_running(cls, keyname, force): if os.path.exists(cls.get_secret_key_location(keyname)): try: - login_host = cls.get_login_host(keyname) + load_balancer_ip = cls.get_host_with_role(keyname, 'load_balancer') secret_key = cls.get_secret_key(keyname) except (IOError, AppScaleException, BadConfigurationException): # If we don't have the locations files, we are not running. return - acc = AppControllerClient(login_host, secret_key) + acc = AppControllerClient(load_balancer_ip, secret_key) try: acc.get_all_public_ips() except AppControllerException: # AC is not running, so we assume appscale is not up and running. - AppScaleLogger.log("AppController not running on login node.") + AppScaleLogger.log( + "AppController not running on {}".format(load_balancer_ip)) else: raise BadConfigurationException("AppScale is already running. Terminate" + " it, set 'force: True' in your AppScalefile, or use the --force flag" + @@ -617,20 +618,6 @@ def encrypt_password(cls, username, password): return hashlib.sha1(username + password).hexdigest() - @classmethod - def get_login_host(cls, keyname): - """Searches through the local metadata to see which virtual machine runs the - login service. - - Args: - keyname: The SSH keypair name that uniquely identifies this AppScale - deployment. - Returns: - A str containing the host that runs the login service. - """ - return cls.get_host_with_role(keyname, 'login') - - @classmethod def get_host_with_role(cls, keyname, role): """Searches through the local metadata to see which virtual machine runs the diff --git a/appscale/tools/node_layout.py b/appscale/tools/node_layout.py index 8329defb..ccdbf33e 100644 --- a/appscale/tools/node_layout.py +++ b/appscale/tools/node_layout.py @@ -40,16 +40,18 @@ class NodeLayout(): # A tuple containing the keys that can be used in advanced deployments. # TODO: remove 'appengine' role. - ADVANCED_FORMAT_KEYS = ['master', 'database', 'appengine', 'compute', 'open', - 'login', 'zookeeper', 'memcache', 'taskqueue', 'search', 'load_balancer'] + ADVANCED_FORMAT_KEYS = [ + 'master', 'database', 'appengine', 'compute', 'open', 'zookeeper', + 'memcache', 'taskqueue', 'search', 'load_balancer'] # A tuple containing all of the roles (simple and advanced) that the # AppController recognizes. These include _master and _slave roles, which # the user may not be able to specify directly. # TODO: remove 'appengine' role. - VALID_ROLES = ('master', 'appengine', 'compute', 'database', 'shadow', 'open', - 'load_balancer', 'login', 'db_master', 'db_slave', 'zookeeper', 'memcache', + VALID_ROLES = ( + 'master', 'appengine', 'compute', 'database', 'shadow', 'open', + 'load_balancer', 'db_master', 'db_slave', 'zookeeper', 'memcache', 'taskqueue', 'taskqueue_master', 'taskqueue_slave', 'search') @@ -205,13 +207,11 @@ def validate_node_layout(self): 'memcache': 0, 'taskqueue': 0, 'zookeeper': 0, - 'login': 0, 'db_master': 0, 'taskqueue_master': 0 } node_count = 0 all_disks = [] - login_found = False # Loop through the list of "node sets", which are grouped by role. for node_set in self.input_yaml: # If the key nodes is mapped to an integer it should be a cloud @@ -290,8 +290,7 @@ def validate_node_layout(self): for node in nodes: for role in roles: node.add_role(role) - if role == 'login': - node.public_ip = self.login_host or node.public_ip + node.instance_type = instance_type if not node.is_valid(): self.invalid(",".join(node.errors())) @@ -300,12 +299,6 @@ def validate_node_layout(self): # so get the updated list of roles from the first node. roles = nodes[0].roles - # Check for login after roles have been expanded. - if 'login' in roles and login_found: - self.invalid("Only one login is allowed.") - elif 'login' in roles: - login_found = True - # Update dictionary containing role counts. role_count.update({role: role_count.get(role, 0) + len(nodes) for role in roles}) @@ -407,9 +400,6 @@ def distribute_unassigned_roles(self, nodes, role_count): elif role == 'taskqueue': self.master.add_role('taskqueue') self.master.add_role('taskqueue_master') - elif role == 'login': - self.master.add_role('login') - self.master.public_ip = self.login_host or self.master.public_ip elif role == 'db_master': # Get first database node. db_node = self.get_nodes('database', True, nodes)[0] @@ -436,8 +426,8 @@ def generate_cloud_layout(self): Returns: A dict that has one controller node and the other nodes set as servers. """ - master_node_roles = ['master', 'database', 'memcache', 'login', - 'zookeeper', 'taskqueue'] + master_node_roles = ['master', 'database', 'memcache', 'zookeeper', + 'taskqueue'] layout = [{'roles' : master_node_roles, 'nodes' : 1}] if self.min_machines == 1: layout[0]['roles'].append('appengine') @@ -718,7 +708,7 @@ def errors(self): def expand_roles(self): """Converts the 'master' composite role into the roles it represents, and - adds dependencies necessary for the 'login' and 'database' roles. + adds dependencies necessary for the 'database' role. """ for i in range(len(self.roles)): role = self.roles[i] @@ -733,9 +723,6 @@ def expand_roles(self): self.roles.append('shadow') self.roles.append('load_balancer') - if 'login' in self.roles: - self.roles.append('load_balancer') - # TODO: remove these, db_slave and taskqueue_slave are currently deprecated. if 'db_slave' in self.roles or 'db_master' in self.roles \ and 'database' not in self.roles: diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index 663d8a7a..9560310c 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -108,21 +108,23 @@ def start_all_nodes(cls, options, node_layout): # If we have running instances under the current keyname, we try to # re-attach to them. If we have issue finding the locations file or the # IP of the head node, we throw an exception. - login_ip = None + head_node_public_ip = None public_ips, private_ips, instance_ids = agent.describe_instances(params) if public_ips: try: - login_ip = LocalState.get_login_host(options.keyname) + head_node_public_ip = LocalState.get_host_with_role( + options.keyname, 'shadow') except (IOError, BadConfigurationException): raise AppScaleException( "Couldn't get login ip for running deployment with keyname" " {}.".format(options.keyname)) - if login_ip not in public_ips: + + if head_node_public_ip not in public_ips: raise AppScaleException( "Couldn't recognize running instances for deployment with" " keyname {}.".format(options.keyname)) - if login_ip in public_ips: + if head_node_public_ip in public_ips: AppScaleLogger.log("Reusing already running instances.") # Get the node_info from the locations JSON. node_info = LocalState.get_local_nodes_info(keyname=options.keyname) @@ -838,7 +840,13 @@ def create_user_accounts(cls, email, password, public_ip, keyname): # means their XMPP account name is a@login_ip username_regex = re.compile('\A(.*)@') username = username_regex.match(email).groups()[0] - xmpp_user = "{0}@{1}".format(username, LocalState.get_login_host(keyname)) + + try: + login_host = acc.get_property('login')['login'] + except KeyError: + raise AppControllerException('login property not found') + + xmpp_user = "{0}@{1}".format(username, login_host) xmpp_pass = LocalState.encrypt_password(xmpp_user, password) is_xmpp_user_exist = acc.does_user_exist(xmpp_user) @@ -847,7 +855,7 @@ def create_user_accounts(cls, email, password, public_ip, keyname): AppScaleLogger.log("XMPP User {0} conflict!".format(xmpp_user)) generated_xmpp_username = LocalState.generate_xmpp_username(username) - xmpp_user = "{0}@{1}".format(generated_xmpp_username, LocalState.get_login_host(keyname)) + xmpp_user = "{0}@{1}".format(generated_xmpp_username, login_host) xmpp_pass = LocalState.encrypt_password(xmpp_user, password) acc.create_user(xmpp_user, xmpp_pass) @@ -1130,8 +1138,9 @@ def copy_app_to_host(cls, app_location, app_id, keyname, is_verbose, AppScaleLogger.log("Copying over application") remote_app_tar = "{0}/{1}.tar.gz".format(cls.REMOTE_APP_DIR, app_id) - cls.scp(LocalState.get_login_host(keyname), keyname, local_tarred_app, - remote_app_tar, is_verbose) + head_node_public_ip = LocalState.get_host_with_role(keyname, 'shadow') + cls.scp(head_node_public_ip, keyname, local_tarred_app, remote_app_tar, + is_verbose) AppScaleLogger.verbose("Removing local copy of tarred application", is_verbose) diff --git a/test/test_appscale_add_instances.py b/test/test_appscale_add_instances.py index f4c46c54..dd4e8cdb 100644 --- a/test/test_appscale_add_instances.py +++ b/test/test_appscale_add_instances.py @@ -130,7 +130,7 @@ def test_add_nodes_in_virt_deployment(self): fake_nodes_json.should_receive('read').and_return(json.dumps({ "node_info": [{ "public_ip": "public1", "private_ip": "private1", - "jobs": ["shadow", "login"] }]})) + "jobs": ["shadow", "load_balancer"] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location(self.keyname), 'r') \ .and_return(fake_nodes_json) diff --git a/test/test_appscale_describe_instances.py b/test/test_appscale_describe_instances.py index 771d7ae3..e10dbbb9 100644 --- a/test/test_appscale_describe_instances.py +++ b/test/test_appscale_describe_instances.py @@ -32,7 +32,8 @@ def success(self, msg): class TestPrintAppscaleStatus(unittest.TestCase): def test_started_two_nodes(self): # Mock functions which provides inputs for print_cluster_status - flexmock(LocalState).should_receive("get_login_host").and_return("1.1.1.1") + flexmock(LocalState).should_receive("get_host_with_role").\ + and_return("1.1.1.1") flexmock(LocalState).should_receive("get_secret_key").and_return("xxxxxxx") fake_ac_client = flexmock() (flexmock(appscale_tools) @@ -47,7 +48,7 @@ def test_started_two_nodes(self): 'private_ip': '10.10.4.220', 'public_ip': '1.1.1.1', 'roles': ['load_balancer', 'taskqueue_master', 'zookeeper', - 'db_master','taskqueue', 'shadow', 'login'], + 'db_master','taskqueue', 'shadow'], 'is_initialized': True, 'is_loaded': True, 'apps': { @@ -85,6 +86,8 @@ def test_started_two_nodes(self): 'services': {}, } ] + fake_ac_client.should_receive('get_property').\ + and_return({'login': '1.1.1.1'}) (fake_ac_client.should_receive("get_cluster_stats") .and_return(cluster_stats)) @@ -114,7 +117,7 @@ def test_started_two_nodes(self): def test_deployment_looks_down(self): for err in (faultType, AppControllerException, BadConfigurationException): - flexmock(LocalState).should_receive("get_login_host").and_raise(err) + flexmock(LocalState).should_receive("get_host_with_role").and_raise(err) # Catch warning and check if it matches expectation (flexmock(appscale_tools.AppScaleLogger) diff --git a/test/test_appscale_gather_logs.py b/test/test_appscale_gather_logs.py index b3dacfa1..79d5acd2 100644 --- a/test/test_appscale_gather_logs.py +++ b/test/test_appscale_gather_logs.py @@ -77,7 +77,7 @@ def test_appscale_in_two_node_virt_deployment(self): "public_ip": "public1", "private_ip": "private1", "jobs": ["load_balancer", "taskqueue_master", "zookeeper", - "db_master", "taskqueue", "shadow", "login"] + "db_master", "taskqueue", "shadow"] }, { "public_ip": "public2", "private_ip": "private2", @@ -133,7 +133,6 @@ def test_appscale_in_two_node_virt_deployment(self): utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/db_master') utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/taskqueue') utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/shadow') - utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/login') utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/memcache') utils.should_receive('mkdir').with_args('/tmp/foobaz/symlinks/appengine') @@ -147,7 +146,6 @@ def test_appscale_in_two_node_virt_deployment(self): '/tmp/foobaz/symlinks/db_master/public1', '/tmp/foobaz/symlinks/taskqueue/public1', '/tmp/foobaz/symlinks/shadow/public1', - '/tmp/foobaz/symlinks/login/public1', ], '../../public2': [ '/tmp/foobaz/symlinks/private-ips/private2', diff --git a/test/test_appscale_get_property.py b/test/test_appscale_get_property.py index c250099f..983e6a10 100644 --- a/test/test_appscale_get_property.py +++ b/test/test_appscale_get_property.py @@ -61,7 +61,7 @@ def test_get_property(self): {"node_info": [{ 'public_ip': 'public1', 'private_ip': 'private1', - 'jobs': ['login', 'shadow'] + 'jobs': ['shadow'] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location(self.keyname), 'r') \ diff --git a/test/test_appscale_relocate_app.py b/test/test_appscale_relocate_app.py index 9e7afc61..8f52941d 100644 --- a/test/test_appscale_relocate_app.py +++ b/test/test_appscale_relocate_app.py @@ -53,7 +53,7 @@ def setUp(self): json.dumps({"node_info": [{ "public_ip": "public1", "private_ip": "private1", - "jobs": ["shadow", "login"] + "jobs": ["shadow", "load_balancer"] }]})) fake_nodes_json.should_receive('write').and_return() builtins = flexmock(sys.modules['__builtin__']) @@ -127,6 +127,8 @@ def test_all_ok(self): version_key = '{}_default_v1'.format(self.appid) fake_appcontroller.should_receive('relocate_version').with_args( version_key, 80, 443, 'the secret').and_return('OK') + fake_appcontroller.should_receive('get_property').\ + and_return('{"login":"public1"}') flexmock(SOAPpy) SOAPpy.should_receive('SOAPProxy').with_args('https://public1:17443') \ .and_return(fake_appcontroller) diff --git a/test/test_appscale_remove_app.py b/test/test_appscale_remove_app.py index 63884099..3a950031 100644 --- a/test/test_appscale_remove_app.py +++ b/test/test_appscale_remove_app.py @@ -71,7 +71,7 @@ def test_remove_app_but_app_isnt_running(self): json.dumps({"node_info": [{ "public_ip": "public1", "private_ip": "private1", - "jobs": ["shadow", "login"] + "jobs": ["shadow", "load_balancer"] }]})) fake_nodes_json.should_receive('write').and_return() builtins.should_receive('open').with_args( @@ -113,7 +113,7 @@ def test_remove_app_and_app_is_running(self): json.dumps({"node_info": [{ "public_ip": "public1", "private_ip": "private1", - "jobs": ["shadow", "login"] + "jobs": ["shadow", "load_balancer"] }]})) fake_nodes_json.should_receive('write').and_return() builtins.should_receive('open').with_args( diff --git a/test/test_appscale_reset_pwd.py b/test/test_appscale_reset_pwd.py index 1efff208..9b0ccf3e 100644 --- a/test/test_appscale_reset_pwd.py +++ b/test/test_appscale_reset_pwd.py @@ -67,7 +67,7 @@ def test_reset_password_for_user_that_exists(self): json.dumps({"node_info": [{ 'public_ip': 'public1', 'private_ip': 'private1', - 'jobs': ['login', 'db_master'] + 'jobs': ['load_balancer', 'db_master'] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location(self.keyname), 'r') \ @@ -120,7 +120,7 @@ def test_reset_password_for_user_that_doesnt_exist(self): json.dumps({"node_info": [{ 'public_ip': 'public1', 'private_ip': 'private1', - 'jobs': ['login', 'db_master'] + 'jobs': ['load_balancer', 'db_master'] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location(self.keyname), 'r') \ diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index 81e5b598..ad2beea6 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -213,7 +213,7 @@ def setup_appcontroller_mocks(self, public_ip, private_ip): role_info = [{ 'public_ip' : public_ip, 'private_ip' : private_ip, - 'jobs' : ['shadow', 'login'], + 'jobs' : ['shadow'], 'instance_id': 'i-APPSCALE' }] fake_appcontroller.should_receive('get_role_info').with_args('the secret') \ @@ -356,7 +356,7 @@ def test_appscale_in_one_node_virt_deployment(self): json.dumps([{ "public_ip": IP_1, "private_ip": IP_1, - "jobs": ["shadow", "login"] + "jobs": ["shadow"] }]))) # Assume the locations files were copied successfully. @@ -384,6 +384,8 @@ def test_appscale_in_one_node_virt_deployment(self): flexmock(AppControllerClient) AppControllerClient.should_receive('does_user_exist').and_return(True) + AppControllerClient.should_receive('get_property').\ + and_return({'login': IP_1}) # don't use a 192.168.X.Y IP here, since sometimes we set our virtual # machines to boot with those addresses (and that can mess up our tests). @@ -539,7 +541,7 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): json.dumps([{ "public_ip" : "elastic-ip", "private_ip" : "private1", - "jobs": ["shadow", "login"] + "jobs": ["shadow"] }]))) # copying over the locations yaml and json files should be fine @@ -553,6 +555,8 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): flexmock(RemoteHelper).should_receive('copy_deployment_credentials') flexmock(AppControllerClient) AppControllerClient.should_receive('does_user_exist').and_return(True) + AppControllerClient.should_receive('get_property').\ + and_return({'login': 'elastic-ip'}) # Let's mock the call to describe_instances when checking for old # instances to re-use, and then to start the headnode. @@ -699,7 +703,7 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): json.dumps([{ "public_ip" : "public1", "private_ip" : "private1", - "jobs" : ["shadow", "login"] + "jobs" : ["shadow"] }]))) # copying over the locations json file should be fine @@ -723,6 +727,8 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): flexmock(RemoteHelper).should_receive('copy_deployment_credentials') flexmock(AppControllerClient) AppControllerClient.should_receive('does_user_exist').and_return(True) + AppControllerClient.should_receive('get_property').\ + and_return({'login': 'public1'}) # Let's mock the call to describe_instances when checking for old # instances to re-use, and then to start the headnode. @@ -773,7 +779,7 @@ def test_appscale_in_one_node_virt_deployment_with_login_override(self): json.dumps([{ "public_ip" : IP_1, "private_ip" : IP_1, - "jobs" : ["shadow", "login"] + "jobs" : ["shadow"] }]))) self.local_state.should_receive('get_secret_key').and_return("fookey") @@ -794,6 +800,8 @@ def test_appscale_in_one_node_virt_deployment_with_login_override(self): AppControllerClient.should_receive('does_user_exist').and_return(True) AppControllerClient.should_receive('is_initialized').and_return(True) AppControllerClient.should_receive('set_admin_role').and_return('true') + AppControllerClient.should_receive('get_property').\ + and_return({'login': IP_1}) # don't use a 192.168.X.Y IP here, since sometimes we set our virtual # machines to boot with those addresses (and that can mess up our tests). diff --git a/test/test_appscale_set_property.py b/test/test_appscale_set_property.py index b16d3b79..2618b627 100644 --- a/test/test_appscale_set_property.py +++ b/test/test_appscale_set_property.py @@ -61,7 +61,7 @@ def test_get_property(self): json.dumps({"node_info": [{ 'public_ip': 'public1', 'private_ip': 'private1', - 'jobs': ['login', 'shadow'] + 'jobs': ['shadow'] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location(self.keyname), 'r') \ diff --git a/test/test_appscale_upload_app.py b/test/test_appscale_upload_app.py index 7952d8ff..21cb834e 100644 --- a/test/test_appscale_upload_app.py +++ b/test/test_appscale_upload_app.py @@ -240,11 +240,11 @@ def test_upload_app(self): app_id = 'guestbook' source_path = '{}.tar.gz'.format(app_id) extracted_dir = '/tmp/{}'.format(app_id) - login_host = '192.168.33.10' + head_node = '192.168.33.10' secret = 'secret-key' operation_id = 'operation-1' port = 8080 - version_url = 'http://{}:{}'.format(login_host, port) + version_url = 'http://{}:{}'.format(head_node, port) argv = ['--keyname', self.keyname, '--file', source_path, '--test'] options = ParseArgs(argv, self.function).args @@ -256,8 +256,8 @@ def test_upload_app(self): and_return('/tmp/{}'.format(app_id)) flexmock(Version).should_receive('from_tar_gz').and_return(version) flexmock(AppEngineHelper).should_receive('validate_app_id') - flexmock(LocalState).should_receive('get_login_host').\ - and_return(login_host) + flexmock(LocalState).should_receive('get_host_with_role').\ + and_return(head_node) flexmock(LocalState).should_receive('get_secret_key').and_return(secret) flexmock(RemoteHelper).should_receive('copy_app_to_host').\ with_args(extracted_dir, app_id, self.keyname, False, {}, None).\ @@ -270,7 +270,7 @@ def test_upload_app(self): flexmock(AppEngineHelper).should_receive('warn_if_version_defined') given_host, given_port = AppScaleTools.upload_app(options) - self.assertEquals(given_host, login_host) + self.assertEquals(given_host, head_node) self.assertEquals(given_port, port) # If provided user is not app admin, deployment should fail. @@ -285,7 +285,7 @@ def test_upload_app(self): flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) given_host, given_port = AppScaleTools.upload_app(options) - self.assertEquals(given_host, login_host) + self.assertEquals(given_host, head_node) self.assertEquals(given_port, port) diff --git a/test/test_ip_layouts.py b/test/test_ip_layouts.py index f1a1f77a..c9f0bd69 100644 --- a/test/test_ip_layouts.py +++ b/test/test_ip_layouts.py @@ -33,9 +33,6 @@ OPEN_NODE_CLOUD = [{'roles': ['master', 'database', 'appengine'], 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}, {'roles': 'open', 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}] -LOGIN_NODE_CLOUD = [{'roles': ['master', 'database', 'appengine'], 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}, - {'roles': 'login', 'nodes': 1, 'instance_type': INSTANCE_TYPE_2}] - FOUR_NODE_CLOUD = [{'roles': 'master', 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}, {'roles': 'appengine', 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}, {'roles': 'database', 'nodes': 1, 'instance_type': INSTANCE_TYPE_1}, diff --git a/test/test_local_state.py b/test/test_local_state.py index 9674e5c1..dfccf301 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -68,7 +68,8 @@ def test_ensure_appscale_isnt_running_but_it_is(self): os.path.should_receive('exists').with_args( LocalState.get_secret_key_location(self.keyname)).and_return(True) - flexmock(LocalState).should_receive('get_login_host').and_return('login_ip') + flexmock(LocalState).should_receive('get_host_with_role').\ + and_return('load_balancer') flexmock(LocalState).should_receive('get_secret_key').and_return('super-secret') (flexmock(AppControllerClient) .should_receive('get_all_public_ips').and_return("OK")) diff --git a/test/test_node_layout.py b/test/test_node_layout.py index 763a834c..91f83e8f 100644 --- a/test/test_node_layout.py +++ b/test/test_node_layout.py @@ -21,8 +21,7 @@ FOUR_NODE_CLUSTER, THREE_NODES_TWO_DISKS_CLOUD, THREE_NODES_TWO_DISKS_FOR_NODESET_CLOUD, THREE_NODE_CLOUD, TWO_NODES_ONE_NOT_UNIQUE_DISK_CLOUD, - TWO_NODES_TWO_DISKS_CLOUD, OPEN_NODE_CLOUD, - LOGIN_NODE_CLOUD) + TWO_NODES_TWO_DISKS_CLOUD, OPEN_NODE_CLOUD) class TestNodeLayout(unittest.TestCase): @@ -60,36 +59,6 @@ def test_advanced_format_yaml_only(self): layout_1 = NodeLayout(options) self.assertNotEqual([], layout_1.nodes) - def test_login_override_master(self): - # if the user wants to set a login host, make sure that gets set as the - # master node's public IP address instead of what we'd normally put in - - input_yaml_1 = FOUR_NODE_CLUSTER - options_1 = self.default_options.copy() - options_1['ips'] = input_yaml_1 - options_1['login_host'] = "www.booscale.com" - layout_1 = NodeLayout(options_1) - self.assertNotEqual([], layout_1.nodes) - - head_node = layout_1.head_node() - self.assertEquals(options_1['login_host'], head_node.public_ip) - - def test_login_override_login_node(self): - # if the user wants to set a login host, make sure that gets set as the - # login node's public IP address instead of what we'd normally put in - - # use a simple deployment so we can get the login node with .head_node() - input_yaml_1 = LOGIN_NODE_CLOUD - options_1 = self.default_options.copy() - options_1['ips'] = input_yaml_1 - options_1['login_host'] = "www.booscale.com" - layout_1 = NodeLayout(options_1) - self.assertNotEqual([], layout_1.nodes) - - login_nodes = layout_1.get_nodes(role='login', is_role=True) - self.assertEqual(1, len(login_nodes)) - self.assertEquals(options_1['login_host'], login_nodes[0].public_ip) - def test_is_database_replication_valid_with_db_slave(self): input_yaml = [{'roles': ['master', 'database', 'appengine'], 'nodes': 1, @@ -229,7 +198,7 @@ def test_new_with_right_number_of_unique_disks_one_node(self): reattach_node_info = [{ "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", "instance_id": "i-APPSCALE1", - "jobs": ['load_balancer', 'taskqueue', 'shadow', 'login', + "jobs": ['load_balancer', 'taskqueue', 'shadow', 'taskqueue_master'], "instance_type": "instance_type_1"}, { "public_ip": "0.0.0.0", @@ -289,7 +258,6 @@ def test_from_locations_json_list_after_clean(self): "private_ip": "0.0.0.0", "instance_id": "i-APPSCALE1", "jobs": ['load_balancer', 'taskqueue', 'shadow', - 'login', 'taskqueue_master'], "instance_type": "instance_type_1"}, {"public_ip": "0.0.0.0", @@ -363,7 +331,7 @@ def test_from_locations_json_list_invalid_locations(self): node_info = [{ "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", "instance_id": "i-APPSCALE1", - "jobs": ['load_balancer', 'taskqueue', 'shadow', 'login', + "jobs": ['load_balancer', 'taskqueue', 'shadow', 'taskqueue_master'] }, { "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", diff --git a/test/test_remote_helper.py b/test/test_remote_helper.py index 1d77ce9b..6a84a8f3 100644 --- a/test/test_remote_helper.py +++ b/test/test_remote_helper.py @@ -437,7 +437,7 @@ def test_create_user_accounts(self): json.dumps({"node_info": [{ "public_ip": "public1", "private_ip": "private1", - "jobs": ["shadow", "login"] + "jobs": ["shadow"] }]})) builtins.should_receive('open').with_args( LocalState.get_locations_json_location('bookey'), 'r') \ @@ -453,6 +453,8 @@ def test_create_user_accounts(self): 'the secret').and_return('false') fake_appcontroller.should_receive('create_user').with_args('boo@public1', str, 'xmpp_user', 'the secret').and_return('true') + fake_appcontroller.should_receive('get_property').\ + with_args('login', 'the secret').and_return('{"login":"public1"}') flexmock(SOAPpy) SOAPpy.should_receive('SOAPProxy').with_args('https://public1:17443') \ .and_return(fake_appcontroller) @@ -528,7 +530,7 @@ def test_wait_for_machines_to_finish_loading(self): reattach_node_info = [{ "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", "instance_id": "i-APPSCALE1", - "jobs": ['load_balancer', 'taskqueue', 'shadow', 'login', + "jobs": ['load_balancer', 'taskqueue', 'shadow', 'taskqueue_master'], "instance_type" : "instance_type_1"}, { "public_ip": "0.0.0.0", @@ -557,7 +559,7 @@ def test_start_all_nodes_reattach(self): with_args('euca'). \ and_return(fake_agent) - LocalState.should_receive('get_login_host').and_return(IP_1) + LocalState.should_receive('get_host_with_role').and_return(IP_1) LocalState.should_receive('get_local_nodes_info') \ .and_return(self.reattach_node_info) @@ -595,8 +597,6 @@ def test_start_all_nodes_reattach_changed_asf(self): with_args('public cloud'). \ and_return(fake_agent) - LocalState.should_receive('get_login_host').and_return('0.0.0.1') - LocalState.should_receive('get_local_nodes_info')\ .and_return(self.reattach_node_info) @@ -611,12 +611,12 @@ def test_start_all_nodes_reattach_changed_locations(self): with_args('public cloud'). \ and_return(fake_agent) - LocalState.should_receive('get_login_host').and_return('0.0.0.1') + LocalState.should_receive('get_host_with_role').and_return('0.0.0.1') node_info = [{ "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", "instance_id": "i-APPSCALE1", - "jobs": ['load_balancer', 'taskqueue', 'shadow', 'login', + "jobs": ['load_balancer', 'taskqueue', 'shadow', 'taskqueue_master'] }, { "public_ip": "0.0.0.0", "private_ip": "0.0.0.0", diff --git a/test/test_stats.py b/test/test_stats.py index 0337c3d8..8b534010 100644 --- a/test/test_stats.py +++ b/test/test_stats.py @@ -40,7 +40,7 @@ class TestStats(unittest.TestCase): test_all_roles = { '192.168.33.10': [ - 'load_balancer', 'shadow', 'login', 'appengine' + 'load_balancer', 'shadow', 'appengine' ], '192.168.33.11': [ 'taskqueue_master', 'zookeeper', 'db_master', 'taskqueue', 'memcache' @@ -247,7 +247,7 @@ class TestStats(unittest.TestCase): } def test_get_node_stats_headers_and_specified_roles(self): - specified_roles = ['login', 'shadow'] + specified_roles = ['shadow'] node_headers, node_stats = get_node_stats_rows( raw_node_stats=self.test_raw_node_stats, @@ -266,7 +266,7 @@ def test_get_node_stats_headers_and_specified_roles(self): u"\x1b[1m32% (1273 MB)\x1b[0m", u"\x1b[1m0.1 0.4 0.2\x1b[0m", u"\x1b[1m\x1b[31m\x1b[1m/test: 92\x1b[0m\x1b[1m, /main: 84, /: 44, ...\x1b[0m", - u"\x1b[1mload_balancer shadow login appengine\x1b[0m" + u"\x1b[1mload_balancer shadow appengine\x1b[0m" ] ] @@ -290,7 +290,7 @@ def test_get_node_stats_verbose(self): "\x1b[1m0.1 0.4 0.2\x1b[0m", ("\x1b[1m\x1b[31m\x1b[1m/test: 92\x1b[0m\x1b[1m, /main: 84, " "/: 44, /user: 25\x1b[0m"), - "\x1b[1mload_balancer shadow login appengine\x1b[0m" + "\x1b[1mload_balancer shadow appengine\x1b[0m" ], [ "192.168.33.11", @@ -417,11 +417,12 @@ def test_get_proxy_stats_order_and_verbose_and_apps_only(self): self.assertEqual(expected_proxy_stats, proxy_stats) - @patch("appscale.tools.appscale_stats.LocalState.get_login_host") + @patch("appscale.tools.appscale_stats.LocalState.get_host_with_role") @patch("appscale.tools.appscale_stats.LocalState.get_secret_key") @patch("requests.get") - def test_get_stats(self, mock_get, mock_get_secret_key, mock_get_login_host): - mock_get_login_host.return_value = "192.168.33.10" + def test_get_stats(self, mock_get, mock_get_secret_key, + mock_get_host_with_role): + mock_get_host_with_role.return_value = "192.168.33.10" mock_get_secret_key.return_value = "secret_key" attr = { From ea919f85495d61247c1d83f9f700436119c7cd01 Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Fri, 1 Mar 2019 16:26:18 -0800 Subject: [PATCH 02/63] Include subnet and vpc IDs when setting parameters --- appscale/tools/local_state.py | 2 ++ test/test_local_state.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 0fe51909..7b04159b 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -231,6 +231,8 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): iaas_creds['project'] = options.project iaas_creds['gce_user'] = getpass.getuser() elif options.infrastructure in ['euca', 'ec2']: + iaas_creds['aws_subnet_id'] = options.aws_subnet_id + iaas_creds['aws_vpc_id'] = options.aws_vpc_id iaas_creds['EC2_ACCESS_KEY'] = options.EC2_ACCESS_KEY iaas_creds['EC2_SECRET_KEY'] = options.EC2_SECRET_KEY iaas_creds['EC2_URL'] = options.EC2_URL diff --git a/test/test_local_state.py b/test/test_local_state.py index 9674e5c1..82d0921f 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -101,7 +101,8 @@ def test_generate_deployment_params(self): clear_datastore=False, disks={'node-1' : 'vol-ABCDEFG'}, zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, - EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='') + EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', + aws_subnet_id=None, aws_vpc_id=None) node_layout = NodeLayout({ 'table' : 'cassandra', 'infrastructure' : "ec2", @@ -136,7 +137,9 @@ def test_generate_deployment_params(self): 'default_max_appserver_memory' : str(ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY), 'EC2_ACCESS_KEY': 'baz', 'EC2_SECRET_KEY': 'baz', - 'EC2_URL': '' + 'EC2_URL': '', + 'aws_subnet_id': None, + 'aws_vpc_id': None } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price':'1.23'}) @@ -153,7 +156,8 @@ def test_generate_deployment_params_no_login(self): clear_datastore=False, disks={'node-1' : 'vol-ABCDEFG'}, zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, - EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='') + EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', + aws_subnet_id=None, aws_vpc_id=None) node_layout = NodeLayout({ 'table': 'cassandra', 'infrastructure': "ec2", @@ -188,7 +192,9 @@ def test_generate_deployment_params_no_login(self): 'default_max_appserver_memory': str(ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY), 'EC2_ACCESS_KEY': 'baz', 'EC2_SECRET_KEY': 'baz', - 'EC2_URL': '' + 'EC2_URL': '', + 'aws_subnet_id': None, + 'aws_vpc_id': None } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price': '1.23'}) From c5a99dba02da6ae32558579fcdb7a4bc6dd9c177 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Tue, 26 Mar 2019 17:54:02 -0700 Subject: [PATCH 03/63] Log warning on deploy for unsupported queue rate options --- appscale/tools/appscale_tools.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 3f62dd72..2d043f7e 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1213,6 +1213,13 @@ def update_queues(cls, source_location, keyname, project_id): queues = yaml.safe_load(queue_config) AppScaleLogger.log('Updating queues') + + for queue in queues.get('queue', []): + if 'bucket_size' in queue or 'max_concurrent_requests' in queue: + AppScaleLogger.warn('Queue configuration uses unsupported rate options' + ' (bucket size or max concurrent requests)') + break + login_host = LocalState.get_login_host(keyname) secret_key = LocalState.get_secret_key(keyname) admin_client = AdminClient(login_host, secret_key) From b408ed684f6ef87f734305c4927b05dca2f347ef Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Mon, 8 Apr 2019 14:04:09 -0700 Subject: [PATCH 04/63] Use login property if it's defined --- appscale/tools/local_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index d185ed4a..3ae0aa9c 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -203,7 +203,7 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): """ creds = { "table": options.table, - "login": node_layout.head_node().public_ip, + "login": options.login_host or node_layout.head_node().public_ip, "keyname": options.keyname, "replication": str(options.replication), "default_min_appservers": str(options.default_min_appservers), From 30ed84a02d3f1920a6e9bcfd4d7072f961050eee Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Mon, 8 Apr 2019 15:01:11 -0700 Subject: [PATCH 05/63] Fix unit tests --- test/test_local_state.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/test/test_local_state.py b/test/test_local_state.py index 5ef9e145..5dfbf74a 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -95,14 +95,16 @@ def test_ensure_appscale_isnt_running_and_it_isnt(self): def test_generate_deployment_params(self): # this method is fairly light, so just make sure that it constructs the dict # to send to the AppController correctly - options = flexmock(name='options', table='cassandra', keyname='boo', + options = flexmock( + name='options', table='cassandra', keyname='boo', default_min_appservers='1', autoscale=False, group='bazgroup', replication=None, infrastructure='ec2', machine='ami-ABCDEFG', instance_type='m1.large', use_spot_instances=True, max_spot_price=1.23, clear_datastore=False, disks={'node-1' : 'vol-ABCDEFG'}, zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, - EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='') + EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', + login_host='public1') node_layout = NodeLayout({ 'table' : 'cassandra', 'infrastructure' : "ec2", @@ -147,14 +149,16 @@ def test_generate_deployment_params(self): def test_generate_deployment_params_no_login(self): # this method is fairly light, so just make sure that it constructs the dict # to send to the AppController correctly - options = flexmock(name='options', table='cassandra', keyname='boo', + options = flexmock( + name='options', table='cassandra', keyname='boo', default_min_appservers='1', autoscale=False, group='bazgroup', replication=None, infrastructure='ec2', machine='ami-ABCDEFG', instance_type='m1.large', use_spot_instances=True, max_spot_price=1.23, clear_datastore=False, disks={'node-1' : 'vol-ABCDEFG'}, zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, - EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='') + EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', + login_host=None) node_layout = NodeLayout({ 'table': 'cassandra', 'infrastructure': "ec2", From 23e4a46d03b627edd30aacc54d2c656a7583eb09 Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Wed, 1 May 2019 15:01:01 -0700 Subject: [PATCH 06/63] Adds idle instances support --- appscale/tools/admin_api/version.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index 9b70dd9c..540f45dc 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -104,6 +104,17 @@ def from_yaml(app_yaml): except StandardError: raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') + # Adds optional elements for automatic scaling. + if automatic_scaling.get('min_idle_instances'): + version.automatic_scaling['minIdleInstances'] = + automatic_scaling.get('min_idle_instances') + if automatic_scaling.get('max_idle_instances'): + version.automatic_scaling['maxIdleInstances'] = + automatic_scaling.get('max_idle_instances') + if automatic_scaling.get('max_concurrent_requests'): + version.automatic_scaling['maxConcurrentRequests'] = + automatic_scaling.get('max_concurrent_requests') + if version.runtime in ('python27', 'java'): try: version.threadsafe = app_yaml['threadsafe'] From 6f6f69901a965d8c2c3be575eb63b27fcbc3095d Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Mon, 6 May 2019 11:59:32 -0700 Subject: [PATCH 07/63] Add check for int value, add web-xml support --- appscale/tools/admin_api/version.py | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index 540f45dc..a67794f8 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -105,15 +105,20 @@ def from_yaml(app_yaml): raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') # Adds optional elements for automatic scaling. - if automatic_scaling.get('min_idle_instances'): - version.automatic_scaling['minIdleInstances'] = - automatic_scaling.get('min_idle_instances') - if automatic_scaling.get('max_idle_instances'): - version.automatic_scaling['maxIdleInstances'] = - automatic_scaling.get('max_idle_instances') - if automatic_scaling.get('max_concurrent_requests'): - version.automatic_scaling['maxConcurrentRequests'] = - automatic_scaling.get('max_concurrent_requests') + try: + min_idle = automatic_scaling.get('min_idle_instances') + if min_idle is not None: + version.automatic_scaling['minIdleInstances'] = int(min_idle) + max_idle = automatic_scaling.get('max_idle_instances') + if max_idle is not None: + version.automatic_scaling['maxIdleInstances'] = int(max_idle) + max_concurrent = automatic_scaling.get('max_concurrent_requests') + if max_concurrent is not None: + version.automatic_scaling['maxConcurrentRequests'] = + int(max_concurrent) + except ValueError: + raise AppEngineConfigException('Invalid app.yaml: value for ' + 'automatic scaling option is not integer.') if version.runtime in ('python27', 'java'): try: @@ -195,6 +200,22 @@ def from_xml(root): except StandardError: raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') + # Adds optional elements for automatic scaling. + try: + min_idle = root.find(qname('min-idle-instances')) + if min_idle is not None: + version.automatic_scaling = {'minIdleInstances': int(min_idle} + max_idle = root.find(qname('max-idle-instances')) + if max_idle is not None: + version.automatic_scaling = {'maxIdleInstances': int(max_idle)} + max_concurrent = root.find(qname('max-concurrent-requests')) + if max_idle is not None: + version.automatic_scaling = {'maxConcurrentRequests': + int(max_concurrent)} + except ValueError: + raise AppEngineConfigException('Invalid appengine-web.xml: value for ' + 'automatic scaling option is not integer.') + threadsafe_element = root.find(qname('threadsafe')) if threadsafe_element is None: raise AppEngineConfigException( From 4bc9a6791361bdba4deaf59a4bc65177840d9e14 Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Tue, 7 May 2019 17:21:11 -0700 Subject: [PATCH 08/63] Fixed missing ) and ensure we don't overwrite previous valued for automaticScaling --- appscale/tools/admin_api/version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index a67794f8..ac46c860 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -204,14 +204,14 @@ def from_xml(root): try: min_idle = root.find(qname('min-idle-instances')) if min_idle is not None: - version.automatic_scaling = {'minIdleInstances': int(min_idle} + version.automatic_scaling['minIdleInstances'] = int(min_idle) max_idle = root.find(qname('max-idle-instances')) if max_idle is not None: - version.automatic_scaling = {'maxIdleInstances': int(max_idle)} + version.automatic_scaling['maxIdleInstances'] = int(max_idle) max_concurrent = root.find(qname('max-concurrent-requests')) if max_idle is not None: - version.automatic_scaling = {'maxConcurrentRequests': - int(max_concurrent)} + version.automatic_scaling['maxConcurrentRequests'] = + int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid appengine-web.xml: value for ' 'automatic scaling option is not integer.') From 0a9edcfcb06472bdc7897d49f46c32a580a37a13 Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Tue, 7 May 2019 17:23:31 -0700 Subject: [PATCH 09/63] Don't span multiple lines in assignments --- appscale/tools/admin_api/version.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index ac46c860..b2c6d2e1 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -114,8 +114,7 @@ def from_yaml(app_yaml): version.automatic_scaling['maxIdleInstances'] = int(max_idle) max_concurrent = automatic_scaling.get('max_concurrent_requests') if max_concurrent is not None: - version.automatic_scaling['maxConcurrentRequests'] = - int(max_concurrent) + version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid app.yaml: value for ' 'automatic scaling option is not integer.') @@ -210,8 +209,7 @@ def from_xml(root): version.automatic_scaling['maxIdleInstances'] = int(max_idle) max_concurrent = root.find(qname('max-concurrent-requests')) if max_idle is not None: - version.automatic_scaling['maxConcurrentRequests'] = - int(max_concurrent) + version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid appengine-web.xml: value for ' 'automatic scaling option is not integer.') From 439981d28cf63b875b251df39282262efb7fa9dd Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Tue, 7 May 2019 21:28:35 -0700 Subject: [PATCH 10/63] min|max-idle are under automatic-scaling --- appscale/tools/admin_api/version.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index b2c6d2e1..0965cfb5 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -199,20 +199,20 @@ def from_xml(root): except StandardError: raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') - # Adds optional elements for automatic scaling. - try: - min_idle = root.find(qname('min-idle-instances')) - if min_idle is not None: - version.automatic_scaling['minIdleInstances'] = int(min_idle) - max_idle = root.find(qname('max-idle-instances')) - if max_idle is not None: - version.automatic_scaling['maxIdleInstances'] = int(max_idle) - max_concurrent = root.find(qname('max-concurrent-requests')) - if max_idle is not None: - version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) - except ValueError: - raise AppEngineConfigException('Invalid appengine-web.xml: value for ' - 'automatic scaling option is not integer.') + # Adds optional elements for automatic scaling. + try: + min_idle = root.find(qname('min-idle-instances')) + if min_idle is not None: + version.automatic_scaling['minIdleInstances'] = int(min_idle) + max_idle = root.find(qname('max-idle-instances')) + if max_idle is not None: + version.automatic_scaling['maxIdleInstances'] = int(max_idle) + max_concurrent = root.find(qname('max-concurrent-requests')) + if max_idle is not None: + version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) + except ValueError: + raise AppEngineConfigException('Invalid appengine-web.xml: value for ' + 'automatic scaling option is not integer.') threadsafe_element = root.find(qname('threadsafe')) if threadsafe_element is None: From 71a1b456bbf5380bf22f3a7f9a5c84fad6c2a9be Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Wed, 8 May 2019 14:02:02 -0700 Subject: [PATCH 11/63] Follow up PR comments: thanks! --- appscale/tools/admin_api/version.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index 0965cfb5..47a6fc79 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -201,14 +201,14 @@ def from_xml(root): # Adds optional elements for automatic scaling. try: - min_idle = root.find(qname('min-idle-instances')) + min_idle = automatic_scaling.findtext(qname('min-idle-instances')) if min_idle is not None: version.automatic_scaling['minIdleInstances'] = int(min_idle) - max_idle = root.find(qname('max-idle-instances')) + max_idle = automatic_scaling.findtext(qname('max-idle-instances')) if max_idle is not None: version.automatic_scaling['maxIdleInstances'] = int(max_idle) - max_concurrent = root.find(qname('max-concurrent-requests')) - if max_idle is not None: + max_concurrent = automatic_scaling.findtext(qname('max-concurrent')) + if max_concurrent is not None: version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid appengine-web.xml: value for ' From 3f70ffe4864714292c540d2f0440575a4dea6ea5 Mon Sep 17 00:00:00 2001 From: Graziano Obertelli Date: Wed, 8 May 2019 15:39:37 -0700 Subject: [PATCH 12/63] GAE accepts any combination of scaling options This PR removes a limitation for which we had to define both min_instances and max_instances or none. In GAE any combination works. --- appscale/tools/admin_api/version.py | 42 +++++++++++++---------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/appscale/tools/admin_api/version.py b/appscale/tools/admin_api/version.py index 47a6fc79..70380651 100644 --- a/appscale/tools/admin_api/version.py +++ b/appscale/tools/admin_api/version.py @@ -4,6 +4,7 @@ import tarfile import zipfile +from collections import defaultdict from xml.etree import ElementTree import yaml @@ -96,25 +97,22 @@ def from_yaml(app_yaml): except StandardError: raise AppEngineConfigException('Invalid app.yaml: manual_scaling invalid.') elif automatic_scaling: + version.automatic_scaling = defaultdict(dict) try: - version.automatic_scaling = {'standardSchedulerSettings': { - 'minInstances': int(automatic_scaling['min_instances']), - 'maxInstances': int(automatic_scaling['max_instances']) - }} - except StandardError: - raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') - - # Adds optional elements for automatic scaling. - try: + min_instances = automatic_scaling.get('min_instances') + if min_instances is not None: + version.automatic_scaling['standardSchedulerSettings']['minInstances'] = ( + int(min_instances)) + max_instances = automatic_scaling.get('max_instances') + if max_instances is not None: + version.automatic_scaling['standardSchedulerSettings']['maxInstances'] = ( + int(max_instances)) min_idle = automatic_scaling.get('min_idle_instances') if min_idle is not None: version.automatic_scaling['minIdleInstances'] = int(min_idle) max_idle = automatic_scaling.get('max_idle_instances') if max_idle is not None: version.automatic_scaling['maxIdleInstances'] = int(max_idle) - max_concurrent = automatic_scaling.get('max_concurrent_requests') - if max_concurrent is not None: - version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid app.yaml: value for ' 'automatic scaling option is not integer.') @@ -192,24 +190,22 @@ def from_xml(root): except StandardError: raise AppEngineConfigException('Invalid app.yaml: manual_scaling invalid.') elif automatic_scaling is not None: + version.automatic_scaling = defaultdict(dict) try: - version.automatic_scaling = {'standardSchedulerSettings': { - 'minInstances': int(automatic_scaling.findtext(qname('min-instances'))), - 'maxInstances': int(automatic_scaling.findtext(qname('max-instances')))}} - except StandardError: - raise AppEngineConfigException('Invalid app.yaml: automatic_scaling invalid.') - - # Adds optional elements for automatic scaling. - try: + min_instances = automatic_scaling.findtext(qname('min-instances')) + if min_instances is not None: + version.automatic_scaling['standardSchedulerSettings']['minInstances'] = ( + int(min_instances)) + max_instances = automatic_scaling.findtext(qname('max-instances')) + if max_instances is not None: + version.automatic_scaling['standardSchedulerSettings']['maxInstances'] = ( + int(max_instances)) min_idle = automatic_scaling.findtext(qname('min-idle-instances')) if min_idle is not None: version.automatic_scaling['minIdleInstances'] = int(min_idle) max_idle = automatic_scaling.findtext(qname('max-idle-instances')) if max_idle is not None: version.automatic_scaling['maxIdleInstances'] = int(max_idle) - max_concurrent = automatic_scaling.findtext(qname('max-concurrent')) - if max_concurrent is not None: - version.automatic_scaling['maxConcurrentRequests'] = int(max_concurrent) except ValueError: raise AppEngineConfigException('Invalid appengine-web.xml: value for ' 'automatic scaling option is not integer.') From 0453008b1d9bf3dc5fe5773df9afc34539b9fd84 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Fri, 9 Nov 2018 16:51:14 -0600 Subject: [PATCH 13/63] move of appscale.tools.agents to appscale.agents --- Makefile | 21 +++++++++++++++++++-- appscale/tools/appscale_tools.py | 2 +- setup.py | 3 ++- test/test_appscale.py | 1 - test/test_appscale_logger.py | 1 - test/test_appscale_run_instances.py | 2 +- test/test_appscale_terminate_instances.py | 2 +- test/test_factory.py | 18 ------------------ test/test_node_layout.py | 2 +- test/test_parse_args.py | 6 +++--- test/test_remote_helper.py | 8 ++++---- 11 files changed, 32 insertions(+), 34 deletions(-) delete mode 100644 test/test_factory.py diff --git a/Makefile b/Makefile index b4e7ef43..68b53f30 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,27 @@ # # Makefile is mainly used to perform a version bump on the software. # -.PHONY: bump-major bump-minor bump-patch help all +.PHONY: bump-major bump-minor bump-patch help all test all: help help: - @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-10s\033[0m - %s\n", $$1, $$2}' $(MAKEFILE_LIST) + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf "\033[36m%-10s\033[0m - %s\n", $$1, $$2} /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST) +##@ Testing +test: checkenv-VIRTUAL_ENV ## Run Python unit tests + @python -m unittest discover -b -v -s test + +##@ Build +build: checkenv-VIRTUAL_ENV ## Build distro for pypi upload + @python setup.py sdist bdist_wheel + +clean: ## Clean build artifiacts + rm -rf build + rm -rf dist + rm -rf appscale_tools.egg-info + +##@ Utilities bump-major: ## Bump major version number for appscale util/bump_version.sh major @@ -16,3 +30,6 @@ bump-minor: ## Bump minor version number for appscale bump-patch: ## Bump patch version number for appscale util/bump_version.sh patch + +checkenv-%: + $(if $($*), ,$(error virtualenv was not detected, exiting)) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 547dc949..d24a0c1b 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -27,7 +27,7 @@ from appscale.tools.admin_api.client import (AdminClient, DEFAULT_SERVICE, DEFAULT_VERSION) from appscale.tools.admin_api.version import Version -from appscale.tools.agents.factory import InfrastructureAgentFactory +from appscale.agents.factory import InfrastructureAgentFactory from appscale.tools.appcontroller_client import AppControllerClient from appscale.tools.appengine_helper import AppEngineHelper from appscale.tools.appscale_logger import AppScaleLogger diff --git a/setup.py b/setup.py index 45dbc1f0..b0ad635a 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,7 @@ platforms='Posix; MacOS X', install_requires=[ 'adal>=0.4.7', + 'appscale.agents', 'azure==2.0.0', 'azure-mgmt-marketplaceordering', 'cryptography>=2.3.0', @@ -76,7 +77,7 @@ ], namespace_packages=['appscale'], packages=['appscale', 'appscale.tools', 'appscale.tools.admin_api', - 'appscale.tools.agents', 'appscale.tools.scripts'], + 'appscale.tools.scripts'], entry_points={ 'console_scripts': [ 'appscale=appscale.tools.scripts.appscale:main', diff --git a/test/test_appscale.py b/test/test_appscale.py index c0e647ea..e45b4e13 100644 --- a/test/test_appscale.py +++ b/test/test_appscale.py @@ -18,7 +18,6 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.ec2_agent import EC2Agent from appscale.tools.appscale import AppScale from appscale.tools.appscale_tools import AppScaleTools from appscale.tools.custom_exceptions import AppScaleException diff --git a/test/test_appscale_logger.py b/test/test_appscale_logger.py index 9ee49a3a..980084ca 100644 --- a/test/test_appscale_logger.py +++ b/test/test_appscale_logger.py @@ -14,7 +14,6 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.ec2_agent import EC2Agent from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.parse_args import ParseArgs diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index 64af4761..6bca07a4 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -23,7 +23,7 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.ec2_agent import EC2Agent +from appscale.agents.ec2_agent import EC2Agent from appscale.tools.appcontroller_client import AppControllerClient from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.appscale_tools import AppScaleTools diff --git a/test/test_appscale_terminate_instances.py b/test/test_appscale_terminate_instances.py index f256f03e..5042ab84 100644 --- a/test/test_appscale_terminate_instances.py +++ b/test/test_appscale_terminate_instances.py @@ -10,7 +10,7 @@ from flexmock import flexmock # AppScale import, the library that we're testing here -from appscale.tools.agents.ec2_agent import EC2Agent +from appscale.agents.ec2_agent import EC2Agent from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.appscale_tools import AppScaleTools from appscale.tools.custom_exceptions import AppScaleException diff --git a/test/test_factory.py b/test/test_factory.py deleted file mode 100644 index 60ec106c..00000000 --- a/test/test_factory.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -# General-purpose Python library imports -import unittest - - -# AppScale import, the library that we're testing here -from appscale.tools.agents.factory import InfrastructureAgentFactory -from appscale.tools.custom_exceptions import UnknownInfrastructureException - - -class TestFactory(unittest.TestCase): - - - def test_bad_agent_name(self): - # Passing in an invalid agent name should raise an exception. - self.assertRaises(UnknownInfrastructureException, - InfrastructureAgentFactory.create_agent, 'bad agent name') diff --git a/test/test_node_layout.py b/test/test_node_layout.py index 971e8ce0..3836d9f5 100644 --- a/test/test_node_layout.py +++ b/test/test_node_layout.py @@ -12,7 +12,7 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.ec2_agent import EC2Agent +from appscale.agents.ec2_agent import EC2Agent from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.custom_exceptions import BadConfigurationException from appscale.tools.node_layout import NodeLayout diff --git a/test/test_parse_args.py b/test/test_parse_args.py index 824d4971..ab750bbc 100644 --- a/test/test_parse_args.py +++ b/test/test_parse_args.py @@ -14,9 +14,9 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.base_agent import AgentConfigurationException -from appscale.tools.agents.ec2_agent import EC2Agent -from appscale.tools.agents.euca_agent import EucalyptusAgent +from appscale.agents.base_agent import AgentConfigurationException +from appscale.agents.ec2_agent import EC2Agent +from appscale.agents.euca_agent import EucalyptusAgent from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.custom_exceptions import BadConfigurationException from appscale.tools.parse_args import ParseArgs diff --git a/test/test_remote_helper.py b/test/test_remote_helper.py index 9e1cef5d..1cbfdfed 100644 --- a/test/test_remote_helper.py +++ b/test/test_remote_helper.py @@ -19,10 +19,10 @@ # AppScale import, the library that we're testing here -from appscale.tools.agents.euca_agent import EucalyptusAgent -from appscale.tools.agents import factory -from appscale.tools.agents.gce_agent import CredentialTypes -from appscale.tools.agents.gce_agent import GCEAgent +from appscale.agents.euca_agent import EucalyptusAgent +from appscale.agents import factory +from appscale.agents.gce_agent import CredentialTypes +from appscale.agents.gce_agent import GCEAgent from appscale.tools.appcontroller_client import AppControllerClient from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.custom_exceptions import BadConfigurationException From ee49d48dfc3f268d8ecadac65e856bc68fa61b47 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Tue, 13 Nov 2018 17:25:47 -0600 Subject: [PATCH 14/63] remove agents from appscale.tools, minor Makefile changes --- Makefile | 23 + appscale/tools/agents/__init__.py | 2 - appscale/tools/agents/azure_agent.py | 1905 ---------------------- appscale/tools/agents/base_agent.py | 275 ---- appscale/tools/agents/ec2_agent.py | 1034 ------------ appscale/tools/agents/euca_agent.py | 86 - appscale/tools/agents/factory.py | 58 - appscale/tools/agents/gce_agent.py | 1251 -------------- appscale/tools/agents/openstack_agent.py | 133 -- appscale/tools/appscale_tools.py | 3 +- appscale/tools/node_layout.py | 2 +- appscale/tools/parse_args.py | 10 +- appscale/tools/remote_helper.py | 8 +- setup.py | 2 +- 14 files changed, 36 insertions(+), 4756 deletions(-) delete mode 100644 appscale/tools/agents/__init__.py delete mode 100644 appscale/tools/agents/azure_agent.py delete mode 100644 appscale/tools/agents/base_agent.py delete mode 100644 appscale/tools/agents/ec2_agent.py delete mode 100644 appscale/tools/agents/euca_agent.py delete mode 100644 appscale/tools/agents/factory.py delete mode 100644 appscale/tools/agents/gce_agent.py delete mode 100644 appscale/tools/agents/openstack_agent.py diff --git a/Makefile b/Makefile index 68b53f30..f56fc6d7 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ # .PHONY: bump-major bump-minor bump-patch help all test +AGENTS_REPO?=https://github.com/scragraham/appscale-agents +AGENTS_BRANCH?=topic-agent-init + all: help help: @@ -10,16 +13,36 @@ help: ##@ Testing test: checkenv-VIRTUAL_ENV ## Run Python unit tests + @pip install flexmock @python -m unittest discover -b -v -s test +##@ Installation +venv: ## Create virtualenv environment + virtualenv venv + +install: checkenv-VIRTUAL_ENV ## Install via pip, ensuring a virtualenv + pip install . + +install-no-venv: ## Install via pip without ensuring a virtualenv + pip install . + +install-deps: checkenv-VIRTUAL_ENV ## Install appscale specific dependencies (appscale.agents) + pip install -e git+$(AGENTS_REPO)@$(AGENTS_BRANCH)#egg=appscale.agents + + ##@ Build build: checkenv-VIRTUAL_ENV ## Build distro for pypi upload @python setup.py sdist bdist_wheel clean: ## Clean build artifiacts + $(info 'Removing pyc files') + @find . -name "*.pyc" -delete rm -rf build rm -rf dist rm -rf appscale_tools.egg-info + +venv-clean: ## Remove Virtualenv + rm -rf venv ##@ Utilities bump-major: ## Bump major version number for appscale diff --git a/appscale/tools/agents/__init__.py b/appscale/tools/agents/__init__.py deleted file mode 100644 index 1cb28406..00000000 --- a/appscale/tools/agents/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -__author__ = 'hiranya' -__email__ = 'hiranya@appscale.com' \ No newline at end of file diff --git a/appscale/tools/agents/azure_agent.py b/appscale/tools/agents/azure_agent.py deleted file mode 100644 index 26e94fe0..00000000 --- a/appscale/tools/agents/azure_agent.py +++ /dev/null @@ -1,1905 +0,0 @@ -#!/usr/bin/env python -""" -This file provides a single class, AzureAgent, that the AppScale Tools can use to -interact with Microsoft Azure. -""" - -# General-purpose Python library imports -import adal -import concurrent.futures -import logging -import math -import os.path -import re -import time -from itertools import count, ifilter - -# Azure specific imports -from azure.mgmt.compute import ComputeManagementClient -from azure.mgmt.compute.models import ApiEntityReference -from azure.mgmt.compute.models import AvailabilitySet -from azure.mgmt.compute.models import CachingTypes -from azure.mgmt.compute.models import DataDisk -from azure.mgmt.compute.models import DiskCreateOptionTypes -from azure.mgmt.compute.models import HardwareProfile -from azure.mgmt.compute.models import ImageReference -from azure.mgmt.compute.models import LinuxConfiguration -from azure.mgmt.compute.models import ManagedDiskParameters -from azure.mgmt.compute.models import NetworkProfile -from azure.mgmt.compute.models import NetworkInterfaceReference -from azure.mgmt.compute.models import OperatingSystemTypes -from azure.mgmt.compute.models import OSDisk -from azure.mgmt.compute.models import OSProfile -from azure.mgmt.compute.models import Sku as ComputeSku -from azure.mgmt.compute.models import SshConfiguration -from azure.mgmt.compute.models import SshPublicKey -from azure.mgmt.compute.models import StorageProfile -from azure.mgmt.compute.models import SubResource -from azure.mgmt.compute.models import UpgradePolicy -from azure.mgmt.compute.models import UpgradeMode -from azure.mgmt.compute.models import VirtualHardDisk -from azure.mgmt.compute.models import VirtualMachine -from azure.mgmt.compute.models import VirtualMachineScaleSet -from azure.mgmt.compute.models import VirtualMachineScaleSetIPConfiguration -from azure.mgmt.compute.models import VirtualMachineScaleSetNetworkConfiguration -from azure.mgmt.compute.models import VirtualMachineScaleSetNetworkProfile -from azure.mgmt.compute.models import VirtualMachineScaleSetOSDisk -from azure.mgmt.compute.models import VirtualMachineScaleSetOSProfile -from azure.mgmt.compute.models import VirtualMachineScaleSetStorageProfile -from azure.mgmt.compute.models import VirtualMachineScaleSetVMProfile - -from azure.mgmt.marketplaceordering.marketplace_ordering_agreements import MarketplaceOrderingAgreements - -from azure.mgmt.network import NetworkManagementClient -from azure.mgmt.network.models import AddressSpace -from azure.mgmt.network.models import IPAllocationMethod -from azure.mgmt.network.models import NetworkInterfaceIPConfiguration -from azure.mgmt.network.models import NetworkInterface -from azure.mgmt.network.models import PublicIPAddress -from azure.mgmt.network.models import Subnet -from azure.mgmt.network.models import VirtualNetwork - -from azure.mgmt.resource.resources import ResourceManagementClient -from azure.mgmt.resource.resources.models import ResourceGroup - -from azure.mgmt.storage import StorageManagementClient -from azure.mgmt.storage.models import StorageAccountCreateParameters, SkuName, Kind -from azure.mgmt.storage.models import Sku as StorageSku - -from msrestazure.azure_active_directory import ServicePrincipalCredentials -from msrest.exceptions import ClientException -from msrestazure.azure_exceptions import CloudError -from haikunator import Haikunator - -# AppScale-specific imports -from appscale.tools.appscale_logger import AppScaleLogger -from appscale.tools.local_state import LocalState -from base_agent import AgentConfigurationException -from base_agent import AgentRuntimeException -from base_agent import BaseAgent - -class AzureAgent(BaseAgent): - """ AzureAgent defines a specialized BaseAgent that allows for interaction - with Microsoft Azure. It authenticates using the ADAL (Active Directory - Authentication Library). - """ - # The Azure URL endpoint that receives all the authentication requests. - AZURE_AUTH_ENDPOINT = 'https://login.microsoftonline.com/' - - # The Azure Resource URL to get the authentication token using client credentials. - AZURE_RESOURCE_URL = 'https://management.core.windows.net/' - - # Default Storage Account name to use for Azure. - DEFAULT_STORAGE_ACCT = 'appscalestorage' - - # Default resource group name to use for Azure. - DEFAULT_RESOURCE_GROUP = 'appscalegroup' - - # Default resource tag name to use for Azure. - DEFAULT_TAG = 'default-tag' - - # A list of Azure instance types that have less than 4 GB of RAM, the amount - # recommended by Cassandra. AppScale will still run on these instance types, - # but is likely to crash after a day or two of use (as Cassandra will attempt - # to malloc ~800MB of memory, which will fail on these instance types). - DISALLOWED_INSTANCE_TYPES = ["Basic_A0", "Basic_A1", "Basic_A2", "Basic_A3", - "Basic_A4", "Standard_A0", "Standard_A1", - "Standard_A2", "Standard_D1", "Standard_D1_v2", - "Standard_DS1", "Standard_DS1_v2"] - - # The following constants are string literals that can be used by callers to - # index into the parameters that the user passes in, as opposed to having to - # type out the strings each time we need them. - PARAM_APP_ID = 'azure_app_id' - PARAM_APP_SECRET = 'azure_app_secret_key' - PARAM_DISKS = 'disks' - PARAM_EXISTING_RG = 'does_exist' - PARAM_RESOURCE_GROUP = 'azure_resource_group' - PARAM_STORAGE_ACCOUNT = 'azure_storage_account' - PARAM_SUBSCRIBER_ID = 'azure_subscription_id' - PARAM_TENANT_ID = 'azure_tenant_id' - PARAM_TAG = 'azure_group_tag' - - # A set that contains all of the items necessary to run AppScale in Azure. - REQUIRED_CREDENTIALS = ( - PARAM_APP_SECRET, - PARAM_APP_ID, - BaseAgent.PARAM_IMAGE_ID, - BaseAgent.PARAM_INSTANCE_TYPE, - BaseAgent.PARAM_KEYNAME, - PARAM_SUBSCRIBER_ID, - PARAM_TENANT_ID, - BaseAgent.PARAM_ZONE - ) - - # The regex for Azure Marketplace images. - MARKETPLACE_IMAGE = re.compile(".*:.*:.*:.*") - - # The admin username needed to create an Azure VM instance. - ADMIN_USERNAME = 'azureuser' - - # The number of seconds to sleep while polling for - # Azure resources to get created/updated. - SLEEP_TIME = 10 - - # The maximum number of seconds to wait for Azure resources - # to get created/updated. - MAX_SLEEP_TIME = 60 - - # The maximum number of seconds to wait for an Azure VM to be created. - # (Takes longer than the creation time for other resources.) - MAX_VM_UPDATE_TIME = 240 - - # The maximum number of seconds to wait for an Azure scale set to be created. - MAX_VMSS_WAIT_TIME = 300 - - # The maximum limit of allowable VMs within a scale set. - MAX_VMSS_CAPACITY = 20 - - # The Virtual Network and Subnet name to use while creating an Azure - # Virtual machine. - VIRTUAL_NETWORK = 'appscaleazure' - - # The Compute Azure Resource provider namespace. - MICROSOFT_COMPUTE_RESOURCE = 'Microsoft.Compute' - - # The Network Azure Resource provider namespace. - MICROSOFT_NETWORK_RESOURCE = 'Microsoft.Network' - - # The Storage Azure Resource provider namespace. - MICROSOFT_STORAGE_RESOURCE = 'Microsoft.Storage' - - # The compatible Network Management API version to use with scale sets. - NETWORK_MGMT_API_VERSION = '2016-09-01' - - def assert_credentials_are_valid(self, parameters): - """ Contacts Azure with the given credentials to ensure that they are - valid. Gets an access token and a Credentials instance in order to be - able to access any resources. - Args: - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Raises: - AgentConfigurationException: if we are unable to authenticate with Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - try: - resource_client = ResourceManagementClient(credentials, subscription_id) - # Try listing resource groups to make sure we can, a CloudError will be - # raised if the credentials are invalid. - resource_client.resource_groups.list() - except ClientException as error: - logging.exception("Error authenticating with provided credentials.") - raise AgentConfigurationException("Unable to authenticate using the " - "credentials provided. Reason: {}".format(error.message)) - - def configure_instance_security(self, parameters): - """ Configure the resource group and storage account needed to create the - network interface for the VMs to be spawned. This method is called before - starting virtual machines. - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - Returns: - True, if the group and account were created successfully. - False, otherwise. - Raises: - AgentRuntimeException: If security features could not be successfully - configured in the underlying cloud. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - is_autoscale = parameters[self.PARAM_AUTOSCALE_AGENT] - - # While creating instances during autoscaling, we do not need to create a - # new keypair or a resource group. We just make use of the existing one. - if is_autoscale in ['True', True]: - return - - credentials = self.open_connection(parameters) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - storage_account = parameters.get(self.PARAM_STORAGE_ACCOUNT) - zone = parameters[self.PARAM_ZONE] - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - - AppScaleLogger.log("Verifying that SSH key exists locally.") - keyname = parameters[self.PARAM_KEYNAME] - private_key = LocalState.LOCAL_APPSCALE_PATH + keyname - public_key = private_key + ".pub" - - if os.path.exists(private_key) or os.path.exists(public_key): - raise AgentRuntimeException("SSH key already found locally - please " - "use a different keyname.") - - LocalState.generate_rsa_key(keyname, parameters[self.PARAM_VERBOSE]) - - AppScaleLogger.log("Configuring network for machine/s under " - "resource group '{0}' with storage account '{1}' " - "in zone '{2}'".format(resource_group, storage_account, zone)) - # Create a resource group and an associated storage account to access resources. - self.create_resource_group(parameters, credentials) - - resource_client = ResourceManagementClient(credentials, subscription_id) - try: - resource_client.providers.register(self.MICROSOFT_COMPUTE_RESOURCE) - resource_client.providers.register(self.MICROSOFT_NETWORK_RESOURCE) - except CloudError as error: - logging.exception("Encountered an error while registering provider.") - raise AgentRuntimeException("Unable to register provider. Reason: {}" - .format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to register provider. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def attach_disk(self, parameters, disk_name, instance_id): - """ Attaches the persistent disk specified in 'disk_name' to this virtual - machine. - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - disk_name: A str naming the managed disk to attach to this machine. - instance_id: A str naming the id of the instance that the disk should be - attached to. - Returns: - A str indicating a symlink to where the persistent disk has been - attached to. - Raises: - AgentRuntimeException if the disk cannot be attached due to a - CloudError or does not have a successful provisioning state. - """ - credentials = self.open_connection(parameters) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - compute_client = ComputeManagementClient(credentials, subscription_id) - virtual_machine = compute_client.virtual_machines.get(resource_group, - instance_id) - storage_profile = virtual_machine.storage_profile - - # pick the LUN (this must be unique) - existing_luns = set() - for disk in storage_profile.data_disks: - # While we're looping check if disk is already attached and return the - # symlink if it is. - if disk.name == disk_name: - return os.path.realpath('/dev/disk/azure/scsi1/lun{}'.format(disk.lun)) - existing_luns.add(disk.lun) - - # Get the first number not being used as a LUN. - the_chosen_lun = next(ifilter(lambda lun: lun not in existing_luns, count(1))) - - disk = compute_client.disks.get(resource_group, disk_name) - managed_disk_params = ManagedDiskParameters(id=disk.id) - data_disk = DataDisk(lun=the_chosen_lun, name=disk.name, - create_option=DiskCreateOptionTypes.attach, - managed_disk=managed_disk_params) - storage_profile.data_disks.append(data_disk) - disk_response = compute_client.virtual_machines.create_or_update( - resource_group, instance_id, virtual_machine) - try: - disk_response.wait(timeout=self.MAX_VM_UPDATE_TIME) - result = disk_response.result() - if result.provisioning_state == 'Succeeded': - return os.path.realpath('/dev/disk/azure/scsi1/lun{}'.format( - the_chosen_lun)) - else: - raise AgentRuntimeException("Unable to attach disk {0} to " - "VM {1}".format(disk_name, instance_id)) - except CloudError as error: - raise AgentRuntimeException("Unable to attach disk {0} to " - "VM {1}: {2}".format(disk_name, instance_id, error.message)) - - def detach_disk(self, parameters, disk_name, instance_id): - """ Detaches the persistent disk specified in 'disk_name' to this virtual - machine. - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - disk_name: A str naming the managed disk to detach from this machine. - instance_id: A str naming the id of the instance that the disk should be - detached from. - Returns: - True if the disk was detached, and False otherwise. - """ - credentials = self.open_connection(parameters) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - compute_client = ComputeManagementClient(credentials, subscription_id) - virtual_machine = compute_client.virtual_machines.get(resource_group, - instance_id) - storage_profile = virtual_machine.storage_profile - disks_to_keep = [] - - for disk in storage_profile.data_disks: - if disk.name != disk_name: - disks_to_keep.append(disk) - else: - return True - - storage_profile.data_disks = disks_to_keep - disk_response = compute_client.virtual_machines.create_or_update( - resource_group, instance_id, virtual_machine) - try: - disk_response.wait(timeout=self.MAX_VM_UPDATE_TIME) - result = disk_response.result() - if result.provisioning_state == 'Succeeded': - return True - else: - AppScaleLogger.log("Unable to detach Disk {} from VM {}".format( - disk_name, instance_id)) - return False - except CloudError as error: - AppScaleLogger.log("Unable to detach Disk {} from VM {}: {}".format( - disk_name, instance_id, error.message)) - return False - - def describe_instances(self, parameters, pending=False): - """ Queries Microsoft Azure to see which instances are currently - running, and retrieves information about their public and private IPs. - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - pending: If we should show pending instances. - Returns: - public_ips: A list of public IP addresses. - private_ips: A list of private IP addresses. - instance_ids: A list of unique Azure VM names. - Raises: - AgentRuntimeException: If we are unable to list instances in the - cloud. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - - network_client = NetworkManagementClient(credentials, subscription_id, - api_version=self.NETWORK_MGMT_API_VERSION) - compute_client = ComputeManagementClient(credentials, subscription_id) - - try: - # Static IPs are not listed because AppScale does not create them. - # They are used for creating Azure load balancers from the Portal, - # which we would retain even on terminating the deployment and reuse - # across multiple deployments. - public_ips = [public_ip.ip_address for public_ip in - network_client.public_ip_addresses.list(resource_group) - if not public_ip.public_ip_allocation_method == 'Static'] - - private_ips = [ip_config.private_ip_address - for network_interface in - network_client.network_interfaces.list(resource_group) - for ip_config in network_interface.ip_configurations] - - instance_ids = [vm.name for vm in - compute_client.virtual_machines.list(resource_group)] - vm_vmss_list = [(vm, vmss.name) - for vmss in compute_client.virtual_machine_scale_sets.list( - resource_group) - for vm in compute_client.virtual_machine_scale_set_vms.list( - resource_group, vmss.name)] - - for vm, vmss_name in vm_vmss_list: - network_interface_list = network_client.network_interfaces. \ - list_virtual_machine_scale_set_vm_network_interfaces( - resource_group, vmss_name, vm.instance_id) - private_ip = next(ip_config.private_ip_address - for network_interface in network_interface_list - for ip_config in network_interface.ip_configurations - if ip_config.private_ip_address) - public_ips.append(private_ip) - private_ips.append(private_ip) - instance_ids.append(vm.name) - except CloudError as e: - logging.exception("CloudError received while trying to describe " - "instances.") - raise AgentRuntimeException(e.message) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to describe instances. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - return public_ips, private_ips, instance_ids - - def run_instances(self, count, parameters, security_configured, public_ip_needed): - """ Starts 'count' instances in Microsoft Azure, and returns once they - have been started. Callers should create a network and attach a firewall - to it before using this method, or the newly created instances will not - have a network and firewall to attach to (and thus this method will fail). - Args: - count: An int, that specifies how many virtual machines should be started. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - security_configured: Unused, as we assume that the network and firewall - has already been set up. - Returns: - instance_ids: A list of unique Azure VM names. - public_ips: A list of public IP addresses. - private_ips: A list of private IP addresses. - Raises: - AgentRuntimeException: If an error has occurred talking to Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - virtual_network = parameters[self.PARAM_GROUP] - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - - network_client = NetworkManagementClient(credentials, subscription_id) - compute_client = ComputeManagementClient(credentials, subscription_id) - - subnet = self.create_virtual_network(network_client, parameters, - virtual_network, virtual_network) - - active_public_ips, active_private_ips, active_instances = \ - self.describe_instances(parameters) - - # Create an Availability Set for Load Balancer VMs - lb_avail_set_name = "lb-availability-set" - availability_set_names = [availability_set.name for availability_set in - compute_client.availability_sets.list(resource_group)] - azure_image_id = parameters[self.PARAM_IMAGE_ID] - avail_set_sku = None - if self.MARKETPLACE_IMAGE.match(azure_image_id): - avail_set_sku = ComputeSku(name='Aligned') - - if lb_avail_set_name not in availability_set_names: - lb_avail_set = self.create_lb_availability_set( - compute_client, lb_avail_set_name, parameters, avail_set_sku) - else: - lb_avail_set = compute_client.availability_sets.get(resource_group, lb_avail_set_name) - - availability_set = SubResource(lb_avail_set.id) - using_disks = parameters.get(self.PARAM_DISKS, False) - - if using_disks and not self.MARKETPLACE_IMAGE.match(azure_image_id): - raise AgentConfigurationException("Managed Disks require use of a " - "publisher image.") - if public_ip_needed or using_disks: - lb_vms_exceptions = [] - # Only load balancer VMs with public IPs are added to the availability set and - # all other nodes with disks created as regular VMs outside of scaleset - # should not be added. - if using_disks and not public_ip_needed: - availability_set = None - # We can use a with statement to ensure threads are cleaned up promptly - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - lb_vms_futures = [executor.submit(self.setup_virtual_machine_creation, - credentials, network_client, parameters, - subnet, availability_set) - for _ in range(count)] - for future in concurrent.futures.as_completed(lb_vms_futures): - exception = future.exception() - if exception: - lb_vms_exceptions.append(exception) - - for exception in lb_vms_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if lb_vms_exceptions: - raise AgentRuntimeException(str(lb_vms_exceptions)) - else: - self.create_or_update_vm_scale_sets(count, parameters, subnet) - - public_ips, private_ips, instance_ids = self.describe_instances(parameters) - public_ips = self.diff(public_ips, active_public_ips) - private_ips = self.diff(private_ips, active_private_ips) - instance_ids = self.diff(instance_ids, active_instances) - - return instance_ids, public_ips, private_ips - - def create_lb_availability_set(self, compute_client, lb_avail_set_name, parameters, avail_set_sku=None): - """ Creates an Availability Set for the load balancer VMs. - Args: - compute_client: A ComputeManagementClient instance - lb_avail_set_name: The name to use for the availability set. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Raises: - AgentConfigurationException: If the operation to create an availability - set did not succeed. - """ - try: - # The max number of available platform update and fault domains is 3. - return compute_client.availability_sets.create_or_update( - parameters[self.PARAM_RESOURCE_GROUP], lb_avail_set_name, - AvailabilitySet(location=parameters[self.PARAM_ZONE], - sku=avail_set_sku, platform_update_domain_count=3, - platform_fault_domain_count=3)) - - except CloudError as error: - logging.exception("Azure agent received a CloudError while creating an " - "availability set.") - raise AgentRuntimeException("Unable to create an Availability Set " - "{0}: {1}".format(lb_avail_set_name, - error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create an availability set. " - "Please check your cloud configuration. " - "Reason: {}".format(e.message)) - - def setup_virtual_machine_creation(self, credentials, network_client, - parameters, subnet, availability_set): - """ Sets up the network interface and creates the virtual machines needed - with the load balancer roles. - Args: - credentials: A ServicePrincipalCredentials instance, that can be used to - access or create any resources. - network_client: A NetworkManagementClient instance. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - subnet: A Subnet instance from the Virtual Network created. - availability_set: An Availability Set instance for the load balancer VMs. - """ - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - vm_network_name = Haikunator().haikunate() - self.create_network_interface(network_client, vm_network_name, - vm_network_name, subnet, parameters) - network_interface = network_client.network_interfaces.get( - resource_group, vm_network_name) - self.create_virtual_machine(credentials, network_client, - network_interface.id, - parameters, vm_network_name, - availability_set) - - def create_virtual_machine(self, credentials, network_client, network_id, - parameters, vm_network_name, availability_set): - """ Creates an Azure virtual machine using the network interface created. - Args: - credentials: A ServicePrincipalCredentials instance, that can be used to - access or create any resources. - network_client: A NetworkManagementClient instance. - network_id: The network id of the network interface created. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - vm_network_name: The name of the virtual machine to use. - availability_set: An Availability Set instance for the load balancer VMs. - Raises: - AgentRuntimeException: If a virtual machine could not be successfully - created. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - zone = parameters[self.PARAM_ZONE] - verbose = parameters[self.PARAM_VERBOSE] - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - azure_instance_type = parameters[self.PARAM_INSTANCE_TYPE] - AppScaleLogger.verbose("Creating a Virtual Machine '{}'". - format(vm_network_name), verbose) - - compute_client = ComputeManagementClient(credentials, subscription_id) - linux_config = self.create_linux_configuration(parameters) - - os_profile = OSProfile(admin_username=self.ADMIN_USERNAME, - computer_name=vm_network_name, - linux_configuration=linux_config) - - hardware_profile = HardwareProfile(vm_size=azure_instance_type) - - network_profile = NetworkProfile( - network_interfaces=[NetworkInterfaceReference(id=network_id)]) - - os_type = OperatingSystemTypes.linux - azure_image_id = parameters[self.PARAM_IMAGE_ID] - - image_ref = None - image_hd = None - plan = None - virtual_hd = None - - # Publisher images are formatted Publisher:Offer:Sku:Tag - if self.MARKETPLACE_IMAGE.match(azure_image_id): - publisher, offer, sku, version = azure_image_id.split(":") - compatible_zone = zone.lower().replace(" ", "") - - image = compute_client.virtual_machine_images.get( - compatible_zone, publisher, offer, sku, version) - image_ref = ImageReference(publisher=publisher, - offer=offer, - sku=sku, - version=version) - plan = image.plan - else: - storage_account = parameters[self.PARAM_STORAGE_ACCOUNT] - virtual_hd = VirtualHardDisk( - uri='https://{0}.blob.core.windows.net/vhds/{1}.vhd'. - format(storage_account, vm_network_name)) - image_hd = VirtualHardDisk(uri=parameters[self.PARAM_IMAGE_ID]) - - os_disk = OSDisk(os_type=os_type, caching=CachingTypes.read_write, - create_option=DiskCreateOptionTypes.from_image, - name=vm_network_name, vhd=virtual_hd, image=image_hd) - storage_profile = StorageProfile(image_reference=image_ref, - os_disk=os_disk) - try: - compute_client.virtual_machines.create_or_update( - resource_group, vm_network_name, VirtualMachine(location=zone, - plan=plan, os_profile=os_profile, hardware_profile=hardware_profile, - network_profile=network_profile, storage_profile=storage_profile, - availability_set=availability_set)) - # Sleep until an IP address gets associated with the VM. - while True: - public_ip_address = network_client.public_ip_addresses.get( - resource_group, vm_network_name) - if public_ip_address.ip_address: - AppScaleLogger.log('Azure load balancer VM is available at {}'. - format(public_ip_address.ip_address)) - break - AppScaleLogger.verbose("Waiting {} second(s) for IP address to be " - "available".format(self.SLEEP_TIME), verbose) - time.sleep(self.SLEEP_TIME) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create virtual machine. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create virtual machine. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def create_linux_configuration(self, parameters): - """ Creates a Linux Configuration to pass in to the virtual machine - instance to be created. - Args: - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Returns: - An instance of LinuxConfiguration - """ - is_autoscale = parameters[self.PARAM_AUTOSCALE_AGENT] - keyname = parameters[self.PARAM_KEYNAME] - private_key_path = LocalState.LOCAL_APPSCALE_PATH + keyname - public_key_path = private_key_path + ".pub" - auth_keys_path = "/home/{}/.ssh/authorized_keys".format(self.ADMIN_USERNAME) - - if is_autoscale in ['True', True]: - public_key_path = auth_keys_path - - with open(public_key_path, 'r') as pub_ssh_key_fd: - pub_ssh_key = pub_ssh_key_fd.read() - - public_keys = [SshPublicKey(path=auth_keys_path, key_data=pub_ssh_key)] - ssh_config = SshConfiguration(public_keys=public_keys) - linux_config = LinuxConfiguration(disable_password_authentication=True, - ssh=ssh_config) - return linux_config - - def add_instances_to_existing_ss(self, count, parameters): - """ Looks through existing scale sets in a particular resource group and - adds instances (created as a part of autoscaling) to the ones which have - additional capacity. - Args: - count: The number of instances to be created for autoscaling. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Returns: - The number of instances created and added to the existing scale sets. - Raises: - Raises: - AgentRuntimeException: If instances could not successfully be added to - a Scale Set. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - instance_type = parameters[self.PARAM_INSTANCE_TYPE] - compute_client = ComputeManagementClient(credentials, subscription_id) - - num_instances_added = 0 - try: - scalesets_and_counts = [] - for vmss in compute_client.virtual_machine_scale_sets.list( - resource_group): - ss_instance_count = len(list( - compute_client.virtual_machine_scale_set_vms.list(resource_group, vmss.name))) - - if ss_instance_count >= self.MAX_VMSS_CAPACITY: - continue - - if not vmss.sku.name == instance_type: - continue - vmss = compute_client.virtual_machine_scale_sets.get(resource_group, - vmss.name) - scalesets_and_counts.append((vmss, ss_instance_count)) - except CloudError as error: - logging.exception("Azure agent received a CloudError trying to add " - "to Scale Sets.") - raise AgentRuntimeException("Unable to add to Scale Sets due to: {}" - .format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to add to Scale Sets. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - for vmss, ss_instance_count in scalesets_and_counts: - ss_upgrade_policy = vmss.upgrade_policy - ss_location = vmss.location - ss_profile = vmss.virtual_machine_profile - ss_overprovision = vmss.overprovision - - new_capacity = min(ss_instance_count + count, self.MAX_VMSS_CAPACITY) - sku = ComputeSku(name=parameters[self.PARAM_INSTANCE_TYPE], - capacity=new_capacity) - scaleset = VirtualMachineScaleSet(sku=sku, - upgrade_policy=ss_upgrade_policy, - location=ss_location, - virtual_machine_profile=ss_profile, - overprovision=ss_overprovision) - try: - create_update_response = compute_client.virtual_machine_scale_sets. \ - create_or_update(resource_group, vmss.name, scaleset) - - self.wait_for_ss_update(new_capacity, create_update_response, vmss.name) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create/update ScaleSet. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact" - " Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create/update ScaleSet. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - newly_added = new_capacity - ss_instance_count - num_instances_added += newly_added - count -= newly_added - - # If all the additional instances to be created fit within the - # capacity of existing scale sets. - if count == 0: - break - - return num_instances_added - - def create_or_update_vm_scale_sets(self, count, parameters, subnet): - """ Creates/Updates a virtual machine scale set containing the given number - of virtual machines with the virtual network provided. - Args: - count: The number of virtual machines to be created in the scale set. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - subnet:A reference to the subnet ID of the virtual network created. - Raises: - AgentConfigurationException: If the operation to create a virtual - machine scale set did not succeed. - """ - verbose = parameters[self.PARAM_VERBOSE] - random_resource_name = Haikunator().haikunate() - - num_instances_to_add = count - - # While autoscaling, look through existing scale sets to check if they have - # capacity to hold more vms. If they do, update the scale sets with additional - # vms. If not, then create a new scale set for them. - is_autoscale = parameters[self.PARAM_AUTOSCALE_AGENT] - if is_autoscale in ['True', True]: - instances_added = self.add_instances_to_existing_ss( - num_instances_to_add, parameters) - # Exceeded capacity of existing scale sets, so create a new scale set. - if num_instances_to_add > instances_added: - num_instances_to_add = num_instances_to_add - instances_added - else: - # The required number of instances fit within existing scale sets. - return - - # Create multiple scale sets with the allowable maximum capacity of VMs. - if num_instances_to_add > self.MAX_VMSS_CAPACITY: - # Count of the number of scale sets needed depending on the max capacity. - scale_set_count = int(math.ceil(num_instances_to_add / float( - self.MAX_VMSS_CAPACITY))) - remaining_vms_count = num_instances_to_add - - scalesets_futures = [] - scalesets_exceptions = [] - # We can use a with statement to ensure threads are cleaned up promptly - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for ss_count in range(scale_set_count): - resource_name = random_resource_name + "-resource-{}".format(ss_count) - scale_set_name = random_resource_name + "-scaleset-{}".format( - ss_count) - capacity = self.MAX_VMSS_CAPACITY - if remaining_vms_count < self.MAX_VMSS_CAPACITY: - capacity = remaining_vms_count - AppScaleLogger.verbose('Creating a Scale Set {0} with {1} VM(s)'. - format(scale_set_name, capacity), verbose) - - # Start creating scalesets. - scalesets_futures.append(executor.submit(self.create_scale_set, - capacity, parameters, resource_name, scale_set_name, subnet)) - remaining_vms_count = remaining_vms_count - self.MAX_VMSS_CAPACITY - for future in concurrent.futures.as_completed(scalesets_futures): - exception = future.exception() - if exception: - scalesets_exceptions.append(exception) - - for exception in scalesets_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if scalesets_exceptions: - raise AgentRuntimeException(str(scalesets_exceptions)) - - # Create a scale set using the count of VMs provided. - else: - scale_set_name = random_resource_name + "-scaleset-{}vms".format(num_instances_to_add) - AppScaleLogger.verbose('Creating a Scale Set {0} with {1} VM(s)'. - format(scale_set_name, num_instances_to_add), verbose) - self.create_scale_set(num_instances_to_add, parameters, random_resource_name, - scale_set_name, subnet) - - def create_scale_set(self, count, parameters, resource_name, - scale_set_name, subnet): - """ Creates a scale set of 'count' number of virtual machines in the given - subnet and virtual Network. - Args: - count: The VM capacity of the scale set to be created. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - resource_name: The names of the sub resources needed to create - a virtual machine in a scale set. - scale_set_name: The name of the scale set to be created. - subnet: A reference to the subnet ID of the virtual network created. - - Raises: - AgentRuntimeException: If a scale set could not be successfully created. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - zone = parameters[self.PARAM_ZONE] - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - compute_client = ComputeManagementClient(credentials, subscription_id) - - linux_configuration = self.create_linux_configuration(parameters) - - os_profile = VirtualMachineScaleSetOSProfile( - computer_name_prefix=resource_name, admin_username=self.ADMIN_USERNAME, - linux_configuration=linux_configuration) - - - subnet_reference = ApiEntityReference(id=subnet.id) - ip_config = VirtualMachineScaleSetIPConfiguration(name=resource_name, - subnet=subnet_reference) - - network_interface_config = VirtualMachineScaleSetNetworkConfiguration( - name=resource_name, primary=True, ip_configurations=[ip_config]) - - network_profile = VirtualMachineScaleSetNetworkProfile( - network_interface_configurations=[network_interface_config]) - - os_disk = None - plan = None - image_ref = None - azure_image_id = parameters[self.PARAM_IMAGE_ID] - # Publisher images are formatted Publisher:Offer:Sku:Tag - if self.MARKETPLACE_IMAGE.match(azure_image_id): - publisher, offer, sku, version = azure_image_id.split(":") - compatible_zone = zone.lower().replace(" ", "") - - image = compute_client.virtual_machine_images.get( - compatible_zone, publisher, offer, sku, version) - image_ref = ImageReference(publisher=publisher, - offer=offer, - sku=sku, - version=version) - plan = image.plan - else: - image_hd = VirtualHardDisk(uri=azure_image_id) - - os_disk = VirtualMachineScaleSetOSDisk( - name=resource_name, caching=CachingTypes.read_write, - create_option=DiskCreateOptionTypes.from_image, - os_type=OperatingSystemTypes.linux, image=image_hd) - - storage_profile = VirtualMachineScaleSetStorageProfile( - os_disk=os_disk, image_reference=image_ref) - virtual_machine_profile = VirtualMachineScaleSetVMProfile( - os_profile=os_profile, storage_profile=storage_profile, - network_profile=network_profile) - - sku = ComputeSku(name=parameters[self.PARAM_INSTANCE_TYPE], capacity=long(count)) - upgrade_policy = UpgradePolicy(mode=UpgradeMode.manual) - vm_scale_set = VirtualMachineScaleSet( - sku=sku, upgrade_policy=upgrade_policy, location=zone, plan=plan, - virtual_machine_profile=virtual_machine_profile, overprovision=False) - - try: - create_update_response = \ - compute_client.virtual_machine_scale_sets.create_or_update( - resource_group, scale_set_name, vm_scale_set) - - self.wait_for_ss_update(count, create_update_response, scale_set_name) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create or update Scale Set. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create a scale set. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def wait_for_ss_update(self, count, create_update_response, scale_set_name): - """ Waits until the scale set has been successfully updated and all the - instances have been created and are running. - - Args: - count: The VM capacity of the scale set to be created. - create_update_response: An instance, of the AzureOperationPoller to - poll for the status of the operation being performed. - scale_set_name: The name of the scale set being updated. - - Raises: - AgentRuntimeException: If there is a problem updating the virtual - machine scale set. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - try: - create_update_response.wait(timeout=self.MAX_VMSS_WAIT_TIME) - result = create_update_response.result() - if result.provisioning_state == 'Succeeded': - AppScaleLogger.log("Scale Set '{0}' with {1} VM(s) has been successfully " - "configured!".format(scale_set_name, count)) - else: - AppScaleLogger.log("Unable to create a Scale Set of {0} " - "VM(s).Provisioning Status: {1}" - .format(count, result.provisioning_state)) - - except CloudError as error: - logging.exception("CloudError during creation of Scale Set.") - raise AgentRuntimeException("Unable to create a Scale Set of {0} " - "VM(s): {1}".format(count, error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to " - "contact Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create Scale Set. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def associate_static_ip(self, instance_id, static_ip): - """ Associates the given static IP address with the given instance ID. - - Args: - instance_id: A str that names the instance that the static IP should be - bound to. - static_ip: A str naming the static IP to bind to the given instance. - """ - - def terminate_instances(self, parameters): - """ Deletes the instances specified in 'parameters' running in Azure. - Args: - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Raises: - AgentRuntimeException: If instances could not successfully be terminated. - AgentConfigurationException: If we are unable to authenticate with Azure. - """ - credentials = self.open_connection(parameters) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - verbose = parameters[self.PARAM_VERBOSE] - instances_to_delete = parameters[self.PARAM_INSTANCE_IDS] - AppScaleLogger.verbose("Terminating the vm instance(s) '{}'". - format(instances_to_delete), verbose) - - compute_client = ComputeManagementClient(credentials, subscription_id) - try: - vmss_list = list(compute_client.virtual_machine_scale_sets.list( - resource_group)) - except CloudError as e: - logging.exception("CloudError received trying to list Scale Sets.") - raise AgentRuntimeException(e.message) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to list Scale Sets. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - downscale = parameters[self.PARAM_AUTOSCALE_AGENT] - - # On downscaling of instances, we need to delete the specific instance - # from the Scale Set. - if downscale in ['True', True]: - # Delete the scale set virtual machines matching the given instance ids. - vmss_vm_delete_futures = [] - vmss_vm_delete_exceptions = [] - - try: - # Get the instance_ids for the Scale Set VMs. - vmss_vms_to_delete = [(vm.instance_id, vmss.name) for vmss in vmss_list - for vm in compute_client.virtual_machine_scale_set_vms.list( - resource_group, vmss.name) - if vm.name in instances_to_delete] - except CloudError as e: - logging.exception("CloudError received while trying to terminate " - "instances.") - raise AgentRuntimeException(e.message) - except ClientException as e: - logging.exception("ClientException received while attempting to " - "contact Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to terminate instances. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for vm_instance_id, vmss_name in vmss_vms_to_delete: - vmss_vm_delete_futures.append(executor.submit( - self.delete_vmss_instance, compute_client, parameters, - vmss_name, vm_instance_id)) - - for future in concurrent.futures.as_completed(vmss_vm_delete_futures): - exception = future.exception() - if exception: - vmss_vm_delete_exceptions.append(exception) - - for exception in vmss_vm_delete_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if vmss_vm_delete_exceptions: - raise AgentRuntimeException(str(vmss_vm_delete_exceptions)) - - AppScaleLogger.log("Virtual machine(s) have been successfully downscaled.") - AppScaleLogger.log("Cleaning up any Scale Sets, if needed ...") - vmss_delete_futures = [] - vmss_delete_exceptions = [] - - try: - ss_to_delete = [vmss.name for vmss in vmss_list if not - any(compute_client.virtual_machine_scale_set_vms.list(resource_group, vmss.name))] - except CloudError as e: - logging.exception("CloudError received while trying to terminate " - "instances.") - raise AgentRuntimeException(e.message) - except ClientException as e: - logging.exception("ClientException received while attempting to " - "contact Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to terminate instances. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for vmss_name in ss_to_delete: - vmss_delete_futures.append(executor.submit( - self.delete_virtual_machine_scale_set, compute_client, - parameters, vmss_name)) - - for future in concurrent.futures.as_completed(vmss_delete_futures): - exception = future.exception() - if exception: - vmss_delete_exceptions.append(exception) - - for exception in vmss_delete_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if vmss_delete_exceptions: - raise AgentRuntimeException(str(vmss_delete_exceptions)) - return - - # On appscale down --terminate, we delete all the Scale Sets within the - # resource group specified, as it is faster than deleting the individual - # instances within each Scale Set. - - # Get the list of all ScaleSet Instance "Names" before deleting - # ScaleSets. We refer to the Scale Set VM instances by name rather than - # instance id. - try: - delete_ss_instances = [vm.name for vmss in vmss_list - for vm in compute_client.virtual_machine_scale_set_vms.list( - resource_group, vmss.name)] - except CloudError as e: - logging.exception("CloudError received while trying to terminate " - "instances.") - raise AgentRuntimeException(e.message) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to terminate instances. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - # Delete ScaleSets. - vmss_delete_futures = [] - vmss_delete_exceptions = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for vmss in vmss_list: - vmss_delete_futures.append(executor.submit( - self.delete_virtual_machine_scale_set, compute_client, - parameters, vmss.name)) - for future in concurrent.futures.as_completed(vmss_delete_futures): - exception = future.exception() - if exception: - vmss_delete_exceptions.append(exception) - - for exception in vmss_delete_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if vmss_delete_exceptions: - raise AgentRuntimeException(str(vmss_delete_exceptions)) - - AppScaleLogger.log("Virtual machine scale set(s) have been successfully " - "deleted.") - # Delete the load balancer virtual machines matching the given instance ids. - delete_lb_instances = self.diff(instances_to_delete, delete_ss_instances) - lb_delete_futures = [] - lb_delete_exceptions = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: - for vm_name in delete_lb_instances: - lb_delete_futures.append(executor.submit(self.delete_virtual_machine, - compute_client, parameters, vm_name)) - - for future in concurrent.futures.as_completed(lb_delete_futures): - exception = future.exception() - if exception: - lb_delete_exceptions.append(exception) - - for exception in lb_delete_exceptions: - if not isinstance(exception, (CloudError, AgentRuntimeException)): - logging.exception(exception) - if lb_delete_exceptions: - raise AgentRuntimeException(str(lb_delete_exceptions)) - - AppScaleLogger.log("Load balancer virtual machine(s) have been " - "successfully deleted") - - def delete_virtual_machine_scale_set(self, compute_client, parameters, vmss_name): - """ Deletes the virtual machine scale set created from the specified - resource group. - Args: - compute_client: An instance of the Compute Management client. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - vmss_name: The name of the virtual machine scale set to be deleted. - Raises: - AgentConfigurationException: If we are unable to authenticate with Azure. - AgentRuntimeException: If a scale set could not be successfully deleted. - """ - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - verbose = parameters[self.PARAM_VERBOSE] - AppScaleLogger.verbose("Deleting Scale Set {} ...".format(vmss_name), verbose) - try: - delete_response = compute_client.virtual_machine_scale_sets.delete( - resource_group, vmss_name) - except CloudError as error: - logging.exception("CloudError received trying to clean up Scale Set.") - raise AgentRuntimeException("Unable to clean up Scale Set {}. " - "Reason: {}".format(vmss_name, error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up Scale Set {}. Please check your " - "cloud configuration. Reason: {}".format(vmss_name, e.message)) - resource_name = 'Virtual Machine Scale Set' + ":" + vmss_name - self.sleep_until_delete_operation_done(delete_response, resource_name, - self.MAX_VM_UPDATE_TIME, verbose) - AppScaleLogger.verbose("Virtual Machine Scale Set {} has been successfully " - "deleted.".format(vmss_name), verbose) - - def delete_vmss_instance(self, compute_client, parameters, vmss_name, instance_id): - """ Deletes the specified virtual machine instance from the given Scale Set. - Args: - compute_client: An instance of the Compute Management client. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - vmss_name: The Scale Set from which the instance needs to be deleted. - instance_id: The ID of the instance in the Scale Set to be deleted. - """ - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - verbose = parameters[self.PARAM_VERBOSE] - - vm_info = "Virtual Machine {0} from Scale Set {1}".format(instance_id, - vmss_name) - - AppScaleLogger.verbose("Deleting {0} ...".format(vm_info), verbose) - try: - result = compute_client.virtual_machine_scale_set_vms.delete( - resource_group, vmss_name, instance_id) - except CloudError as error: - logging.exception("CloudError received trying to clean up scale set.") - raise AgentRuntimeException("Unable to clean up VM {} from Scale Set {}. " - "Reason: {}".format(instance_id, vmss_name, - error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up VM {} from Scale Set {}. Please check your " - "cloud configuration. Reason: {}".format(instance_id, vmss_name, - e.message)) - resource_name = 'Virtual Machine Instance ' + instance_id - self.sleep_until_delete_operation_done(result, resource_name, - self.MAX_VM_UPDATE_TIME, verbose) - - AppScaleLogger.verbose("{0} from scaleset {1} has been successfully " - "deleted".format(instance_id, vmss_name), verbose) - - def delete_virtual_machine(self, compute_client, parameters, vm_name): - """ Deletes the virtual machine from the resource_group specified. - Args: - compute_client: An instance of the Compute Management client. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - vm_name: The name of the virtual machine to be deleted. - """ - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - verbose = parameters[self.PARAM_VERBOSE] - AppScaleLogger.verbose("Deleting Virtual Machine {} ...".format(vm_name), verbose) - try: - result = compute_client.virtual_machines.delete(resource_group, vm_name) - except CloudError as error: - logging.exception("CloudError received trying to clean up scale set.") - raise AgentRuntimeException("Unable to clean up Virtual Machine {}. " - "Reason: {}".format(vm_name, error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up Virtual Machine {}. Please check your " - "cloud configuration. Reason: {}".format(vm_name, e.message)) - resource_name = 'Virtual Machine' + ':' + vm_name - self.sleep_until_delete_operation_done(result, resource_name, - self.MAX_VM_UPDATE_TIME, verbose) - AppScaleLogger.verbose("Virtual Machine {} has been successfully deleted.". - format(vm_name), verbose) - - def sleep_until_delete_operation_done(self, result, resource_name, - max_sleep, verbose): - """ Sleeps until the delete operation for the resource is completed - successfully. - Args: - result: An instance, of the AzureOperationPoller to poll for the status - of the operation being performed. - resource_name: The name of the resource being deleted. - max_sleep: The maximum number of seconds to sleep for the resources to - be deleted. - verbose: A boolean indicating whether or not in verbose mode. - Raises: - AgentRuntimeException if we time out waiting for the operation to finish. - """ - time_start = time.time() - while not result.done(): - AppScaleLogger.verbose("Waiting {0} second(s) for {1} to be deleted.". - format(self.SLEEP_TIME, resource_name), verbose) - time.sleep(self.SLEEP_TIME) - total_sleep_time = time.time() - time_start - if total_sleep_time > max_sleep: - err_msg = "Waited {0} second(s) for {1} to be deleted. Operation has " \ - "timed out.".format(total_sleep_time, resource_name) - AppScaleLogger.log(err_msg) - raise AgentRuntimeException(err_msg) - - def does_address_exist(self, parameters): - """ Verifies that the specified static IP address has been allocated, and - belongs to the user with the given credentials. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud, as well as a key indicating which static IP address - should be validated. - Returns: - A bool that indicates if the given static IP address exists, and belongs - to this user. - """ - - def does_image_exist(self, parameters): - """ Verifies that the specified machine image exists in this cloud. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud, as well as a key indicating which machine image should - be checked for existence. - Returns: - A bool that indicates if the machine image exists in this cloud. - """ - return True - - def does_disk_exist(self, parameters, disk): - """ Verifies that the specified persistent disk exists in this cloud. - - Args: - parameters: A dict that includes the parameters needed to authenticate - with this cloud. - disk: A str containing the name of the disk that we should check for - existence. - Returns: - True if the named persistent disk exists, and False otherwise, - """ - - def does_zone_exist(self, parameters): - """ Verifies that the specified zone exists in this cloud. - - Args: - parameters: A dict that includes a key indicating the zone to check for - existence. - Returns: - True if the zone exists, and False otherwise. - Raises: - AgentConfigurationException: If an error is encountered during - checking for the zone due to configuration errors (zone does not - exist or authentication issues). - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - zone = parameters[self.PARAM_ZONE] - resource_client = ResourceManagementClient(credentials, subscription_id) - try: - resource_providers = resource_client.providers.list() - for provider in resource_providers: - for resource_type in provider.resource_types: - if zone in resource_type.locations: - return True - except ClientException as error: - logging.exception("Unable to check if zone exists.") - raise AgentConfigurationException("Unable to check if zone exists. " - "Please check your cloud " - "configuration. Reason: {}".format( - error.message)) - return False - - def cleanup_state(self, parameters): - """ Removes any remote state that was created to run AppScale instances - during this deployment. - Args: - parameters: A dict that includes keys indicating the remote state - that should be deleted. - Raises: - AgentConfigurationException: If we are unable to authenticate with Azure. - AgentRuntimeException: If an error is encountered trying to clean up - the state in Azure. - """ - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - resource_group = parameters[self.PARAM_RESOURCE_GROUP] - credentials = self.open_connection(parameters) - network_client = NetworkManagementClient(credentials, subscription_id) - verbose = parameters[self.PARAM_VERBOSE] - - AppScaleLogger.log("Cleaning up the network configuration created for this " - "deployment ...") - try: - network_interfaces = network_client.network_interfaces.list(resource_group) - for interface in network_interfaces: - result = network_client.network_interfaces.delete(resource_group, interface.name) - resource_name = 'Network Interface' + ':' + interface.name - self.sleep_until_delete_operation_done(result, resource_name, - self.MAX_SLEEP_TIME, verbose) - AppScaleLogger.verbose("Network Interface {} has been successfully deleted.". - format(interface.name), verbose) - except CloudError as error: - logging.exception("CloudError received trying to clean up network interfaces.") - raise AgentRuntimeException("Unable to clean up network interfaces. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up network interfaces. Please check your " - "cloud configuration. Reason: {}".format(e.message)) - - AppScaleLogger.log("Network Interface(s) have been successfully deleted.") - - try: - public_ip_addresses = network_client.public_ip_addresses.list(resource_group) - for public_ip in public_ip_addresses: - result = network_client.public_ip_addresses.delete(resource_group, public_ip.name) - resource_name = 'Public IP Address' + ':' + public_ip.name - self.sleep_until_delete_operation_done(result, resource_name, - self.MAX_SLEEP_TIME, verbose) - AppScaleLogger.verbose("Public IP Address {} has been successfully deleted.". - format(public_ip.name), verbose) - except CloudError as error: - logging.exception("Unable to clean up public ips.") - raise AgentRuntimeException("Unable to clean up public ips. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up public ips. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - AppScaleLogger.log("Public IP Address(s) have been successfully deleted.") - - try: - virtual_networks = network_client.virtual_networks.list(resource_group) - for network in virtual_networks: - result = network_client.virtual_networks.delete(resource_group, network.name) - resource_name = 'Virtual Network' + ':' + network.name - self.sleep_until_delete_operation_done(result, resource_name, - self.MAX_SLEEP_TIME, verbose) - AppScaleLogger.verbose("Virtual Network {} has been successfully deleted.". - format(network.name), verbose) - except CloudError as error: - logging.exception("Unable to clean up virtual networks.") - raise AgentRuntimeException("Unable to clean up virtual networks. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to " - "contact Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to clean up virtual networks. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - AppScaleLogger.log("Virtual Network(s) have been successfully deleted.") - - def get_params_from_args(self, args): - """ Constructs a dict with only the parameters necessary to interact with - Microsoft Azure. - Args: - args: A Namespace or dict, that maps all of the arguments the user has - invoked an AppScale command with their associated value. - Returns: - A dict, that maps each argument given to the value that was associated with - it. - Raises: - AgentConfigurationException: If unable to authenticate using the credentials - provided in the AppScalefile. - """ - if not isinstance(args, dict): - args = vars(args) - - params = { - self.PARAM_APP_ID: args[self.PARAM_APP_ID], - self.PARAM_APP_SECRET: args[self.PARAM_APP_SECRET], - self.PARAM_IMAGE_ID: args['machine'], - self.PARAM_INSTANCE_TYPE: args[self.PARAM_INSTANCE_TYPE], - self.PARAM_GROUP: args[self.PARAM_GROUP], - self.PARAM_KEYNAME: args[self.PARAM_KEYNAME], - self.PARAM_RESOURCE_GROUP: args.get(self.PARAM_RESOURCE_GROUP, - self.DEFAULT_RESOURCE_GROUP), - self.PARAM_STORAGE_ACCOUNT: args.get(self.PARAM_STORAGE_ACCOUNT), - self.PARAM_SUBSCRIBER_ID: args[self.PARAM_SUBSCRIBER_ID], - self.PARAM_TAG: args.get(self.PARAM_TAG, self.DEFAULT_TAG), - self.PARAM_TENANT_ID: args[self.PARAM_TENANT_ID], - self.PARAM_VERBOSE : args.get('verbose', False), - self.PARAM_ZONE : args[self.PARAM_ZONE], - self.PARAM_AUTOSCALE_AGENT: False - } - self.assert_credentials_are_valid(params) - - # Perform Marketplace Image checks. - if self.MARKETPLACE_IMAGE.match(params[self.PARAM_IMAGE_ID]): - params[self.PARAM_IMAGE_ID] = self.get_marketplace_image_version(params) - self.check_marketplace_image(params) - # Only assign default storage account if we are not using a Marketplace - # Image. - elif not params[self.PARAM_STORAGE_ACCOUNT]: - params[self.PARAM_STORAGE_ACCOUNT] = self.DEFAULT_STORAGE_ACCT - - return params - - def get_marketplace_image_version(self, parameters): - """ Check if the marketplace image specified exists and it's correct - version if 'latest' was specified. - - Args: - parameters: A dict containing values necessary to authenticate with the - Azure. - Returns: - A string indicating the given or corrected marketplace image formatted - "publisher:offer:sku:version". For example if - "appscale-marketplace:appscale:3:latest" was received at this time would - return "appscale-marketplace:appscale:3:3.5.2" - Raises: - AgentConfigurationException: If the image is invalid or there was an - authentication issue trying to contact Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - azure_image_id = parameters[self.PARAM_IMAGE_ID] - zone = parameters[self.PARAM_ZONE] - - AppScaleLogger.log("Checking publisher image version for {}".format( - azure_image_id)) - publisher, offer, sku, version = azure_image_id.split(":") - compute_client = ComputeManagementClient(credentials, subscription_id) - compatible_zone = zone.lower().replace(" ", "") - - if version.lower() != 'latest': - try: - compute_client.virtual_machine_images.get(compatible_zone, publisher, - offer, sku, version) - return azure_image_id - except ClientException as e: - raise AgentConfigurationException("Received CloudError trying to " - "request specified image. Please " - "ensure your image is valid in " - "the AppScalefile and that your " - "cloud configuration is correct. " - "Reason: {}".format(e.message)) - - try: - top_one = compute_client.virtual_machine_images.list( - compatible_zone, publisher, offer, sku, top=1, orderby='name desc') - except ClientException as e: - raise AgentConfigurationException("Received CloudError trying to " - "request specified image. Please " - "ensure your image is valid in " - "the AppScalefile and that your cloud " - "configuration is correct. " - "Reason: {}".format(e.message)) - if len(top_one) == 0: - raise AgentConfigurationException("Can't resolve the vesion of '{}'" - .format(azure_image_id)) - - version = top_one[0].name - - return ":".join([publisher, offer, sku, version]) - - def check_marketplace_image(self, parameters): - """ Check if the marketplace image specified has its terms accepted. - Otherwise we will not be able to use this image. - - Args: - parameters: A dict containing values necessary to authenticate with the - Azure. - Raises: - AgentRuntimeException: If we cannot complete the calls to Azure. - """ - credentials = self.open_connection(parameters) - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - azure_image_id = parameters[self.PARAM_IMAGE_ID] - zone = parameters[self.PARAM_ZONE] - - publisher, offer, sku, version = azure_image_id.split(":") - compute_client = ComputeManagementClient(credentials, subscription_id) - compatible_zone = zone.lower().replace(" ", "") - - AppScaleLogger.log("Using publisher image {}".format(azure_image_id)) - - try: - image = compute_client.virtual_machine_images.get( - compatible_zone, publisher, offer, sku, version) - if not image.plan: - AppScaleLogger.warn("It is not recommended to use a non-AppScale image") - return - market_place_client = MarketplaceOrderingAgreements(credentials, - subscription_id) - term = market_place_client.marketplace_agreements.get( - image.plan.publisher, image.plan.product, image.plan.name) - if not term.accepted: - AppScaleLogger.log("Marketplace image {}'s license agreement was not " - "accepted, accepting it now.".format(azure_image_id)) - term.accepted = True - market_place_client.marketplace_agreements.create( - image.plan.publisher, image.plan.product, image.plan.name, term) - except CloudError as e: - raise AgentRuntimeException("Received CloudError trying to check and " - "accept terms for specified image. Reason: {}".format(e)) - - def get_cloud_params(self, keyname): - """ Searches through the locations.json file with key - 'infrastructure_info' to build a dict containing the - parameters necessary to interact with Microsoft Azure. - Args: - keyname: A str that uniquely identifies this AppScale deployment. - Returns: - A dict containing all of the credentials necessary to interact with - Microsoft Azure. - """ - params = { - self.PARAM_GROUP: LocalState.get_group(keyname), - self.PARAM_KEYNAME: keyname, - self.PARAM_VERBOSE: True, - self.PARAM_ZONE: LocalState.get_zone(keyname), - self.PARAM_SUBSCRIBER_ID: LocalState.get_subscription_id(keyname), - self.PARAM_APP_ID: LocalState.get_app_id(keyname), - self.PARAM_APP_SECRET: LocalState.get_app_secret_key(keyname), - self.PARAM_TENANT_ID: LocalState.get_tenant_id(keyname), - self.PARAM_RESOURCE_GROUP: LocalState.get_resource_group(keyname), - self.PARAM_STORAGE_ACCOUNT: LocalState.get_storage_account(keyname), - } - return params - - def assert_required_parameters(self, parameters, operation): - """ Check whether all the parameters required to interact with Azure are - present in the provided dict. - Args: - parameters: A dict containing values necessary to authenticate with the - Azure. - operation: A str representing the operation for which the parameters - should be checked. - Raises: - AgentConfigurationException: If a required parameter is absent. - """ - # Make sure that the user has set each parameter. - for param in self.REQUIRED_CREDENTIALS: - if not self.has_parameter(param, parameters): - raise AgentConfigurationException('The required parameter, {0}, was not' - ' specified.'.format(param)) - - def open_connection(self, parameters): - """ Connects to Microsoft Azure with the given credentials, creates an - authentication token and uses that to get the ServicePrincipalCredentials - which is needed to access any resources. - Args: - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. We assume that the user has - already authorized this account by creating a Service Principal - with the appropriate (Contributor) role. - Returns: - A ServicePrincipalCredentials instance, that can be used to access or - create any resources. - """ - app_id = parameters[self.PARAM_APP_ID] - app_secret_key = parameters[self.PARAM_APP_SECRET] - tenant_id = parameters[self.PARAM_TENANT_ID] - - # Get an Authentication token using ADAL. - context = adal.AuthenticationContext(self.AZURE_AUTH_ENDPOINT + tenant_id) - try: - token_response = context.acquire_token_with_client_credentials( - self.AZURE_RESOURCE_URL, app_id, app_secret_key) - except adal.adal_error.AdalError as e: - raise AgentConfigurationException( - "Unable to communicate with Azure! Please check your cloud " - "configuration. Reason: {}".format(e.message)) - token_response.get('accessToken') - - # To access Azure resources for an application, we need a Service Principal - # with the accurate role assignment. It can be created using the Azure CLI. - credentials = ServicePrincipalCredentials(client_id=app_id, - secret=app_secret_key, - tenant=tenant_id) - return credentials - - - def create_virtual_network(self, network_client, parameters, network_name, - subnet_name): - """ Creates the network resources, such as Virtual network and Subnet. - Args: - network_client: A NetworkManagementClient instance. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - network_name: The name to use for the Virtual Network resource. - subnet_name: The name to use for the Subnet resource. - Returns: - A Subnet instance from the Virtual Network created. - Raises: - AgentConfigurationException: If we are unable to authenticate with Azure. - AgentRuntimeException: If an error is encountered trying to create a - virtual network in Azure. - """ - group_name = parameters[self.PARAM_RESOURCE_GROUP] - region = parameters[self.PARAM_ZONE] - verbose = parameters[self.PARAM_VERBOSE] - AppScaleLogger.verbose("Creating/Updating the Virtual Network '{}'". - format(network_name), verbose) - address_space = AddressSpace(address_prefixes=['10.1.0.0/16']) - subnet1 = Subnet(name=subnet_name, address_prefix='10.1.0.0/24') - try: - result = network_client.virtual_networks.create_or_update( - group_name, network_name, - VirtualNetwork(location=region, address_space=address_space, subnets=[subnet1])) - self.sleep_until_update_operation_done(result, network_name, verbose) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create virtual network. Reason: " - "{}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create virtual network. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - subnet = network_client.subnets.get(group_name, network_name, subnet_name) - return subnet - - def create_network_interface(self, network_client, interface_name, ip_name, - subnet, parameters): - """ Creates the Public IP Address resource and uses that to create the - Network Interface. - Args: - network_client: A NetworkManagementClient instance. - interface_name: The name to use for the Network Interface. - ip_name: The name to use for the Public IP Address. - subnet: The Subnet resource from the Virtual Network created. - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - Raises: - AgentConfigurationException: If we are unable to authenticate with Azure. - AgentRuntimeException: If an error is encountered trying to create a - network interface in Azure. - """ - group_name = parameters[self.PARAM_RESOURCE_GROUP] - region = parameters[self.PARAM_ZONE] - verbose = parameters[self.PARAM_VERBOSE] - AppScaleLogger.verbose("Creating/Updating the Public IP Address '{}'". - format(ip_name), verbose) - ip_address = PublicIPAddress( - location=region, public_ip_allocation_method=IPAllocationMethod.dynamic, - idle_timeout_in_minutes=4) - try: - result = network_client.public_ip_addresses.create_or_update( - group_name, ip_name, ip_address) - self.sleep_until_update_operation_done(result, ip_name, verbose) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create public ip address. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create a public ip address. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - public_ip_address = network_client.public_ip_addresses.get(group_name, ip_name) - - AppScaleLogger.verbose("Creating/Updating the Network Interface '{}'". - format(interface_name), verbose) - network_interface_ip_conf = NetworkInterfaceIPConfiguration( - name=interface_name, private_ip_allocation_method=IPAllocationMethod.dynamic, - subnet=subnet, public_ip_address=PublicIPAddress(id=(public_ip_address.id))) - - try: - result = network_client.network_interfaces.create_or_update(group_name, - interface_name, NetworkInterface(location=region, - ip_configurations=[network_interface_ip_conf])) - self.sleep_until_update_operation_done(result, interface_name, verbose) - except CloudError as error: - logging.exception("Azure agent received a CloudError.") - raise AgentRuntimeException("Unable to create network interface. " - "Reason: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create network interface. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def sleep_until_update_operation_done(self, result, resource_name, verbose): - """ Sleeps until the create/update operation for the resource is completed - successfully. - Args: - result: An instance, of the AzureOperationPoller to poll for the status - of the operation being performed. - resource_name: The name of the resource being updated. - """ - time_start = time.time() - while not result.done(): - AppScaleLogger.verbose("Waiting {0} second(s) for {1} to be created/updated.". - format(self.SLEEP_TIME, resource_name), verbose) - time.sleep(self.SLEEP_TIME) - total_sleep_time = time.time() - time_start - if total_sleep_time > self.MAX_SLEEP_TIME: - AppScaleLogger.log("Waited {0} second(s) for {1} to be created/updated. " - "Operation has timed out.".format(total_sleep_time, resource_name)) - break - - def create_resource_group(self, parameters, credentials): - """ Creates a Resource Group for the application using the Service Principal - Credentials, if it does not already exist. In the case where no resource - group is specified, a default group is created. - Args: - parameters: A dict, containing all the parameters necessary to - authenticate this user with Azure. - credentials: A ServicePrincipalCredentials instance, that can be used to - access or create any resources. - Raises: - AgentConfigurationException: If we are unable to authenticate with Azure. - AgentRuntimeException: If there was a problem creating or accessing - a resource group with the given subscription. - """ - subscription_id = str(parameters[self.PARAM_SUBSCRIBER_ID]) - resource_client = ResourceManagementClient(credentials, subscription_id) - resource_group_name = parameters[self.PARAM_RESOURCE_GROUP] - - tag_name = parameters[self.PARAM_TAG] - - storage_client = StorageManagementClient(credentials, subscription_id) - try: - resource_client.providers.register(self.MICROSOFT_STORAGE_RESOURCE) - # If the resource group does not already exist, create a new one with the - # specified storage account. - if not self.does_resource_group_exist(resource_group_name, resource_client): - AppScaleLogger.log("Creating a new resource group '{0}' with the tag " - "'{1}'.".format(resource_group_name, tag_name)) - resource_client.resource_groups.create_or_update( - resource_group_name, ResourceGroup(location=parameters[self.PARAM_ZONE], - tags={'tag': tag_name})) - - if not self.MARKETPLACE_IMAGE.match(parameters[self.PARAM_IMAGE_ID]) \ - and parameters.get(self.PARAM_STORAGE_ACCOUNT): - # If it already exists, check if the specified storage account exists - # under it and if not, create a new account. - storage_accounts = storage_client.storage_accounts.\ - list_by_resource_group(resource_group_name) - acct_names = [] - for account in storage_accounts: - acct_names.append(account.name) - - if parameters[self.PARAM_STORAGE_ACCOUNT] in acct_names: - AppScaleLogger.log("Storage account '{0}' under '{1}' resource group " - "already exists. So not creating it again.".format( - parameters[self.PARAM_STORAGE_ACCOUNT], resource_group_name)) - else: - self.create_storage_account(parameters, storage_client) - except CloudError as error: - logging.exception("Unable to create resource group.") - raise AgentConfigurationException("Unable to create a resource group " - "using the credentials provided: {}".format(error.message)) - except ClientException as e: - logging.exception("ClientException received while attempting to contact " - "Azure.") - raise AgentConfigurationException("Unable to communicate with Azure " - "while trying to create resource group. Please check your cloud " - "configuration. Reason: {}".format(e.message)) - - def create_storage_account(self, parameters, storage_client): - """ Creates a Storage Account under the Resource Group, if it does not - already exist. In the case where no resource group is specified, a default - storage account is created. - Args: - parameters: A dict, containing all the parameters necessary to authenticate - this user with Azure. - credentials: A ServicePrincipalCredentials instance, that can be used to access or - create any resources. - Raises: - AgentConfigurationException: If there was a problem creating or accessing - a storage account with the given subscription. - """ - storage_account = parameters[self.PARAM_STORAGE_ACCOUNT] - rg_name = parameters[self.PARAM_RESOURCE_GROUP] - - try: - AppScaleLogger.log("Creating a new storage account '{0}' under the " - "resource group '{1}'.".format(storage_account, rg_name)) - result = storage_client.storage_accounts.create( - rg_name, storage_account,StorageAccountCreateParameters( - sku=StorageSku(SkuName.standard_lrs), kind=Kind.storage, - location=parameters[self.PARAM_ZONE])) - # Result is a msrestazure.azure_operation.AzureOperationPoller instance. - # wait() insures polling the underlying async operation until it's done. - result.wait() - except CloudError as error: - logging.exception("Unable to create storage account.") - raise AgentConfigurationException("Unable to create a storage account " - "using the credentials provided: {}".format(error.message)) - - def does_resource_group_exist(self, resource_group_name, resource_client): - """ Checks if the given resource group already exists. - Args: - resource_group_name: The name of the resource group to check. - resource_client: An instance of the ResourceManagementClient. - Returns: - True, if resource group already exists. - False, otherwise. - """ - resource_groups = resource_client.resource_groups.list() - for resource_group in resource_groups: - if resource_group_name == resource_group.name: - return True - return False diff --git a/appscale/tools/agents/base_agent.py b/appscale/tools/agents/base_agent.py deleted file mode 100644 index 5c008619..00000000 --- a/appscale/tools/agents/base_agent.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/env python - - -class BaseAgent(object): - """BaseAgent class defines the interface that must be implemented by - each cloud agent.""" - - # The following constants are string literals that can be used by callers to - # index into the parameters that the user passes in, as opposed to having to - # type out the strings each time we need them. These are the params that - # all agents have in common. - PARAM_AUTOSCALE_AGENT = 'autoscale_agent' - PARAM_CREDENTIALS = 'credentials' - PARAM_GROUP = 'group' - PARAM_IMAGE_ID = 'image_id' - PARAM_INSTANCE_TYPE = 'instance_type' - PARAM_KEYNAME = 'keyname' - PARAM_INSTANCE_IDS = 'instance_ids' - PARAM_REGION = 'region' - PARAM_ZONE = 'zone' - PARAM_STATIC_IP = 'static_ip' - PARAM_TEST = 'test' - PARAM_VERBOSE = 'IS_VERBOSE' - - # A str constant the callers can use when they want to start virtual machines. - OPERATION_RUN = 'run' - - - # A str constant the callers can use when they want to kill virtual machines. - OPERATION_TERMINATE = 'terminate' - - - def assert_credentials_are_valid(self, parameters): - """Checks with the given cloud to ensure that the given credentials can be - used to interact with it. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - - Raises: - AgentConfigurationException: If the given credentials cannot be used to - make requests to the underlying cloud. - """ - raise NotImplementedError - - - def configure_instance_security(self, parameters): - """Configure and setup security features for the VMs spawned via this - agent. - - This method is called before starting virtual machines. Implementations may - configure security features such as VM login and firewalls in this method. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - - Returns: - True if some action was taken to configure security for the VMs - and False otherwise. - - Raises: - AgentRuntimeException: If security features could not be successfully - configured in the underlying cloud. - """ - raise NotImplementedError - - - def describe_instances(self, parameters, pending=False): - """Query the underlying cloud platform regarding VMs that are running. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - pending: If we should show pending instances. - Returns: - A tuple of the form (public, private, id) where public is a list - of private IP addresses, private is a list of private IP addresses, - and id is a list of platform specific VM identifiers. - """ - raise NotImplementedError - - - def run_instances(self, count, parameters, security_configured, public_ip_needed): - """Start a set of virtual machines using the parameters provided. - - Args: - count: An int that indicates the number of VMs to be spawned. - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - security_configured: True if security has been configured for the VMs - by this agent, or False otherwise. This is usually the value that was - returned by a call to the configure_instance_security method. - Returns: - A tuple consisting of information related to the spawned VMs. The - tuple should contain a list of instance IDs, a list of public IP - addresses and a list of private IP addresses. - - Raises: - AgentRuntimeException: If an error occurs while trying to spawn VMs. - """ - raise NotImplementedError - - - def associate_static_ip(self, instance_id, static_ip): - """Associates the given static IP address with the given instance ID. - - Args: - instance_id: A str that names the instance that the static IP should be - bound to. - static_ip: A str naming the static IP to bind to the given instance. - """ - raise NotImplementedError - - - def terminate_instances(self, parameters): - """Terminate a set of virtual machines using the parameters given. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - """ - raise NotImplementedError - - - def does_address_exist(self, parameters): - """Verifies that the specified static IP address has been allocated, and - belongs to the user with the given credentials. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud, as well as a key indicating which static IP address - should be validated. - Returns: - A bool that indicates if the given static IP address exists, and belongs - to this user. - """ - raise NotImplementedError - - - def does_image_exist(self, parameters): - """Verifies that the specified machine image exists in this cloud. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud, as well as a key indicating which machine image should - be checked for existence. - Returns: - A bool that indicates if the machine image exists in this cloud. - """ - raise NotImplementedError - - - def does_disk_exist(self, parameters, disk): - """Verifies that the specified persistent disk exists in this cloud. - - Args: - parameters: A dict that includes the parameters needed to authenticate - with this cloud. - disk: A str containing the name of the disk that we should check for - existence. - Returns: - True if the named persistent disk exists, and False otherwise, - """ - raise NotImplementedError - - - def does_zone_exist(self, parameters): - """Verifies that the specified zone exists in this cloud. - - Args: - parameters: A dict that includes a key indicating the zone to check for - existence. - Returns: - True if the zone exists, and False otherwise. - """ - raise NotImplementedError - - - def cleanup_state(self, parameters): - """Removes any remote state that was created to run AppScale instances - during this deployment. - - Args: - parameters: A dict that includes keys indicating the remote state - that should be deleted. - """ - raise NotImplementedError - - - def get_params_from_args(self, args): - """Converts a Namespace of arguments to a dict, the internal format used by - Agents. - - Cloud-specific parameters are also added to this dict. - - Args: - args: A Namespace of arguments. - - Returns: - A dict that maps each argument given to the value that was associated with - it. - """ - raise NotImplementedError - - - def assert_required_parameters(self, parameters, operation): - """Check whether all the platform specific parameters are present in the - provided dict. If all the parameters required to perform the given operation - is available this method simply returns. Otherwise it throws an - AgentConfigurationException. - - Args: - parameters: A dict containing values necessary to authenticate with the - underlying cloud. - operation: A str representing the operation for which the parameters - should be checked. - - Raises: - AgentConfigurationException: If a required parameter is absent. - """ - raise NotImplementedError - - - def has_parameter(self, param, params): - """Checks whether the parameter is present in the params dict. - - Args: - param: A str representing a parameter name. - params: A dictionary of parameters. - - Returns: - True if params contains param and the value of param is not None. - Returns False otherwise. - """ - return params.get(param) != None - - - def diff(self, list1, list2): - """ - Returns the list of entries that are present in list1 but not - in list2. - - Args: - list1: A list of elements - list2: Another list of elements - - Returns: - A list of elements unique to list1 - """ - diffed_list = [] - list2 = set(list2) - for item in list1: - if item not in list2: - diffed_list.append(item) - return diffed_list - - -class AgentConfigurationException(Exception): - """An agent implementation may throw this exception when it detects that a - given cloud configuration is missing some required parameters or contains - invalid values. - """ - - - def __init__(self, msg): - Exception.__init__(self, msg) - - -class AgentRuntimeException(Exception): - - - def __init__(self, msg): - Exception.__init__(self, msg) diff --git a/appscale/tools/agents/ec2_agent.py b/appscale/tools/agents/ec2_agent.py deleted file mode 100644 index 239212b5..00000000 --- a/appscale/tools/agents/ec2_agent.py +++ /dev/null @@ -1,1034 +0,0 @@ -""" -Helper library for EC2 interaction -""" -import boto -import boto.ec2 -import boto.vpc -import datetime -import glob -import os -import time - -from boto.ec2.networkinterface import NetworkInterfaceCollection -from boto.ec2.networkinterface import NetworkInterfaceSpecification -from boto.exception import EC2ResponseError - -from appscale.tools.appscale_logger import AppScaleLogger -from appscale.tools.local_state import LocalState -from base_agent import AgentConfigurationException -from base_agent import AgentRuntimeException -from base_agent import BaseAgent - - -# pylint: disable-msg=W0511 -# don't bother about todo's - - -class SecurityGroupNotFoundException(Exception): - """ Exception to raise when a security group could not be found on EC2.""" - pass - - -class EC2Agent(BaseAgent): - """ - EC2 infrastructure agent class which can be used to spawn and terminate - VMs in an EC2 based environment. - """ - - # The maximum amount of time, in seconds, that we are willing to wait for - # a virtual machine to start up, from the initial run-instances request. - # Setting this value is a bit of an art, but we choose the value below - # because our image is roughly 10GB in size, and if Eucalyptus doesn't - # have the image cached, it could take half an hour to get our image - # started. We set this to 60 minutes so we have some leeway. - MAX_VM_CREATION_TIME = 3600 - - # The amount of time that run_instances waits between each describe-instances - # request. Setting this value too low can cause Eucalyptus to interpret - # requests as replay attacks. - SLEEP_TIME = 20 - - PARAM_SPOT = 'use_spot_instances' - PARAM_SPOT_PRICE = 'max_spot_price' - PARAM_SUBNET_ID = 'aws_subnet_id' - PARAM_VPC_ID = 'aws_vpc_id' - - REQUIRED_EC2_RUN_INSTANCES_PARAMS = ( - BaseAgent.PARAM_CREDENTIALS, - BaseAgent.PARAM_GROUP, - BaseAgent.PARAM_IMAGE_ID, - BaseAgent.PARAM_KEYNAME, - PARAM_SPOT - ) - - REQUIRED_EC2_TERMINATE_INSTANCES_PARAMS = ( - BaseAgent.PARAM_CREDENTIALS, - BaseAgent.PARAM_INSTANCE_IDS - ) - - # A list of the environment variables that must be provided - # to control machines in Amazon EC2. - REQUIRED_EC2_CREDENTIALS = ( - 'EC2_SECRET_KEY', - 'EC2_ACCESS_KEY' - ) - - # A tuple of the credentials that we build our internal - # credential list from. - REQUIRED_CREDENTIALS = REQUIRED_EC2_CREDENTIALS - - - # An int that indicates how many times we should try to create a security - # group and authorize it for TCP, UDP, or ICMP traffic. - SECURITY_GROUP_RETRY_COUNT = 3 - - - DESCRIBE_INSTANCES_RETRY_COUNT = 3 - - - # The region that instances should be started in and terminated from, if the - # user does not specify a zone. - DEFAULT_REGION = "us-east-1" - - - # A list of EC2 instance types that have less than 4 GB of RAM, the amount - # recommended by Cassandra. AppScale will still run on these instance types, - # but is likely to crash after a day or two of use (as Cassandra will attempt - # to malloc ~800MB of memory, which will fail on these instance types). - DISALLOWED_INSTANCE_TYPES = ["m1.small", "c1.medium", "t1.micro"] - - - def assert_credentials_are_valid(self, parameters): - """Contacts AWS to see if the given access key and secret key represent a - valid set of credentials. - - Args: - parameters: A dict containing the user's AWS access key and secret key. - Raises: - AgentConfigurationException: If the given AWS access key and secret key - cannot be used to make requests to AWS. - """ - conn = self.open_connection(parameters) - try: - conn.get_all_instances() - except EC2ResponseError: - raise AgentConfigurationException("We couldn't validate your EC2 " + \ - "access key and EC2 secret key. Are your credentials valid?") - - - def configure_instance_security(self, parameters): - """ - Setup EC2 security keys and groups. Required input values are read from - the parameters dictionary. More specifically, this method expects to - find a 'keyname' parameter and a 'group' parameter in the parameters - dictionary. Using these provided values, this method will create a new - EC2 key-pair and a security group. Security group will be granted permission - to access any port on the instantiated VMs. (Also see documentation for the - BaseAgent class) - - Args: - parameters: A dictionary of parameters. - """ - keyname = parameters[self.PARAM_KEYNAME] - group = parameters[self.PARAM_GROUP] - is_autoscale = parameters[self.PARAM_AUTOSCALE_AGENT] - - AppScaleLogger.log("Verifying that keyname {0}".format(keyname) + \ - " is not already registered.") - conn = self.open_connection(parameters) - - # While creating instances during autoscaling, we do not need to create a - # new keypair or a security group. We just make use of the existing one. - if is_autoscale in ['True', True]: - return - - if conn.get_key_pair(keyname): - self.handle_failure("SSH keyname {0} is already registered. Please " \ - "change the 'keyname' specified in your AppScalefile to a different " \ - "value, or erase it to have one automatically generated for you." \ - .format(keyname)) - - try: - self.get_security_group_by_name(conn, group, - parameters.get(self.PARAM_VPC_ID)) - except SecurityGroupNotFoundException: - # If this is raised, the group does not exist. - pass - else: - self.handle_failure("Security group {0} is already registered. Please " - "change the 'group' specified in your AppScalefile " - "to a different value, or erase it to have one " - "automatically generated for you.".format(group)) - - - AppScaleLogger.log("Creating key pair: {0}".format(keyname)) - key_pair = conn.create_key_pair(keyname) - ssh_key = '{0}{1}.key'.format(LocalState.LOCAL_APPSCALE_PATH, keyname) - LocalState.write_key_file(ssh_key, key_pair.material) - - sg = self.create_security_group(parameters, group) - - self.authorize_security_group(parameters, sg.id, from_port=1, - to_port=65535, ip_protocol='udp', - cidr_ip='0.0.0.0/0') - self.authorize_security_group(parameters, sg.id, from_port=1, - to_port=65535, ip_protocol='tcp', - cidr_ip='0.0.0.0/0') - self.authorize_security_group(parameters, sg.id, from_port=-1, - to_port=-1, ip_protocol='icmp', - cidr_ip='0.0.0.0/0') - return True - - - def create_security_group(self, parameters, group): - """Creates a new security group in AWS with the given name. - - Args: - parameters: A dict that contains the credentials necessary to authenticate - with AWS. - group: A str that names the group that should be created. - Returns: - The 'boto.ec2.securitygroup.SecurityGroup' that was just created. - Raises: - AgentRuntimeException: If the security group could not be created. - """ - AppScaleLogger.log('Creating security group: {0}'.format(group)) - conn = self.open_connection(parameters) - specified_vpc = parameters.get(self.PARAM_VPC_ID) - - retries_left = self.SECURITY_GROUP_RETRY_COUNT - while retries_left: - try: - conn.create_security_group(group, 'AppScale security group', - specified_vpc) - except EC2ResponseError: - pass - try: - return self.get_security_group_by_name(conn, group, specified_vpc) - except SecurityGroupNotFoundException: - pass - time.sleep(self.SLEEP_TIME) - retries_left -= 1 - - raise AgentRuntimeException("Couldn't create security group with " \ - "name {0}".format(group)) - - - @classmethod - def get_security_group_by_name(cls, conn, group, vpc_id): - """Gets a security group in AWS with the given name. - - Args: - conn: A boto connection. - group: A str that names the group that should be found. - vpc_id: A str containing the id of the VPC, used for checking if the - security group located is in the proper VPC or None. - Returns: - The 'boto.ec2.securitygroup.SecurityGroup' that has the correct group - name. - Raises: - SecurityGroupNotFoundException: If the security group could not be found. - """ - for sg in conn.get_all_security_groups(): - if sg.name == group and sg.vpc_id == vpc_id: - return sg - - if vpc_id: - msg = 'Could not find security group with name {} in VPC {}!'.format( - group, vpc_id) - else: - msg = 'Could not find security group with name {} in classic ' \ - 'network!'.format(group) - raise SecurityGroupNotFoundException(msg) - - - def authorize_security_group(self, parameters, group_id, from_port, - to_port, ip_protocol, cidr_ip): - """Opens up traffic on the given port range for traffic of the named type. - - Args: - parameters: A dict that contains the credentials necessary to authenticate - with AWS. - group_id: A str that contains the id of the group whose ports should be - opened. - from_port: An int that names the first port that access should be allowed - on. - to_port: An int that names the last port that access should be allowed on. - ip_protocol: A str that indicates if TCP, UDP, or ICMP traffic should be - allowed. - cidr_ip: A str that names the IP range that traffic should be allowed - from. - Raises: - AgentRuntimeException: If the ports could not be opened on the security - group. - """ - AppScaleLogger.log('Authorizing security group {0} for {1} traffic from ' \ - 'port {2} to port {3}'.format(group_id, ip_protocol, from_port, to_port)) - conn = self.open_connection(parameters) - retries_left = self.SECURITY_GROUP_RETRY_COUNT - while retries_left: - try: - conn.authorize_security_group(group_id=group_id, from_port=from_port, - to_port=to_port, cidr_ip=cidr_ip, - ip_protocol=ip_protocol) - except EC2ResponseError: - pass - try: - group_info = self.get_security_group_by_name( - conn, parameters[self.PARAM_GROUP], parameters.get(self.PARAM_VPC_ID)) - for rule in group_info.rules: - if int(rule.from_port) == from_port and int(rule.to_port) == to_port \ - and rule.ip_protocol == ip_protocol: - return - except SecurityGroupNotFoundException as e: - raise AgentRuntimeException(e.message) - time.sleep(self.SLEEP_TIME) - retries_left -= 1 - - raise AgentRuntimeException("Couldn't authorize {0} traffic from port " \ - "{1} to port {2} on CIDR IP {3}".format(ip_protocol, from_port, to_port, - cidr_ip)) - - - def get_params_from_args(self, args): - """ - Searches through args to build a dict containing the parameters - necessary to interact with Amazon EC2. - - Args: - args: A Namespace containing the arguments that the user has - invoked an AppScale Tool with. - """ - # need to convert this to a dict if it is not already - if not isinstance(args, dict): - args = vars(args) - - params = { - self.PARAM_CREDENTIALS : {}, - self.PARAM_GROUP : args['group'], - self.PARAM_IMAGE_ID : args['machine'], - self.PARAM_INSTANCE_TYPE : args['instance_type'], - self.PARAM_KEYNAME : args['keyname'], - self.PARAM_STATIC_IP : args.get(self.PARAM_STATIC_IP), - self.PARAM_ZONE : args.get('zone'), - self.PARAM_VERBOSE : args.get('verbose', False), - self.PARAM_AUTOSCALE_AGENT : False - } - - if params[self.PARAM_ZONE]: - params[self.PARAM_REGION] = params[self.PARAM_ZONE][:-1] - else: - params[self.PARAM_REGION] = self.DEFAULT_REGION - - for credential in self.REQUIRED_CREDENTIALS: - if args.get(credential): - params[self.PARAM_CREDENTIALS][credential] = args[credential] - else: - raise AgentConfigurationException("Couldn't find {0} in your " \ - "environment. Please set it and run AppScale again." - .format(credential)) - self.assert_credentials_are_valid(params) - - if args.get('use_spot_instances') == True: - params[self.PARAM_SPOT] = True - else: - params[self.PARAM_SPOT] = False - - if params[self.PARAM_SPOT]: - if args.get('max_spot_price'): - params[self.PARAM_SPOT_PRICE] = args['max_spot_price'] - else: - params[self.PARAM_SPOT_PRICE] = self.get_optimal_spot_price( - self.open_connection(params), params[self.PARAM_INSTANCE_TYPE], - params[self.PARAM_ZONE]) - - # If VPC id and Subnet id are not set assume classic networking should be - # used. - vpc_id = args.get(self.PARAM_VPC_ID) - subnet_id = args.get(self.PARAM_SUBNET_ID) - if not vpc_id and not subnet_id: - AppScaleLogger.log('Using Classic Networking since subnet and vpc were ' - 'not specified.') - # All further checks are for VPC Networking. - elif (vpc_id or subnet_id) and not (vpc_id and subnet_id): - raise AgentConfigurationException('Both VPC id and Subnet id must be ' - 'specified to use VPC Networking.') - else: - # VPC must exist. - vpc_conn = self.open_vpc_connection(params) - - params[self.PARAM_VPC_ID] = args[self.PARAM_VPC_ID] - try: - vpc_conn.get_all_vpcs(params[self.PARAM_VPC_ID]) - except EC2ResponseError as e: - raise AgentConfigurationException('Error looking for vpc: {}'.format( - e.message)) - - # Subnet must exist. - all_subnets = vpc_conn.get_all_subnets(filters={'vpcId': params[self.PARAM_VPC_ID]}) - params[self.PARAM_SUBNET_ID] = args[self.PARAM_SUBNET_ID] - - if not any(subnet.id == params[self.PARAM_SUBNET_ID] for subnet in all_subnets): - raise AgentConfigurationException('Specified subnet {} does not exist ' - 'in vpc {}!'.format(params[self.PARAM_SUBNET_ID], - params[self.PARAM_VPC_ID])) - return params - - - def get_cloud_params(self, keyname): - """Searches through the locations.json file with key - 'infrastructure_info' to build a dict containing the - parameters necessary to interact with Amazon EC2. - - Args: - keyname: The name of the SSH keypair that uniquely identifies this - AppScale deployment. - """ - params = { - self.PARAM_CREDENTIALS : {}, - self.PARAM_GROUP : LocalState.get_group(keyname), - self.PARAM_KEYNAME : keyname - } - - zone = LocalState.get_zone(keyname) - if zone: - params[self.PARAM_REGION] = zone[:-1] - else: - params[self.PARAM_REGION] = self.DEFAULT_REGION - - - for credential in self.REQUIRED_CREDENTIALS: - cred = LocalState.get_infrastructure_option(tag=credential, - keyname=keyname) - if not cred: - raise AgentConfigurationException("no " + credential) - - params[self.PARAM_CREDENTIALS][credential] = cred - - return params - - def assert_required_parameters(self, parameters, operation): - """ - Assert that all the parameters required for the EC2 agent are in place. - (Also see documentation for the BaseAgent class) - - Args: - parameters: A dictionary of parameters. - operation: Operations to be invoked using the above parameters. - """ - required_params = () - if operation == BaseAgent.OPERATION_RUN: - required_params = self.REQUIRED_EC2_RUN_INSTANCES_PARAMS - elif operation == BaseAgent.OPERATION_TERMINATE: - required_params = self.REQUIRED_EC2_TERMINATE_INSTANCES_PARAMS - - # make sure the user set something for each parameter - for param in required_params: - if not self.has_parameter(param, parameters): - raise AgentConfigurationException('no ' + param) - - # next, make sure the user actually put in their credentials - for credential in self.REQUIRED_EC2_CREDENTIALS: - if not self.has_parameter(credential, parameters['credentials']): - raise AgentConfigurationException('no ' + credential) - - def describe_instances(self, parameters, pending=False): - """ - Retrieves the list of running instances that have been instantiated using a - particular EC2 keyname. The target keyname is read from the input parameter - map. (Also see documentation for the BaseAgent class). - - Args: - parameters: A dictionary containing the 'keyname' parameter. - pending: Indicates we also want the pending instances. - Returns: - A tuple of the form (public_ips, private_ips, instances) where each - member is a list. - """ - instance_ids = [] - public_ips = [] - private_ips = [] - - conn = self.open_connection(parameters) - reservations = conn.get_all_instances() - instances = [i for r in reservations for i in r.instances] - for i in instances: - if (i.state == 'running' or (pending and i.state == 'pending'))\ - and i.key_name == parameters[self.PARAM_KEYNAME]: - instance_ids.append(i.id) - public_ips.append(i.ip_address or i.private_ip_address) - private_ips.append(i.private_ip_address) - return public_ips, private_ips, instance_ids - - def run_instances(self, count, parameters, security_configured, public_ip_needed): - """ - Spawns the specified number of EC2 instances using the parameters - provided. This method is blocking in that it waits until the - requested VMs are properly booted up. However if the requested - VMs cannot be procured within 1800 seconds, this method will treat - it as an error and return. (Also see documentation for the BaseAgent - class) - - Args: - count: Number of VMs to spawned. - parameters: A dictionary of parameters. This must contain - 'keyname', 'group', 'image_id' and 'instance_type' parameters. - security_configured: Uses this boolean value as an heuristic to - detect brand new AppScale deployments. - public_ip_needed: A boolean, specifies whether to launch with a public - ip or not. - Returns: - A tuple of the form (instances, public_ips, private_ips) - """ - image_id = parameters[self.PARAM_IMAGE_ID] - instance_type = parameters[self.PARAM_INSTANCE_TYPE] - keyname = parameters[self.PARAM_KEYNAME] - group = parameters[self.PARAM_GROUP] - zone = parameters[self.PARAM_ZONE] - - # In case of autoscaling, the server side passes these parameters as a - # string, so this check makes sure that spot instances are only created - # when the flag is True. - spot = parameters[self.PARAM_SPOT] in ['True', 'true', True] - - AppScaleLogger.log("Starting {0} machines with machine id {1}, with " \ - "instance type {2}, keyname {3}, in security group {4}, in availability" \ - " zone {5}".format(count, image_id, instance_type, keyname, group, zone)) - - if spot: - AppScaleLogger.log("Using spot instances") - else: - AppScaleLogger.log("Using on-demand instances") - - start_time = datetime.datetime.now() - active_public_ips = [] - active_private_ips = [] - active_instances = [] - - # Make sure we do not have terminated instances using the same keyname. - instances = self.__describe_instances(parameters) - term_instance_info = self.__get_instance_info(instances, - 'terminated', keyname) - if len(term_instance_info[2]): - self.handle_failure('SSH keyname {0} is already registered to a '\ - 'terminated instance. Please change the "keyname" '\ - 'you specified in your AppScalefile to a different '\ - 'value. If the keyname was autogenerated, erase it '\ - 'to have a new one generated for you.'.format(keyname)) - - try: - attempts = 1 - while True: - instance_info = self.describe_instances(parameters) - active_public_ips = instance_info[0] - active_private_ips = instance_info[1] - active_instances = instance_info[2] - - # If security has been configured on this agent just now, - # that's an indication that this is a fresh cloud deployment. - # As such it's not expected to have any running VMs. - if len(active_instances) > 0 or security_configured: - break - elif attempts == self.DESCRIBE_INSTANCES_RETRY_COUNT: - self.handle_failure('Failed to invoke describe_instances') - attempts += 1 - - # Get subnet from parameters. - subnet = parameters.get(self.PARAM_SUBNET_ID) - - network_interfaces = None - groups = None - - conn = self.open_connection(parameters) - - # A subnet indicates we're using VPC Networking. - if subnet: - # Get security group by name. - try: - sg = self.get_security_group_by_name(conn, group, - parameters[self.PARAM_VPC_ID]) - except SecurityGroupNotFoundException as e: - raise AgentRuntimeException(e.message) - # Create network interface specification. - network_interface = NetworkInterfaceSpecification( - associate_public_ip_address=public_ip_needed, - groups=[sg.id], subnet_id=subnet) - network_interfaces = NetworkInterfaceCollection(network_interface) - else: - groups = [group] - - if spot: - price = parameters[self.PARAM_SPOT_PRICE] or \ - self.get_optimal_spot_price(conn, instance_type, zone) - - conn.request_spot_instances(str(price), image_id, key_name=keyname, - instance_type=instance_type, count=count, - placement=zone, security_groups=groups, - network_interfaces=network_interfaces) - else: - conn.run_instances(image_id, count, count, key_name=keyname, - instance_type=instance_type, placement=zone, - security_groups=groups, - network_interfaces=network_interfaces) - - instance_ids = [] - public_ips = [] - private_ips = [] - end_time = datetime.datetime.now() + datetime.timedelta(0, - self.MAX_VM_CREATION_TIME) - - while datetime.datetime.now() < end_time: - AppScaleLogger.log("Waiting for your instances to start...") - public_ips, private_ips, instance_ids = self.describe_instances( - parameters) - - # If we need a public ip, make sure we actually get one. - if public_ip_needed and not self.diff(public_ips, private_ips): - time.sleep(self.SLEEP_TIME) - continue - - public_ips = self.diff(public_ips, active_public_ips) - private_ips = self.diff(private_ips, active_private_ips) - instance_ids = self.diff(instance_ids, active_instances) - if count == len(public_ips): - break - time.sleep(self.SLEEP_TIME) - - if not public_ips: - self.handle_failure('No public IPs were able to be procured ' - 'within the time limit') - - if len(public_ips) != count: - for index in range(0, len(public_ips)): - if public_ips[index] == '0.0.0.0': - instance_to_term = instance_ids[index] - AppScaleLogger.log('Instance {0} failed to get a public IP address'\ - 'and is being terminated'.format(instance_to_term)) - conn.terminate_instances([instance_to_term]) - - end_time = datetime.datetime.now() - total_time = end_time - start_time - if spot: - AppScaleLogger.log("Started {0} spot instances in {1} seconds" \ - .format(count, total_time.seconds)) - else: - AppScaleLogger.log("Started {0} on-demand instances in {1} seconds" \ - .format(count, total_time.seconds)) - return instance_ids, public_ips, private_ips - except EC2ResponseError as exception: - self.handle_failure('EC2 response error while starting VMs: ' + - exception.error_message) - - - def associate_static_ip(self, parameters, instance_id, elastic_ip): - """Associates the given Elastic IP address with the given instance ID. - - Args: - parameters: A dict that includes the credentials necessary to communicate - with Amazon Web Services. - instance_id: A str naming the running instance to associate an Elastic IP - with. - elastic_ip: A str naming the already allocated Elastic IP address that - will be associated. - """ - try: - conn = self.open_connection(parameters) - conn.associate_address(instance_id, elastic_ip) - except EC2ResponseError as exception: - self.handle_failure('Unable to associate Elastic IP {0} with instance ' \ - 'ID {1} because: {2}'.format(elastic_ip, instance_id, - exception.error_message)) - - - def stop_instances(self, parameters): - """ - Stop one of more EC2 instances. The input instance IDs are - fetched from the 'instance_ids' parameters in the input map. (Also - see documentation for the BaseAgent class) - - Args: - parameters: A dictionary of parameters. - """ - instance_ids = parameters[self.PARAM_INSTANCE_IDS] - conn = self.open_connection(parameters) - conn.stop_instances(instance_ids) - AppScaleLogger.log('Stopping instances: '+' '.join(instance_ids)) - if not self.wait_for_status_change(parameters, conn, 'stopped', - max_wait_time=120): - AppScaleLogger.log("re-stopping instances: "+' '.join(instance_ids)) - conn.stop_instances(instance_ids) - if not self.wait_for_status_change(parameters, conn, 'stopped', - max_wait_time=120): - self.handle_failure("ERROR: could not stop instances: " + \ - ' '.join(instance_ids)) - - - def terminate_instances(self, parameters): - """ - Terminate one of more EC2 instances. The input instance IDs are - fetched from the 'instance_ids' parameters in the input map. (Also - see documentation for the BaseAgent class) - - Args: - parameters: A dictionary of parameters. - """ - instance_ids = parameters[self.PARAM_INSTANCE_IDS] - conn = self.open_connection(parameters) - conn.terminate_instances(instance_ids) - AppScaleLogger.log('Terminating instances: ' + ' '.join(instance_ids)) - if not self.wait_for_status_change(parameters, conn, 'terminated', - max_wait_time=120): - AppScaleLogger.log("re-terminating instances: " + ' '.join(instance_ids)) - conn.terminate_instances(instance_ids) - if not self.wait_for_status_change(parameters, conn, 'terminated', - max_wait_time=120): - self.handle_failure("ERROR: could not terminate instances: " + \ - ' '.join(instance_ids)) - # Sending a second terminate to a terminated instance to remove it - # from the system (ie no more in describe-instances). This helps when - # bringing deployments up and down frequently and instances are still - # associated with keyname (although they are terminated). - AppScaleLogger.log("Removing terminated instances: " + ' '.join(instance_ids)) - conn.terminate_instances(instance_ids) - - - def wait_for_status_change(self, parameters, conn, state_requested, - max_wait_time=60,poll_interval=10): - """ After we have sent a signal to the cloud infrastructure to change the state - of the instances (unsually from runnning to either stoppped or - terminated), wait for the status to change. If all the instances change - successfully, return True, if not return False. - - Args: - parameters: A dictionary of parameters. - conn: A connection object returned from self.open_connection(). - state_requested: String of the requested final state of the instances. - max_wait_time: int of maximum amount of time (in seconds) to wait for the - state change. - poll_interval: int of the number of seconds to wait between checking of - the state. - """ - time_start = time.time() - instance_ids = parameters[self.PARAM_INSTANCE_IDS] - instances_in_state = {} - while True: - time.sleep(poll_interval) - reservations = conn.get_all_instances(instance_ids) - instances = [i for r in reservations for i in r.instances] - for i in instances: - # instance i.id reports status = i.state - if i.state == state_requested and \ - i.key_name == parameters[self.PARAM_KEYNAME]: - if i.id not in instances_in_state.keys(): - instances_in_state[i.id] = 1 # mark instance done - if len(instances_in_state.keys()) >= len(instance_ids): - return True - if time.time() - time_start > max_wait_time: - return False - - - def does_address_exist(self, parameters): - """ Queries Amazon EC2 to see if the specified Elastic IP address has been - allocated with the given credentials. - - Args: - parameters: A dict that contains the Elastic IP to check for existence. - Returns: - True if the given Elastic IP has been allocated, and False otherwise. - """ - elastic_ip = parameters[self.PARAM_STATIC_IP] - try: - conn = self.open_connection(parameters) - conn.get_all_addresses(elastic_ip) - AppScaleLogger.log('Elastic IP {0} can be used for this AppScale ' \ - 'deployment.'.format(elastic_ip)) - return True - except boto.exception.EC2ResponseError: - AppScaleLogger.log('Elastic IP {0} does not exist.'.format(elastic_ip)) - return False - - - def does_image_exist(self, parameters): - """ Queries Amazon EC2 to see if the specified image exists. - - Args: - parameters: A dict that contains the machine ID to check for existence. - Returns: - True if the machine ID exists, False otherwise. - """ - image_id = parameters[self.PARAM_IMAGE_ID] - try: - conn = self.open_connection(parameters) - conn.get_image(image_id) - AppScaleLogger.log('Machine image {0} does exist'.format(image_id)) - return True - except boto.exception.EC2ResponseError: - AppScaleLogger.log('Machine image {0} does not exist'.format(image_id)) - return False - - - def does_disk_exist(self, parameters, disk_name): - """ Queries Amazon EC2 to see if the specified EBS volume exists. - - Args: - parameters: A dict that contains the credentials needed to authenticate - with AWS. - disk_name: A str naming the EBS volume to check for existence. - Returns: - True if the named EBS volume exists, and False otherwise. - """ - conn = self.open_connection(parameters) - try: - conn.get_all_volumes([disk_name]) - AppScaleLogger.log('EBS volume {0} does exist'.format(disk_name)) - return True - except boto.exception.EC2ResponseError: - AppScaleLogger.log('EBS volume {0} does not exist'.format(disk_name)) - return False - - - def attach_disk(self, parameters, disk_name, instance_id): - """ Attaches the Elastic Block Store volume specified in 'disk_name' to this - virtual machine. - - Args: - parameters: A dict with keys for each parameter needed to connect to AWS. - disk_name: A str naming the EBS mount to attach to this machine. - instance_id: A str naming the id of the instance that the disk should be - attached to. In practice, callers add disks to their own instances. - Returns: - The location on the local filesystem where the disk has been attached. - """ - # In Amazon Web Services, if we're running on a Xen Paravirtualized machine, - # then devices get added starting at /dev/xvda. If not, they get added at - # /dev/sda. Find out which one we're on so that we know where the disk will - # get attached to. - if glob.glob("/dev/xvd*"): - mount_point = '/dev/xvdc' - elif glob.glob("/dev/vd*"): - mount_point = '/dev/vdc' - else: - mount_point = '/dev/sdc' - - conn = self.open_connection(parameters) - - try: - AppScaleLogger.log('Attaching volume {0} to instance {1}, at {2}'.format( - disk_name, instance_id, mount_point)) - conn.attach_volume(disk_name, instance_id, mount_point) - return mount_point - except EC2ResponseError as exception: - if self.disk_attached(conn, disk_name, instance_id): - return mount_point - AppScaleLogger.log('An error occurred when trying to attach volume {0} ' - 'to instance {1} at {2}'.format(disk_name, instance_id, mount_point)) - self.handle_failure('EC2 response error while attaching volume:' + - exception.error_message) - - - def disk_attached(self, conn, disk_name, instance_id): - """ Check if disk is attached to instance id. - - Args: - conn: A boto connection. - disk_name: A str naming the EBS mount to check. - instance_id: A str naming the id of the instance that the disk should be - attached to. - Returns: - True if the volume is attached to the instance, False if it is not. - """ - try: - volumes = conn.get_all_volumes(filters={'attachment.instance-id': - instance_id}) - for volume in volumes: - if volume.id == disk_name: - return True - - return False - except EC2ResponseError as exception: - AppScaleLogger.log('An error occurred when trying to find ' - 'attached volumes.') - self.handle_failure('EC2 response error while checking attached ' - 'volumes: {}'.format(exception.error_message)) - - - def detach_disk(self, parameters, disk_name, instance_id): - """ Detaches the EBS mount specified in disk_name from the named instance. - - Args: - parameters: A dict with keys for each parameter needed to connect to AWS. - disk_name: A str naming the EBS volume to detach. - instance_id: A str naming the id of the instance that the disk should be - detached from. - Returns: - True if the disk was detached, and False otherwise. - """ - conn = self.open_connection(parameters) - try: - conn.detach_volume(disk_name, instance_id) - return True - except boto.exception.EC2ResponseError: - AppScaleLogger.log("Could not detach volume with name {0}".format( - disk_name)) - return False - - - def does_zone_exist(self, parameters): - """ Queries Amazon EC2 to see if the specified availability zone exists. - - Args: - parameters: A dict that contains the availability zone to check for - existence. - Returns: - True if the availability zone exists, and False otherwise. - """ - zone = parameters[self.PARAM_ZONE] - try: - conn = self.open_connection(parameters) - conn.get_all_zones(zone) - AppScaleLogger.log('Availability zone {0} does exist'.format(zone)) - return True - except boto.exception.EC2ResponseError: - AppScaleLogger.log('Availability zone {0} does not exist'.format(zone)) - return False - - - def cleanup_state(self, parameters): - """ Removes the keyname and security group created during this AppScale - deployment. - - Args: - parameters: A dict that contains the keyname and security group to delete. - """ - AppScaleLogger.log("Deleting keyname {0}".format( - parameters[self.PARAM_KEYNAME])) - conn = self.open_connection(parameters) - conn.delete_key_pair(parameters[self.PARAM_KEYNAME]) - - AppScaleLogger.log("Deleting security group {0}".format( - parameters[self.PARAM_GROUP])) - retries_left = self.SECURITY_GROUP_RETRY_COUNT - while True: - try: - sg = self.get_security_group_by_name(conn, parameters[self.PARAM_GROUP], - parameters.get(self.PARAM_VPC_ID)) - conn.delete_security_group(group_id=sg.id) - return - except EC2ResponseError as e: - time.sleep(self.SLEEP_TIME) - retries_left -= 1 - if retries_left == 0: - raise AgentRuntimeException('Error deleting security group! Reason: ' - '{}'.format(e.message)) - except SecurityGroupNotFoundException: - AppScaleLogger.log('Could not find security group {}, skipping ' - 'delete.'.format(parameters[self.PARAM_GROUP])) - return - - - def get_optimal_spot_price(self, conn, instance_type, zone): - """ - Returns the spot price for an EC2 instance of the specified instance type. - The returned value is computed by averaging all the spot price history - values returned by the back-end EC2 APIs and incrementing the average by - extra 10%. - - Args: - conn: A boto.EC2Connection that can be used to communicate with AWS. - instance_type: A str representing the instance type whose prices we - should speculate for. - zone: A str representing the availability zone that the instance will - be placed in. - Returns: - The estimated spot price for the specified instance type, in the - specified availability zone. - """ - end_time = datetime.datetime.now() - start_time = end_time - datetime.timedelta(days=7) - history = conn.get_spot_price_history(start_time=start_time.isoformat(), - end_time=end_time.isoformat(), product_description='Linux/UNIX', - instance_type=instance_type, availability_zone=zone) - var_sum = 0.0 - for entry in history: - var_sum += entry.price - average = var_sum / len(history) - bid_price = average * 1.10 - AppScaleLogger.log('The average spot instance price for a {0} machine is'\ - ' {1}, and 10% more is {2}'.format(instance_type, average, bid_price)) - return bid_price - - def open_connection(self, parameters): - """ - Initialize a connection to the back-end EC2 APIs. - - Args: - parameters: A dictionary containing the 'credentials' parameter. - Returns: - An instance of Boto EC2Connection - """ - credentials = parameters[self.PARAM_CREDENTIALS] - return boto.ec2.connect_to_region(parameters[self.PARAM_REGION], - aws_access_key_id=credentials['EC2_ACCESS_KEY'], - aws_secret_access_key=credentials['EC2_SECRET_KEY']) - - def open_vpc_connection(self, parameters): - """ - Initialize a connection to the back-end VPC APIs. - - Args: - parameters: A dictionary containing the 'credentials' parameter. - Returns: - An instance of Boto VPCConnection - """ - credentials = parameters[self.PARAM_CREDENTIALS] - return boto.vpc.connect_to_region(parameters[self.PARAM_REGION], - aws_access_key_id=credentials['EC2_ACCESS_KEY'], - aws_secret_access_key=credentials['EC2_SECRET_KEY']) - - def handle_failure(self, msg): - """ Log the specified error message and raise an AgentRuntimeException - - Args: - msg: An error message to be logged and included in the raised exception. - Raises: - AgentRuntimeException Contains the input error message. - """ - AppScaleLogger.log(msg) - raise AgentRuntimeException(msg) - - def __describe_instances(self, parameters): - """ Query the back-end EC2 services for instance details and return - a list of instances. This is equivalent to running the standard - ec2-describe-instances command. The returned list of instances - will contain all the running and pending instances and it might - also contain some recently terminated instances. - - Args: - parameters: A dictionary of parameters. - Returns: - A list of instances (element type definition in boto.ec2 package). - """ - conn = self.open_connection(parameters) - reservations = conn.get_all_instances() - instances = [i for r in reservations for i in r.instances] - return instances - - def __get_instance_info(self, instances, status, keyname): - """ Filter out a list of instances by instance status and keyname. - - Args: - instances: A list of instances as returned by describe_instances. - status: Status of the VMs (eg: running, terminated). - keyname: Keyname used to spawn instances. - Returns: - A tuple of the form (public ips, private ips, instance ids). - """ - instance_ids = [] - public_ips = [] - private_ips = [] - for i in instances: - if i.state == status and i.key_name == keyname: - instance_ids.append(i.id) - public_ips.append(i.ip_address) - private_ips.append(i.private_ip_address) - return public_ips, private_ips, instance_ids diff --git a/appscale/tools/agents/euca_agent.py b/appscale/tools/agents/euca_agent.py deleted file mode 100644 index aa02da9e..00000000 --- a/appscale/tools/agents/euca_agent.py +++ /dev/null @@ -1,86 +0,0 @@ -import boto - -from appscale.tools.appscale_logger import AppScaleLogger -from ec2_agent import EC2Agent -from urlparse import urlparse - -class EucalyptusAgent(EC2Agent): - """ - Eucalyptus infrastructure agent which can be used to spawn and terminate - VMs in an Eucalyptus based environment. - """ - - # The version of Eucalyptus API used to interact with Euca clouds - EUCA_API_VERSION = '2016-11-15' - - - # A list of the credentials that we require users to provide the AppScale - # Tools with so that they can interact with Eucalyptus clouds. Right now - # it's the same as what's needed for EC2, with an extra argument indicating - # where the Eucalyptus deployment is located. - REQUIRED_EUCA_CREDENTIALS = list(EC2Agent.REQUIRED_EC2_CREDENTIALS) + \ - ['EC2_URL'] - - - # A list of credentials that we build our internal credential list from. - REQUIRED_CREDENTIALS = REQUIRED_EUCA_CREDENTIALS - - - def open_connection(self, parameters): - """ - Initialize a connection to the back-end Eucalyptus APIs. - - Args: - parameters A dictionary containing the 'credentials' parameter - - Returns: - An instance of Boto EC2Connection - """ - credentials = parameters[self.PARAM_CREDENTIALS] - access_key = str(credentials['EC2_ACCESS_KEY']) - secret_key = str(credentials['EC2_SECRET_KEY']) - ec2_url = str(credentials['EC2_URL']) - result = urlparse(ec2_url) - if result.port is not None: - port = result.port - elif result.scheme == 'http': - port = 80 - elif result.scheme == 'https': - port = 443 - else: - self.handle_failure('Unknown scheme in EC2_URL: ' + result.scheme) - return None - - if parameters.get(self.PARAM_VERBOSE, False): - debug_level = 2 # extremely verbose - else: - debug_level = 0 # the silent treatment - - return boto.connect_euca(host=result.hostname, - aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - port=port, - path=result.path, - is_secure=(result.scheme == 'https'), - api_version=self.EUCA_API_VERSION, debug=debug_level) - - def does_zone_exist(self, parameters): - """ - Queries Eucalyptus to see if the specified availability zone exists. - - Args: - parameters: A dict that contains the zone to check for existence. - Returns: - True if the availability zone exists, False otherwise. - """ - # Note that we can't use does_zone_exist in EC2Agent. There, if the image - # doesn't exist, it throws an EC2ResponseError, but in Eucalyptus, it - # doesn't (and returns None instead). - conn = self.open_connection(parameters) - zone = parameters[self.PARAM_ZONE] - if conn.get_all_zones(zone): - AppScaleLogger.log('Availability zone {0} does exist'.format(zone)) - return True - else: - AppScaleLogger.log('Availability zone {0} does not exist'.format(zone)) - return False diff --git a/appscale/tools/agents/factory.py b/appscale/tools/agents/factory.py deleted file mode 100644 index 70005319..00000000 --- a/appscale/tools/agents/factory.py +++ /dev/null @@ -1,58 +0,0 @@ -import struct - -from appscale.tools.custom_exceptions import UnknownInfrastructureException -try: - from azure_agent import AzureAgent -except (ImportError, struct.error): - AzureAgent = None -from ec2_agent import EC2Agent -from euca_agent import EucalyptusAgent -from gce_agent import GCEAgent -from openstack_agent import OpenStackAgent - - -__author__ = 'hiranya' -__email__ = 'hiranya@appscale.com' - - -class InfrastructureAgentFactory: - """ Factory implementation which can be used to instantiate infrastructure - agents. """ - - - # A set containing each of the cloud infrastructures that AppScale can - # deploy over. - VALID_AGENTS = ('ec2', 'euca', 'gce', 'openstack', 'azure') - - - # A dict that maps each VALID_AGENT above to the class that implements - # support for it in AppScale. - agents = { - 'ec2': EC2Agent, - 'euca': EucalyptusAgent, - 'gce': GCEAgent, - 'openstack': OpenStackAgent, - } - - if AzureAgent is not None: - agents['azure'] = AzureAgent - - @classmethod - def create_agent(cls, infrastructure): - """ - Instantiate a new infrastructure agent. - - Args: - infrastructure: A string indicating the type of infrastructure - agent to be initialized. - Returns: - An infrastructure agent instance that implements the BaseAgent API - Raises: - UnknownInfrastructureException: If the infrastructure given is not one - that we support. - """ - if cls.agents.has_key(infrastructure): - return cls.agents[infrastructure]() - else: - raise UnknownInfrastructureException('Unrecognized infrastructure: {0}' \ - .format(infrastructure)) diff --git a/appscale/tools/agents/gce_agent.py b/appscale/tools/agents/gce_agent.py deleted file mode 100644 index aef612ad..00000000 --- a/appscale/tools/agents/gce_agent.py +++ /dev/null @@ -1,1251 +0,0 @@ -#!/usr/bin/env python -""" -This file provides a single class, GCEAgent, that the AppScale Tools can use to -interact with Google Compute Engine. -""" - -# General-purpose Python library imports -import datetime -import json -import os.path -import pwd -import shutil -import time -import uuid - - -# Third-party imports -from apiclient import discovery -from apiclient import errors -# Don't bother us about the discovery.Resource not having certain -# methods, since it gets built dynamically. -# pylint: disable-msg=E1101 -import httplib2 -import oauth2client.client -import oauth2client.file -from oauth2client.service_account import ServiceAccountCredentials -import oauth2client.tools - - -# AppScale-specific imports -from appscale.tools.appscale_logger import AppScaleLogger -from appscale.tools.local_state import LocalState -from base_agent import AgentConfigurationException -from base_agent import AgentRuntimeException -from base_agent import BaseAgent - - -class CredentialJSONKeys(object): - """ A class containing valid JSON keys in credential files. """ - TYPE = 'type' - - -class GCPScopes(object): - """ A class containing scopes for Google's Cloud Platform. """ - COMPUTE = 'https://www.googleapis.com/auth/compute' - - -class CredentialTypes(object): - """ A class containing the supported credential types. """ - SERVICE = 'service_account' - OAUTH = 'oauth_client' - - -class GCEAgent(BaseAgent): - """ GCEAgent defines a specialized BaseAgent that allows for interaction with - Google Compute Engine. - - It authenticates via OAuth2 and interacts with GCE via the Google Client - Library. - """ - - - # The maximum amount of time, in seconds, that we are willing to wait for a - # virtual machine to start up, after calling instances().add(). GCE is pretty - # fast at starting up images, and in practice, we haven't seen it take longer - # than 200 seconds, but this upper bound is set just to be safe. - MAX_VM_CREATION_TIME = 600 - - - # The amount of time that run_instances waits between each instances().list() - # request. Setting this value lower results in more requests made to Google, - # but is more responsive to when machines become ready to use. - SLEEP_TIME = 20 - - - # The following constants are string literals that can be used by callers to - # index into the parameters the user passes in, as opposed to having to type - # out the strings each time we need them. These are specific to the GCE agent. - PARAM_PROJECT = 'project' - PARAM_SECRETS = 'client_secrets' - PARAM_STORAGE = 'oauth2_storage' - - - # A set that contains all of the items necessary to run AppScale in Google - # Compute Engine. - REQUIRED_CREDENTIALS = ( - BaseAgent.PARAM_GROUP, - BaseAgent.PARAM_IMAGE_ID, - BaseAgent.PARAM_KEYNAME, - PARAM_PROJECT, - BaseAgent.PARAM_ZONE - ) - - - # The OAuth 2.0 scope used to interact with Google Compute Engine. - GCE_SCOPE = 'https://www.googleapis.com/auth/compute' - - - # The version of the Google Compute Engine API that we support. - API_VERSION = 'v1' - - - # The URL endpoint that receives Google Compute Engine API requests. - GCE_URL = 'https://www.googleapis.com/compute/{0}/projects/'.format( - API_VERSION) - - - # The zone that instances should be created in and removed from. - DEFAULT_ZONE = 'us-central1-a' - - - # The region that instances should be created in and removed from. - DEFAULT_REGION = 'us-central1' - - - # The person to contact if there is a problem with the instance. We set this - # to 'default' to not have to actually put anyone's personal information in. - DEFAULT_SERVICE_EMAIL = 'default' - - - # A list of GCE instance types that have less than 4 GB of RAM, the amount - # recommended by Cassandra. AppScale will still run on these instance types, - # but is likely to crash after a day or two of use (as Cassandra will attempt - # to malloc ~800MB of memory, which will fail on these instance types). - DISALLOWED_INSTANCE_TYPES = ["n1-highcpu-2", "n1-highcpu-2-d", "f1-micro", - "g1-small"] - - # The credentials files location on the machine. - OAUTH2_STORAGE_LOCATION = '/etc/appscale/oauth2.dat' - CLIENT_SECRETS_LOCATION = '/etc/appscale/client_secrets.json' - - - def assert_credentials_are_valid(self, parameters): - """Contacts GCE to see if the given credentials are valid. - - Args: - parameters: A dict containing the credentials necessary to interact with - GCE. - - Raises: - AgentConfigurationException: If an error is encountered during - authentication. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().list(project=parameters - [self.PARAM_PROJECT], zone=parameters[self.PARAM_ZONE]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError as e: - error_message = json.loads(e.content)['error']['message'] - raise AgentConfigurationException(error_message) - - - def configure_instance_security(self, parameters): - """ Creates a GCE network and firewall with the specified name, and opens - the ports on that firewall as needed for AppScale. - - We expect both the network and the firewall to not exist before this point, - to avoid accidentally placing AppScale instances from different deployments - in the same network and firewall (thus enabling them to see each other's web - traffic). - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - network and firewall that we should create in GCE. - Returns: - True, if the named network and firewall was created successfully. - Raises: - AgentRuntimeException: If the named network or firewall already exist in - GCE. - """ - is_autoscale_agent = parameters.get(self.PARAM_AUTOSCALE_AGENT, False) - - # While creating instances during autoscaling, we do not need to create a - # new keypair or a network. We just make use of the existing one. - if is_autoscale_agent: - return - - AppScaleLogger.log("Verifying that SSH key exists locally") - keyname = parameters[self.PARAM_KEYNAME] - private_key = LocalState.LOCAL_APPSCALE_PATH + keyname - public_key = private_key + ".pub" - - if os.path.exists(private_key) or os.path.exists(public_key): - raise AgentRuntimeException("SSH key already found locally - please " + - "use a different keyname") - - LocalState.generate_rsa_key(keyname, parameters[self.PARAM_VERBOSE]) - - ssh_key_exists, all_ssh_keys = self.does_ssh_key_exist(parameters) - if not ssh_key_exists: - self.create_ssh_key(parameters, all_ssh_keys) - - if self.does_network_exist(parameters): - raise AgentRuntimeException("Network already exists - please use a " + \ - "different group name.") - - if self.does_firewall_exist(parameters): - raise AgentRuntimeException("Firewall already exists - please use a " + \ - "different group name.") - - network_url = self.create_network(parameters) - self.create_firewall(parameters, network_url) - - - def does_ssh_key_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified SSH key exists. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. We don't have an additional key for the name of - the SSH key, since we use the one in ~/.ssh. - Returns: - A tuple of two items. The first item is a bool that is True if - our public key's contents are in GCE, and False otherwise, while - the second item is the contents of all SSH keys stored in GCE. - """ - our_public_ssh_key = None - public_ssh_key_location = LocalState.LOCAL_APPSCALE_PATH + \ - parameters[self.PARAM_KEYNAME] + ".pub" - with open(public_ssh_key_location) as file_handle: - system_user = os.getenv('LOGNAME', default=pwd.getpwuid(os.getuid())[0]) - our_public_ssh_key = system_user + ":" + file_handle.read().rstrip() - - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.projects().get( - project=parameters[self.PARAM_PROJECT]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - - if not 'items' in response['commonInstanceMetadata']: - return False, "" - - metadata = response['commonInstanceMetadata']['items'] - if not metadata: - return False, "" - - all_ssh_keys = "" - for item in metadata: - if item['key'] != 'sshKeys': - continue - - # Now that we know there's one or more SSH keys, just make sure that - # ours is in this list. - all_ssh_keys = item['value'] - if our_public_ssh_key in all_ssh_keys: - return True, all_ssh_keys - - return False, all_ssh_keys - except errors.HttpError: - return False, "" - - - def does_network_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified network exists. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - network that we should query for existence in GCE. - Returns: - True if the named network exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.networks().get( - project=parameters[self.PARAM_PROJECT], - network=parameters[self.PARAM_GROUP]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError: - return False - - - def does_firewall_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified firewall exists. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - firewall that we should query for existence in GCE. - Returns: - True if the named firewall exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.firewalls().get( - project=parameters[self.PARAM_PROJECT], - firewall=parameters[self.PARAM_GROUP]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError: - return False - - - def create_ssh_key(self, parameters, all_ssh_keys): - """ Creates a new SSH key in Google Compute Engine with the contents of - our newly generated public key. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - all_ssh_keys: A str that contains all of the SSH keys that are - currently passed in to GCE instances. - """ - public_ssh_key_location = LocalState.LOCAL_APPSCALE_PATH + \ - parameters[self.PARAM_KEYNAME] + ".pub" - with open(public_ssh_key_location) as file_handle: - system_user = os.getenv('LOGNAME', default=pwd.getpwuid(os.getuid())[0]) - public_ssh_key = file_handle.read().rstrip() - our_public_ssh_keys = "{system_user}:{key}\nroot:{key}".format( - system_user=system_user, key=public_ssh_key) - - if all_ssh_keys: - new_all_ssh_keys = our_public_ssh_keys + "\n" + all_ssh_keys - else: - new_all_ssh_keys = our_public_ssh_keys - - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.projects().setCommonInstanceMetadata( - project=parameters[self.PARAM_PROJECT], - body={ - "kind": "compute#metadata", - "items": [{ - "key": "sshKeys", - "value": new_all_ssh_keys - }] - } - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - - def create_network(self, parameters): - """ Creates a new network in Google Compute Engine with the specified name. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - network that we should create in GCE. - Returns: - The URL corresponding to the name of the network that was created, for use - with binding this network to one or more firewalls. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.networks().insert( - project=parameters[self.PARAM_PROJECT], - body={ - "name" : parameters[self.PARAM_GROUP], - "description" : "Network used for AppScale instances", - "IPv4Range" : "10.240.0.0/16" - } - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - return response['targetLink'] - - - def delete_network(self, parameters): - """ Deletes the network in Google Compute Engine with the specified name. - - Note that callers should not invoke this method unless they are confident - that no firewalls or instances are using this network, or this method will - fail. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - network that we should delete. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.networks().delete( - project=parameters[self.PARAM_PROJECT], - network=parameters[self.PARAM_GROUP] - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - def create_firewall(self, parameters, network_url): - """ Creates a new firewall in Google Compute Engine with the specified name, - bound to the specified network. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - firewall that we should create. - network_url: A str containing the URL of the network that this new - firewall should be applied to. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.firewalls().insert( - project=parameters[self.PARAM_PROJECT], - body={ - "name" : parameters[self.PARAM_GROUP], - "description" : "Firewall used for AppScale instances", - "network" : network_url, - "sourceRanges" : ["0.0.0.0/0"], - "allowed" : [ - {"IPProtocol" : "tcp", "ports": ["1-65535"]}, - {"IPProtocol" : "udp", "ports": ["1-65535"]}, - {"IPProtocol" : "icmp"} - ] - } - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - - def delete_firewall(self, parameters): - """ Deletes a firewall in Google Compute Engine with the specified name. - - Callers should not invoke this method until they are certain that no - instances are using the specified firewall, or this method will fail. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - firewall that we should create. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.firewalls().delete( - project=parameters[self.PARAM_PROJECT], - firewall=parameters[self.PARAM_GROUP] - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - - def get_params_from_args(self, args): - """ Constructs a dict with only the parameters necessary to interact with - Google Compute Engine (here, the client_secrets file and the image name). - - Args: - args: A Namespace or dict that maps all of the arguments the user has - invoked an AppScale command with their associated value. - Returns: - A dict containing the location of the client_secrets file and that name - of the image to use in GCE. - Raises: - AgentConfigurationException: If the caller fails to specify a - client_secrets file, or if it doesn't exist on the local filesystem. - """ - if not isinstance(args, dict): - args = vars(args) - - if not args.get('client_secrets') and not args.get('oauth2_storage'): - raise AgentConfigurationException("Please specify a client_secrets " + \ - "file or a oauth2_storage file in your AppScalefile when running " + \ - "over Google Compute Engine.") - - credentials_file = args.get('client_secrets') or args.get('oauth2_storage') - full_credentials = os.path.expanduser(credentials_file) - if not os.path.exists(full_credentials): - raise AgentConfigurationException("Couldn't find your credentials " + \ - "at {0}".format(full_credentials)) - - if args.get('client_secrets'): - destination = LocalState.get_client_secrets_location(args['keyname']) - - # Make sure the destination's parent directory exists. - destination_par = os.path.abspath(os.path.join(destination, os.pardir)) - if not os.path.exists(destination_par): - os.makedirs(destination_par) - - shutil.copy(full_credentials, destination) - elif args.get('oauth2_storage'): - destination = LocalState.get_oauth2_storage_location(args['keyname']) - - # Make sure the destination's parent directory exists. - destination_par = os.path.abspath(os.path.join(destination, os.pardir)) - if not os.path.exists(destination_par): - os.makedirs(destination_par) - - shutil.copy(full_credentials, destination) - - params = { - self.PARAM_GROUP : args['group'], - self.PARAM_IMAGE_ID : args['machine'], - self.PARAM_INSTANCE_TYPE : args[self.PARAM_INSTANCE_TYPE], - self.PARAM_KEYNAME : args['keyname'], - self.PARAM_PROJECT : args['project'], - self.PARAM_STATIC_IP : args.get(self.PARAM_STATIC_IP), - self.PARAM_ZONE : args['zone'], - self.PARAM_TEST: args['test'], - self.PARAM_VERBOSE: args.get('verbose', False), - self.PARAM_AUTOSCALE_AGENT: False - } - - # A zone in GCE looks like 'us-central2-a', which is in the region - # 'us-central2'. Therefore, strip off the last two characters from the zone - # to get the region name. - if params[self.PARAM_ZONE]: - params[self.PARAM_REGION] = params[self.PARAM_ZONE][:-2] - else: - params[self.PARAM_REGION] = self.DEFAULT_REGION - - if args.get(self.PARAM_SECRETS): - params[self.PARAM_SECRETS] = os.path.expanduser( - args.get(self.PARAM_SECRETS)) - elif args.get(self.PARAM_STORAGE): - params[self.PARAM_STORAGE] = os.path.expanduser( - args.get(self.PARAM_STORAGE)) - - params[self.PARAM_VERBOSE] = args.get('verbose', False) - self.assert_credentials_are_valid(params) - - return params - - - def get_cloud_params(self, keyname): - """ Searches through the locations.json file with key - 'infrastructure_info' to build a dict containing the - parameters necessary to interact with Google Compute Engine. - - Args: - keyname: A str that uniquely identifies this AppScale deployment. - Returns: - A dict containing all of the credentials necessary to interact with - Google Compute Engine. - """ - params = { - self.PARAM_GROUP : LocalState.get_group(keyname), - self.PARAM_KEYNAME : keyname, - self.PARAM_PROJECT : LocalState.get_project(keyname), - self.PARAM_VERBOSE : False, # TODO(cgb): Don't put False in here. - self.PARAM_ZONE : LocalState.get_zone(keyname) - } - - if os.path.exists(LocalState.get_client_secrets_location(keyname)): - params[self.PARAM_SECRETS] = \ - LocalState.get_client_secrets_location(keyname) - else: - params[self.PARAM_STORAGE] = \ - LocalState.get_oauth2_storage_location(keyname) - - return params - - - def assert_required_parameters(self, parameters, operation): - """ Checks the given parameters to make sure that they can be used to - interact with Google Compute Engine. - - Args: - parameters: A dict that maps the name of each credential to be used in GCE - with the value we should use. - operation: A BaseAgent.OPERATION that indicates if we wish to add or - delete instances. Unused here, as all operations require the same - credentials. - Raises: - AgentConfigurationException: If any of the required credentials are not - present, or if the client_secrets parameter refers to a file that is not - present on the local filesystem. - """ - is_autoscale_agent = parameters.get(self.PARAM_AUTOSCALE_AGENT, False) - - # Make sure the user has set each parameter. - for param in self.REQUIRED_CREDENTIALS: - if not self.has_parameter(param, parameters): - raise AgentConfigurationException('The required parameter, {0}, was' \ - ' not specified.'.format(param)) - - # Determine if credentials file exists - if is_autoscale_agent: - # AppScale VM holds creds files in /etc/appscale/ directory - has_oauth2_storage = os.path.exists(self.OAUTH2_STORAGE_LOCATION) - has_client_secrets = os.path.exists(self.CLIENT_SECRETS_LOCATION) - if not has_oauth2_storage and not has_client_secrets: - raise AgentConfigurationException( - 'Could not find neither OAuth2 file ' - 'at {} nor client secrets file at {}' - .format(self.OAUTH2_STORAGE_LOCATION, self.CLIENT_SECRETS_LOCATION) - ) - else: - # AppScale tools has creds file in ~/.appscale/ directory - creds_location = ( - parameters.get(self.PARAM_SECRETS) - or parameters.get(self.PARAM_STORAGE) - ) - if not os.path.exists(creds_location): - raise AgentConfigurationException( - 'Could not find credentials file at {}'.format(creds_location)) - - - def describe_instances(self, parameters, pending=False): - """ Queries Google Compute Engine to see which instances are currently - running, and retrieve information about their public and private IPs. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - pending: Boolean if we should show pending instances. - Returns: - A tuple of the form (public_ips, private_ips, instance_ids), where each - member is a list. Items correspond to each other across these lists, - so a caller is guaranteed that item X in each list belongs to the same - virtual machine. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().list( - project=parameters[self.PARAM_PROJECT], - filter="name eq {group}-.*".format(group=parameters[self.PARAM_GROUP]), - zone=parameters[self.PARAM_ZONE] - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - - instance_ids = [] - public_ips = [] - private_ips = [] - - if response and 'items' in response: - instances = response['items'] - for instance in instances: - if instance['status'] == "RUNNING": - instance_ids.append(instance['name']) - network_interface = instance['networkInterfaces'][0] - public_ips.append(network_interface['accessConfigs'][0]['natIP']) - private_ips.append(network_interface['networkIP']) - - return public_ips, private_ips, instance_ids - - def generate_disk_name(self, parameters): - """ Creates a temporary name for a disk. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - Returns: - A str, a disk name associated with the root disk of AppScale on GCE. - """ - return '{group}-{uuid}'.format(group=parameters[self.PARAM_GROUP], - uuid=uuid.uuid4().hex)[:60] - - def create_scratch_disk(self, parameters): - """ Creates a disk from a given machine image. - - GCE does not support scratch disks on API version v1 and higher. We create - a persistent disk upon creation to act like one to keep the abstraction used - in other infrastructures. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - Returns: - A str, the url to the disk to use. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - disk_name = self.generate_disk_name(parameters) - project_url = '{0}{1}'.format(self.GCE_URL, - parameters[self.PARAM_PROJECT]) - source_image_url = '{0}{1}/global/images/{2}'.format(self.GCE_URL, - parameters[self.PARAM_PROJECT], parameters[self.PARAM_IMAGE_ID]) - request = gce_service.disks().insert( - project=parameters[self.PARAM_PROJECT], - zone=parameters[self.PARAM_ZONE], - body={ - 'name':disk_name - }, - sourceImage=source_image_url - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - disk_url = "{0}/zones/{1}/disks/{2}".format( - project_url, parameters[self.PARAM_ZONE], disk_name) - return disk_url - - def run_instances(self, count, parameters, security_configured, public_ip_needed): - """ Starts 'count' instances in Google Compute Engine, and returns once they - have been started. - - Callers should create a network and attach a firewall to it before using - this method, or the newly created instances will not have a network and - firewall to attach to (and thus this method will fail). - - Args: - count: An int that specifies how many virtual machines should be started. - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - security_configured: Unused, as we assume that the network and firewall - has already been set up. - """ - project_id = parameters[self.PARAM_PROJECT] - image_id = parameters[self.PARAM_IMAGE_ID] - instance_type = parameters[self.PARAM_INSTANCE_TYPE] - keyname = parameters[self.PARAM_KEYNAME] - group = parameters[self.PARAM_GROUP] - zone = parameters[self.PARAM_ZONE] - - AppScaleLogger.log("Starting {0} machines with machine id {1}, with " \ - "instance type {2}, keyname {3}, in security group {4}, in zone {5}" \ - .format(count, image_id, instance_type, keyname, group, zone)) - - # First, see how many instances are running and what their info is. - start_time = datetime.datetime.now() - active_public_ips, active_private_ips, active_instances = \ - self.describe_instances(parameters) - - # Construct URLs - image_url = '{0}{1}/global/images/{2}'.format(self.GCE_URL, project_id, - image_id) - project_url = '{0}{1}'.format(self.GCE_URL, project_id) - machine_type_url = '{0}/zones/{1}/machineTypes/{2}'.format(project_url, - zone, instance_type) - network_url = '{0}/global/networks/{1}'.format(project_url, group) - - # Construct the request body - for index in range(count): - disk_url = self.create_scratch_disk(parameters) - instances = { - # Truncate the name down to the first 62 characters, since GCE doesn't - # let us use arbitrarily long instance names. - 'name': '{group}-{uuid}'.format(group=group, uuid=uuid.uuid4())[:62], - 'machineType': machine_type_url, - 'disks':[{ - 'source': disk_url, - 'boot': 'true', - 'type': 'PERSISTENT' - }], - 'image': image_url, - 'networkInterfaces': [{ - 'accessConfigs': [{ - 'type': 'ONE_TO_ONE_NAT', - 'name': 'External NAT' - }], - 'network': network_url - }], - 'serviceAccounts': [{ - 'email': self.DEFAULT_SERVICE_EMAIL, - 'scopes': [self.GCE_SCOPE] - }] - } - - # Create the instance - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().insert( - project=project_id, body=instances, zone=zone) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - instance_ids = [] - public_ips = [] - private_ips = [] - end_time = datetime.datetime.now() + datetime.timedelta(0, - self.MAX_VM_CREATION_TIME) - now = datetime.datetime.now() - - while now < end_time: - AppScaleLogger.log("Waiting for your instances to start...") - instance_info = self.describe_instances(parameters) - public_ips = instance_info[0] - private_ips = instance_info[1] - instance_ids = instance_info[2] - public_ips = self.diff(public_ips, active_public_ips) - private_ips = self.diff(private_ips, active_private_ips) - instance_ids = self.diff(instance_ids, active_instances) - if count == len(public_ips): - break - time.sleep(self.SLEEP_TIME) - now = datetime.datetime.now() - - if not public_ips: - self.handle_failure('No public IPs were able to be procured ' - 'within the time limit') - - if len(public_ips) != count: - for index in range(0, len(public_ips)): - if public_ips[index] == '0.0.0.0': - instance_to_term = instance_ids[index] - AppScaleLogger.log('Instance {0} failed to get a public IP address'\ - 'and is being terminated'.format(instance_to_term)) - self.terminate_instances([instance_to_term]) - - end_time = datetime.datetime.now() - total_time = end_time - start_time - AppScaleLogger.log("Started {0} on-demand instances in {1} seconds" \ - .format(count, total_time.seconds)) - return instance_ids, public_ips, private_ips - - - def associate_static_ip(self, parameters, instance_id, static_ip): - """ Associates the given static IP address with the given instance ID. - - In Google Compute Engine, this is done by removing the route from the - outside world to the instance's public IP address, then adding a new route - from the outside world to the static IP address the caller has provided. - - Args: - parameters: A dict that includes the credentials necessary to communicate - with Google Compute Engine. - instance_id: A str naming the running instance to associate a static IP - with. - static_ip: A str naming the already allocated static IP address that will - be associated. - """ - self.delete_access_config(parameters, instance_id) - self.add_access_config(parameters, instance_id, static_ip) - - - def delete_access_config(self, parameters, instance_id): - """ Instructs Google Compute Engine to remove the public IP address from - the named instance. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key mapping to a list of - instance names that should be deleted. - instance_id: A str naming the running instance that the new public IP - address should be added to. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().deleteAccessConfig( - project=parameters[self.PARAM_PROJECT], - accessConfig="External NAT", - instance=instance_id, - networkInterface="nic0", - zone=parameters[self.PARAM_ZONE] - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - - - def add_access_config(self, parameters, instance_id, static_ip): - """ Instructs Google Compute Engine to use the given IP address as the - public IP for the named instance. - - This assumes that there is no existing public IP address for the named - instance. If this is not the case, callers should use delete_access_config - first to remove it. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key mapping to a list of - instance names that should be deleted. - instance_id: A str naming the running instance that the new public IP - address should be added to. - static_ip: A str naming the already allocated static IP address that - will be used for the named instance. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().addAccessConfig( - project=parameters[self.PARAM_PROJECT], - instance=instance_id, - networkInterface="nic0", - zone=parameters[self.PARAM_ZONE], - body={ - "kind": "compute#accessConfig", - "type" : "ONE_TO_ONE_NAT", - "name" : "External NAT", - "natIP" : static_ip - } - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - - - def terminate_instances(self, parameters): - """ Deletes the instances specified in 'parameters' running in Google - Compute Engine. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key mapping to a list of - instance names that should be deleted. - """ - instance_ids = parameters[self.PARAM_INSTANCE_IDS] - responses = [] - for instance_id in instance_ids: - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.instances().delete( - project=parameters[self.PARAM_PROJECT], - zone=parameters[self.PARAM_ZONE], - instance=instance_id - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - responses.append(response) - - for response in responses: - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - - def does_address_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified static IP address - exists for this user. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - static IP address that we should check for existence. - Returns: - True if the named address exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.addresses().list( - project=parameters[self.PARAM_PROJECT], - filter="address eq {0}".format(parameters[self.PARAM_STATIC_IP]), - region=parameters[self.PARAM_REGION] - ) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - - if 'items' in response: - return True - else: - return False - - - def does_image_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified image exists for - this user. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - image that we should check for existence. - Returns: - True if the named image exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.images().get(project=parameters[self.PARAM_PROJECT], - image=parameters[self.PARAM_IMAGE_ID]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError: - return False - - - def does_zone_exist(self, parameters): - """ Queries Google Compute Engine to see if the specified zone exists for - this user. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine, and an additional key indicating the name of the - zone that we should check for existence. - Returns: - True if the named zone exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.zones().get(project=parameters[self.PARAM_PROJECT], - zone=parameters[self.PARAM_ZONE]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError: - return False - - - def does_disk_exist(self, parameters, disk): - """ Queries Google Compute Engine to see if the specified persistent disk - exists for this user. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - disk: A str containing the name of the disk that we should check for - existence. - Returns: - True if the named persistent disk exists, and False otherwise. - """ - gce_service, credentials = self.open_connection(parameters) - try: - http = httplib2.Http() - auth_http = credentials.authorize(http) - request = gce_service.disks().get(project=parameters[self.PARAM_PROJECT], - disk=disk, zone=parameters[self.PARAM_ZONE]) - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - return True - except errors.HttpError: - return False - - - def detach_disk(self, parameters, disk_name, instance_id): - """ Detaches the persistent disk specified in 'disk_name' from the named - instance. - - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - disk_name: A str naming the persistent disk to detach. - instance_id: A str naming the id of the instance that the disk should be - detached from. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - project_id = parameters[self.PARAM_PROJECT] - request = gce_service.instances().detachDisk( - project=project_id, - zone=parameters[self.PARAM_ZONE], - instance=instance_id, - deviceName='sdb') - response = request.execute(http=auth_http) - AppScaleLogger.verbose(str(response), parameters[self.PARAM_VERBOSE]) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - - def cleanup_state(self, parameters): - """ Deletes the firewall and network that were created during this AppScale - deployment. - - Args: - parameters: A dict that contains the name of the firewall and network to - delete (the group name) as well as the credentials necessary to do so. - """ - self.delete_firewall(parameters) - self.delete_network(parameters) - - @staticmethod - def get_secrets_type(secrets_location): - """ Determines whether the secrets file is for a service account or OAuth. - - Args: - secrets_location: A string that contains the location of the JSON - credentials file downloaded from GCP. - Returns: - A string containing the type of credentials to use. - """ - with open(secrets_location) as secrets_file: - secrets_json = secrets_file.read() - secrets = json.loads(secrets_json) - if (CredentialJSONKeys.TYPE in secrets and - secrets[CredentialJSONKeys.TYPE] == CredentialTypes.SERVICE): - return CredentialTypes.SERVICE - else: - return CredentialTypes.OAUTH - - - def open_connection(self, parameters): - """ Connects to Google Compute Engine with the given credentials. - - Args: - parameters: A dict that contains all the parameters necessary to - authenticate this user with Google Compute Engine. We assume that the - user has already authorized this account for use with GCE. - Returns: - An apiclient.discovery.Resource that is a connection valid for requests - to Google Compute Engine for the given user, and a Credentials object that - can be used to sign requests performed with that connection. - Raises: - AppScaleException if the user wants to abort. - """ - is_autoscale_agent = parameters.get(self.PARAM_AUTOSCALE_AGENT, False) - - # Determine paths to credential files - if is_autoscale_agent: - client_secrets_path = self.CLIENT_SECRETS_LOCATION - oauth2_storage_path = self.OAUTH2_STORAGE_LOCATION - else: - # Determine client secrets path - client_secrets_path = LocalState.get_client_secrets_location( - parameters[self.PARAM_KEYNAME]) - if not os.path.exists(client_secrets_path): - client_secrets_path = parameters.get(self.PARAM_SECRETS, '') - # Determine oauth2 storage - oauth2_storage_path = parameters.get(self.PARAM_STORAGE) - if not oauth2_storage_path or not os.path.exists(oauth2_storage_path): - oauth2_storage_path = LocalState.get_oauth2_storage_location( - parameters[self.PARAM_KEYNAME]) - - if os.path.exists(client_secrets_path): - # Attempt to perform authorization using Service account - secrets_type = GCEAgent.get_secrets_type(client_secrets_path) - if secrets_type == CredentialTypes.SERVICE: - scopes = [GCPScopes.COMPUTE] - credentials = ServiceAccountCredentials.from_json_keyfile_name( - client_secrets_path, scopes=scopes) - return discovery.build('compute', self.API_VERSION), credentials - - # Perform authorization using OAuth2 storage - storage = oauth2client.file.Storage(oauth2_storage_path) - credentials = storage.get() - - if not credentials or credentials.invalid: - # If we couldn't get valid credentials from OAuth2 storage - if not os.path.exists(client_secrets_path): - raise AgentConfigurationException( - 'Couldn\'t find client secrets file at {}'.format(client_secrets_path) - ) - # Run OAuth2 flow to get credentials - flow = oauth2client.client.flow_from_clientsecrets( - client_secrets_path, scope=self.GCE_SCOPE) - flags = oauth2client.tools.argparser.parse_args(args=[]) - credentials = oauth2client.tools.run_flow(flow, storage, flags) - - # Build the service - return discovery.build('compute', self.API_VERSION), credentials - - def attach_disk(self, parameters, disk_name, instance_id): - """ Attaches the persistent disk specified in 'disk_name' to this virtual - machine. - Args: - parameters: A dict with keys for each parameter needed to connect to - Google Compute Engine. - disk_name: A str naming the persistent disk to attach to this machine. - instance_id: A str naming the id of the instance that the disk should be - attached to. In practice, callers add disks to their own instance. - Returns: - A str indicating where the persistent disk has been attached to. - """ - gce_service, credentials = self.open_connection(parameters) - http = httplib2.Http() - auth_http = credentials.authorize(http) - project = parameters[self.PARAM_PROJECT] - zone = parameters[self.PARAM_ZONE] - - # If the disk is already attached, return the mount point. - request = gce_service.instances().get(project=project, zone=zone, - instance=instance_id) - disks = request.execute(auth_http)['disks'] - for disk in disks: - path = disk['source'].split('/') - if project == path[-5] and zone == path[-3] and disk_name == path[-1]: - device_name = '/dev/{}'.format(disk['deviceName']) - AppScaleLogger.log('Disk is already attached at {}'.format(device_name)) - return device_name - - request = gce_service.instances().attachDisk( - project=project, - zone=zone, - instance=instance_id, - body={ - 'kind': 'compute#attachedDisk', - 'type': 'PERSISTENT', - 'mode': 'READ_WRITE', - 'source': "https://www.googleapis.com/compute/{0}/projects/{1}" \ - "/zones/{2}/disks/{3}".format(self.API_VERSION, project, - zone, disk_name), - 'deviceName': 'sdb' - } - ) - response = request.execute(auth_http) - AppScaleLogger.log(str(response)) - self.ensure_operation_succeeds(gce_service, auth_http, response, - parameters[self.PARAM_PROJECT]) - - return '/dev/sdb' - - - def ensure_operation_succeeds(self, gce_service, auth_http, response, - project_id): - """ Waits for the given GCE operation to finish successfully. - - Callers should use this function whenever they perform a destructive - operation in Google Compute Engine. For example, it is not necessary to use - this function when seeing if a resource exists (e.g., a network, firewall, - or instance), but it is useful to use this method when creating or deleting - a resource. One example is when we create a network. As we are only allowed - to have five networks, it is useful to make sure that the network was - successfully created before trying to create a firewall attached to that - network. - - Args: - gce_service: An apiclient.discovery.Resource that is a connection valid - for requests to Google Compute Engine for the given user. - auth_http: A HTTP connection that has been signed with the given user's - Credentials, and is authorized with the GCE scope. - response: A dict that contains the operation that we want to ensure has - succeeded, referenced by a unique ID (the 'name' field). - project_id: A str that identifies the GCE project that requests should - be billed to. - """ - status = response['status'] - while status != 'DONE' and response: - operation_id = response['name'] - - # Identify if this is a per-zone resource - if 'zone' in response: - zone_name = response['zone'].split('/')[-1] - request = gce_service.zoneOperations().get( - project=project_id, - operation=operation_id, - zone=zone_name) - else: - request = gce_service.globalOperations().get( - project=project_id, operation=operation_id) - - response = request.execute(http=auth_http) - if response: - status = response['status'] - - if 'error' in response: - message = "\n".join([errors['message'] for errors in - response['error']['errors']]) - raise AgentRuntimeException(str(message)) diff --git a/appscale/tools/agents/openstack_agent.py b/appscale/tools/agents/openstack_agent.py deleted file mode 100644 index 155c54d6..00000000 --- a/appscale/tools/agents/openstack_agent.py +++ /dev/null @@ -1,133 +0,0 @@ -""" The Openstack Agent. """ -import boto -import time - -from ec2_agent import EC2Agent -from urlparse import urlparse - -__author__ = 'dario nascimento' -__email__ = 'dario.nascimento@tecnico.ulisboa.pt' - -class OpenStackAgent(EC2Agent): - """ - OpenStack infrastructure agent which can be used to spawn and terminate - VMs in an OpenStack based environment. - """ - - # The version of OpenStack API used to interact with Boto - # OpenStack_API_VERSION = 'ICE-HOUSE-2014.1' - - # A list of the credentials that we require users to provide the AppScale - # Tools with so that they can interact with Openstack clouds. Right now - # it's the same as what's needed for EC2, with an extra argument indicating - # where the Openstack deployment is located. - REQUIRED_OPENSTACK_CREDENTIALS = list(EC2Agent.REQUIRED_EC2_CREDENTIALS) + \ - ['EC2_URL'] - - # A list of credentials that we build our internal credential list from. - REQUIRED_CREDENTIALS = REQUIRED_OPENSTACK_CREDENTIALS - - # The default openstack region. - DEFAULT_REGION = "nova" - - def describe_instances(self, parameters, pending=False): - """ - Retrieves the list of running instances that have been instantiated using a - particular EC2 keyname. The target keyname is read from the input parameter - map. (Also see documentation for the BaseAgent class). - - Args: - parameters: A dictionary containing the 'keyname' parameter. - pending: Indicates we also want the pending instances. - Returns: - A tuple of the form (public_ips, private_ips, instances) where each - member is a list. - """ - instance_ids = [] - public_ips = [] - private_ips = [] - - conn = self.open_connection(parameters) - reservations = conn.get_all_instances() - instances = [i for r in reservations for i in r.instances] - for i in instances: - if (i.state == 'running' or (pending and i.state == 'pending')) \ - and i.key_name.startswith(parameters[self.PARAM_KEYNAME]): - instance_ids.append(i.id) - public_ips.append(i.public_dns_name) - private_ips.append(i.private_dns_name) - return public_ips, private_ips, instance_ids - - - def open_connection(self, parameters): - """ - Initialize a connection to the back-end OpenStack APIs. - - Args: - parameters: A dictionary containing the 'credentials' parameter - EC2_URL usually has the format: - http://:8773/services/Cloud - - Returns: - An instance of Boto EC2Connection - """ - credentials = parameters[self.PARAM_CREDENTIALS] - region_str = self.DEFAULT_REGION - access_key = str(credentials['EC2_ACCESS_KEY']) - secret_key = str(credentials['EC2_SECRET_KEY']) - ec2_url = str(credentials['EC2_URL']) - - result = urlparse(ec2_url) - - if result.port is None or result.hostname is None\ - or result.path is None: - self.handle_failure('Unknown scheme in Openstack_URL: {0}'+\ - ' : expected like http://:8773/services/Cloud'\ - .format(result.geturl())) - return None - - region = boto.ec2.regioninfo.RegionInfo(name=region_str, - endpoint=result.hostname) - return boto.connect_ec2(aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - is_secure=(result.scheme == 'https'), - region=region, - port=result.port, - path=result.path, debug=2) - - def wait_for_status_change(self, parameters, conn, state_requested, - max_wait_time=60, poll_interval=10): - """ - After we have sent a signal to the cloud infrastructure to change the state - of the instances (unsually from runnning to either stoppped or - terminated), wait for the status to change. - - Args: - parameters: A dictionary of parameters. - conn: A connection object returned from self.open_connection(). - state_requrested: String of the requested final state of the instances. - max_wait_time: int of maximum amount of time (in seconds) to wait for the - state change. - poll_interval: int of the number of seconds to wait between checking of - the state. - - Returns: - If all the instances change successfully, return True, if not return False. - """ - time_start = time.time() - instance_ids = parameters[self.PARAM_INSTANCE_IDS] - instances_in_state = {} - while True: - time.sleep(poll_interval) - reservations = conn.get_all_instances(instance_ids) - instances = [i for r in reservations for i in r.instances] - for i in instances: - if i.state == state_requested and \ - i.key_name.startswith(parameters[self.PARAM_KEYNAME]): - if i.id not in instances_in_state.keys(): - instances_in_state[i.id] = 1 # mark instance done - if len(instances_in_state.keys()) >= len(instance_ids): - return True - if time.time() - time_start > max_wait_time: - return False - diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index d24a0c1b..4f302c3f 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -27,7 +27,6 @@ from appscale.tools.admin_api.client import (AdminClient, DEFAULT_SERVICE, DEFAULT_VERSION) from appscale.tools.admin_api.version import Version -from appscale.agents.factory import InfrastructureAgentFactory from appscale.tools.appcontroller_client import AppControllerClient from appscale.tools.appengine_helper import AppEngineHelper from appscale.tools.appscale_logger import AppScaleLogger @@ -40,6 +39,8 @@ from appscale.tools.remote_helper import RemoteHelper from appscale.tools.version_helper import latest_tools_version +from appscale.agents.factory import InfrastructureAgentFactory + def async_layout_upgrade(ip, keyname, script, error_bucket, verbose=False): """ Run a command over SSH and place exceptions in a bucket. diff --git a/appscale/tools/node_layout.py b/appscale/tools/node_layout.py index 63d72944..d0a824b3 100644 --- a/appscale/tools/node_layout.py +++ b/appscale/tools/node_layout.py @@ -7,7 +7,7 @@ # AppScale-specific imports -from agents.factory import InfrastructureAgentFactory +from appscale.agents.factory import InfrastructureAgentFactory from appscale_logger import AppScaleLogger from custom_exceptions import BadConfigurationException from local_state import LocalState diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index ed8c64de..d7c92e89 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -14,13 +14,13 @@ # AppScale-specific imports try: - from agents.azure_agent import AzureAgent + from appscale.agents.azure_agent import AzureAgent except ImportError: AzureAgent = None -from agents.base_agent import BaseAgent -from agents.ec2_agent import EC2Agent -from agents.gce_agent import GCEAgent -from agents.factory import InfrastructureAgentFactory +from appscale.agents.base_agent import BaseAgent +from appscale.agents.ec2_agent import EC2Agent +from appscale.agents.gce_agent import GCEAgent +from appscale.agents.factory import InfrastructureAgentFactory from custom_exceptions import BadConfigurationException from local_state import APPSCALE_VERSION from local_state import LocalState diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index 9560310c..2c0618a5 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -16,7 +16,6 @@ from boto.exception import BotoServerError # AppScale-specific imports -from agents.factory import InfrastructureAgentFactory from appcontroller_client import AppControllerClient from appscale_logger import AppScaleLogger from custom_exceptions import AppControllerException @@ -24,9 +23,10 @@ from custom_exceptions import BadConfigurationException from custom_exceptions import ShellException from custom_exceptions import TimeoutException -from agents.base_agent import AgentRuntimeException -from agents.gce_agent import CredentialTypes -from agents.gce_agent import GCEAgent +from appscale.agents.base_agent import AgentRuntimeException +from appscale.agents.gce_agent import CredentialTypes +from appscale.agents.gce_agent import GCEAgent +from appscale.agents.factory import InfrastructureAgentFactory from local_state import APPSCALE_VERSION from local_state import LocalState diff --git a/setup.py b/setup.py index b0ad635a..1dde127b 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ platforms='Posix; MacOS X', install_requires=[ 'adal>=0.4.7', - 'appscale.agents', + 'appscale-agents', 'azure==2.0.0', 'azure-mgmt-marketplaceordering', 'cryptography>=2.3.0', From b0f7df66595d8b0dc15df74eee886797776bf055 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Tue, 13 Nov 2018 17:47:15 -0600 Subject: [PATCH 15/63] fix import loop issue --- appscale/tools/node_layout.py | 9 +++++---- appscale/tools/parse_args.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/appscale/tools/node_layout.py b/appscale/tools/node_layout.py index d0a824b3..bc5465c4 100644 --- a/appscale/tools/node_layout.py +++ b/appscale/tools/node_layout.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +from __future__ import absolute_import # General-purpose Python library imports import re @@ -8,10 +9,10 @@ # AppScale-specific imports from appscale.agents.factory import InfrastructureAgentFactory -from appscale_logger import AppScaleLogger -from custom_exceptions import BadConfigurationException -from local_state import LocalState -from parse_args import ParseArgs +from .appscale_logger import AppScaleLogger +from .custom_exceptions import BadConfigurationException +from .local_state import LocalState +from .parse_args import ParseArgs class NodeLayout(): diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index d7c92e89..b26d6140 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +from __future__ import absolute_import # General-purpose Python library imports import argparse @@ -21,9 +22,9 @@ from appscale.agents.ec2_agent import EC2Agent from appscale.agents.gce_agent import GCEAgent from appscale.agents.factory import InfrastructureAgentFactory -from custom_exceptions import BadConfigurationException -from local_state import APPSCALE_VERSION -from local_state import LocalState +from .custom_exceptions import BadConfigurationException +from .local_state import APPSCALE_VERSION +from .local_state import LocalState class ParseArgs(object): From b78cb068da4838380e2fb62d11202b3699e42d3d Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Tue, 13 Nov 2018 17:58:28 -0600 Subject: [PATCH 16/63] fix additional import issues, tests are now passing --- Makefile | 1 + appscale/tools/remote_helper.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index f56fc6d7..cf97ff20 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ help: ##@ Testing test: checkenv-VIRTUAL_ENV ## Run Python unit tests @pip install flexmock + @pip install mock @python -m unittest discover -b -v -s test ##@ Installation diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index 2c0618a5..c5be1c20 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +from __future__ import absolute_import # General-purpose Python library imports import getpass @@ -16,19 +17,19 @@ from boto.exception import BotoServerError # AppScale-specific imports -from appcontroller_client import AppControllerClient -from appscale_logger import AppScaleLogger -from custom_exceptions import AppControllerException -from custom_exceptions import AppScaleException -from custom_exceptions import BadConfigurationException -from custom_exceptions import ShellException -from custom_exceptions import TimeoutException +from .appcontroller_client import AppControllerClient +from .appscale_logger import AppScaleLogger +from .custom_exceptions import AppControllerException +from .custom_exceptions import AppScaleException +from .custom_exceptions import BadConfigurationException +from .custom_exceptions import ShellException +from .custom_exceptions import TimeoutException from appscale.agents.base_agent import AgentRuntimeException from appscale.agents.gce_agent import CredentialTypes from appscale.agents.gce_agent import GCEAgent from appscale.agents.factory import InfrastructureAgentFactory -from local_state import APPSCALE_VERSION -from local_state import LocalState +from .local_state import APPSCALE_VERSION +from .local_state import LocalState class RemoteHelper(object): From eaeef01b1867360a43c4744c687b0d6a8cbf45e9 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Wed, 14 Nov 2018 12:06:26 -0600 Subject: [PATCH 17/63] more import fixes, these weren't causing issues but changed them to help avoid future issues --- appscale/tools/local_state.py | 14 +++++++------- appscale/tools/registration_helper.py | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index e8c8a1fb..abf1584e 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -21,13 +21,13 @@ # AppScale-specific imports -from appcontroller_client import AppControllerClient -from appscale_logger import AppScaleLogger -from custom_exceptions import AppControllerException -from custom_exceptions import AppScaleException -from custom_exceptions import AppScalefileException -from custom_exceptions import BadConfigurationException -from custom_exceptions import ShellException +from .appcontroller_client import AppControllerClient +from .appscale_logger import AppScaleLogger +from .custom_exceptions import AppControllerException +from .custom_exceptions import AppScaleException +from .custom_exceptions import AppScalefileException +from .custom_exceptions import BadConfigurationException +from .custom_exceptions import ShellException # The version of the AppScale Tools we're running on. diff --git a/appscale/tools/registration_helper.py b/appscale/tools/registration_helper.py index e9042a16..b783427d 100644 --- a/appscale/tools/registration_helper.py +++ b/appscale/tools/registration_helper.py @@ -4,10 +4,10 @@ import urllib import urllib2 -from appcontroller_client import AppControllerClient -from custom_exceptions import AppScaleException -from local_state import LocalState -from local_state import APPSCALE_VERSION +from .appcontroller_client import AppControllerClient +from .custom_exceptions import AppScaleException +from .local_state import LocalState +from .local_state import APPSCALE_VERSION class RegistrationHelper(object): """ RegistrationHelper provides convenience methods used during the From c6ba99c6d0c516cd68b15c75db34e649b7efc821 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Tue, 19 Feb 2019 16:58:43 -0600 Subject: [PATCH 18/63] fix flexmock to use the agents AppScaleState instead of local_state --- test/test_appscale_run_instances.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index 6bca07a4..ebb08ce9 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -24,6 +24,7 @@ # AppScale import, the library that we're testing here from appscale.agents.ec2_agent import EC2Agent +from appscale.agents.config import AppScaleState from appscale.tools.appcontroller_client import AppControllerClient from appscale.tools.appscale_logger import AppScaleLogger from appscale.tools.appscale_tools import AppScaleTools @@ -107,6 +108,8 @@ def setUp(self): # And add in mocks for libraries most of the tests mock out self.local_state = flexmock(LocalState) + # AppScale Agents + self.appscale_state = flexmock(AppScaleState) def setup_ec2_mocks(self): # first, pretend that our image does exist in EC2 @@ -145,7 +148,7 @@ def setup_ec2_mocks(self): # mock out creating the keypair fake_key = flexmock(name='fake_key', material='baz') - self.local_state.should_receive('write_key_file').with_args( + self.appscale_state.should_receive('write_key_file').with_args( re.compile(self.keyname), fake_key.material).and_return() self.fake_ec2.should_receive('create_key_pair').with_args(self.keyname) \ .and_return(fake_key) @@ -425,7 +428,7 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): # mock out writing the secret key to ~/.appscale, as well as reading it # later - secret_key_location = LocalState.get_secret_key_location(self.keyname) + secret_key_location = AppScaleState.ssh_key(self.keyname) fake_secret = flexmock(name="fake_secret") fake_secret.should_receive('read').and_return('the secret') fake_secret.should_receive('write').and_return() From 8897ce237883db694d3c5f49e5331d284a17bbed Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Wed, 20 Feb 2019 10:53:42 -0600 Subject: [PATCH 19/63] add local_state back into mock objects --- test/test_appscale_run_instances.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index ebb08ce9..b4c59450 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -437,6 +437,17 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): self.builtins.should_receive('open').with_args(secret_key_location, 'w') \ .and_return(fake_secret) + # mock out writing the secret key to ~/.appscale, as well as reading it + # later + secret_key_location = LocalState.get_secret_key_location(self.keyname) + fake_secret = flexmock(name="fake_secret") + fake_secret.should_receive('read').and_return('the secret') + fake_secret.should_receive('write').and_return() + self.builtins.should_receive('open').with_args(secret_key_location, 'r') \ + .and_return(fake_secret) + self.builtins.should_receive('open').with_args(secret_key_location, 'w') \ + .and_return(fake_secret) + self.setup_ec2_mocks() # slip in some fake spot instance info From 3f790dcec7ec7f195e36f19eae25d76229ecc587 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Tue, 21 May 2019 14:33:15 -0500 Subject: [PATCH 20/63] change default repo to appscale --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index cf97ff20..c7a6a32f 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,8 @@ # .PHONY: bump-major bump-minor bump-patch help all test -AGENTS_REPO?=https://github.com/scragraham/appscale-agents -AGENTS_BRANCH?=topic-agent-init +AGENTS_REPO?=https://github.com/appscale/appscale-agents +AGENTS_BRANCH?=master all: help From 62da99611c8995e351491e96a8c6cf69d90ccb78 Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Mon, 28 Jan 2019 20:53:11 +0200 Subject: [PATCH 21/63] Added search2 role --- appscale/tools/node_layout.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/appscale/tools/node_layout.py b/appscale/tools/node_layout.py index bc5465c4..d74e7dde 100644 --- a/appscale/tools/node_layout.py +++ b/appscale/tools/node_layout.py @@ -43,7 +43,8 @@ class NodeLayout(): # TODO: remove 'appengine' role. ADVANCED_FORMAT_KEYS = [ 'master', 'database', 'appengine', 'compute', 'open', 'zookeeper', - 'memcache', 'taskqueue', 'search', 'load_balancer'] + 'memcache', 'taskqueue', 'search', 'search2', 'load_balancer' + ] # A tuple containing all of the roles (simple and advanced) that the @@ -53,7 +54,8 @@ class NodeLayout(): VALID_ROLES = ( 'master', 'appengine', 'compute', 'database', 'shadow', 'open', 'load_balancer', 'db_master', 'db_slave', 'zookeeper', 'memcache', - 'taskqueue', 'taskqueue_master', 'taskqueue_slave', 'search') + 'taskqueue', 'taskqueue_master', 'taskqueue_slave', 'search', 'search2' + ) # A regular expression that matches IP addresses, used in ips.yaml files for From 08fcf1c07e93260549f0abcd6bd20c21398e6c93 Mon Sep 17 00:00:00 2001 From: Steven Graham <1858109+scragraham@users.noreply.github.com> Date: Mon, 8 Jul 2019 12:31:26 -0500 Subject: [PATCH 22/63] set version to 3.8.0 --- appscale/tools/local_state.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index abf1584e..3fb9ff68 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -31,7 +31,7 @@ # The version of the AppScale Tools we're running on. -APPSCALE_VERSION = "3.7.1" +APPSCALE_VERSION = "3.8.0" class LocalState(object): diff --git a/setup.py b/setup.py index 1dde127b..795da769 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name='appscale-tools', - version='3.7.1', + version='3.8.0', description='A set of command-line tools for interacting with AppScale', long_description=long_description, author='AppScale Systems, Inc.', From 78a0e23d543f2dec0c80a59baaf8b631d6a6aa34 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Thu, 11 Jul 2019 15:07:31 -0700 Subject: [PATCH 23/63] use hermes dictionary keys --- appscale/tools/cluster_stats.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/appscale/tools/cluster_stats.py b/appscale/tools/cluster_stats.py index c4e7f315..c9cf02cb 100644 --- a/appscale/tools/cluster_stats.py +++ b/appscale/tools/cluster_stats.py @@ -47,11 +47,9 @@ def __init__(self, mountpoint, partition_dict): class LoadAvg(object): def __init__(self, loadavg_dict): - self.last_1_min = loadavg_dict["last_1_min"] - self.last_5_min = loadavg_dict["last_5_min"] - self.last_15_min = loadavg_dict["last_15_min"] - self.runnable_entities = loadavg_dict["runnable_entities"] - self.scheduling_entities = loadavg_dict["scheduling_entities"] + self.last_1_min = loadavg_dict["last_1min"] + self.last_5_min = loadavg_dict["last_5min"] + self.last_15_min = loadavg_dict["last_15min"] class Disk(object): def __init__(self, partitions): @@ -70,8 +68,7 @@ def __init__(self, private_ip, node_stats_dict): self.swap = NodeStats.Swap(node_stats_dict["swap"]) partitions = [ NodeStats.Partition(mountpoint, details) - for partition_dict in node_stats_dict["disk"] - for mountpoint, details in partition_dict.iteritems() + for mountpoint, details in node_stats_dict["partitions_dict"].iteritems() ] self.disk = NodeStats.Disk(partitions) self.loadavg = NodeStats.LoadAvg(node_stats_dict["loadavg"]) From 1a705aed1af5301213106e69ba4a61652afc8385 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Fri, 12 Jul 2019 23:58:02 -0700 Subject: [PATCH 24/63] Update ec2 run instances mock for removed terminated instance key check --- test/test_appscale_run_instances.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index b4c59450..4e29e92e 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -181,8 +181,9 @@ def setup_ec2_mocks(self): running_reservation = flexmock(name='running_reservation', instances=[running_instance]) - self.fake_ec2.should_receive('get_all_instances').and_return(no_instances) \ - .and_return(no_instances).and_return(pending_reservation) \ + self.fake_ec2.should_receive('get_all_instances') \ + .and_return(no_instances) \ + .and_return(pending_reservation) \ .and_return(running_reservation) # finally, inject the mocked EC2 in @@ -586,9 +587,9 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): running_reservation = flexmock(name='running_reservation', instances=[running_instance]) - self.fake_ec2.should_receive('get_all_instances').and_return(no_instances) \ + self.fake_ec2.should_receive('get_all_instances') \ .and_return(no_instances) \ - .and_return(no_instances).and_return(pending_reservation) \ + .and_return(pending_reservation) \ .and_return(running_reservation) argv = [ @@ -759,9 +760,9 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): running_reservation = flexmock(name='running_reservation', instances=[running_instance]) - self.fake_ec2.should_receive('get_all_instances').and_return(no_instances) \ + self.fake_ec2.should_receive('get_all_instances') \ .and_return(no_instances) \ - .and_return(no_instances).and_return(pending_reservation) \ + .and_return(pending_reservation) \ .and_return(running_reservation) argv = [ From cb6e1d94c4d48e61bc9600ee1f377f1cda14bfd1 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Tue, 16 Jul 2019 11:31:57 -0700 Subject: [PATCH 25/63] use available percent key for cpu --- appscale/tools/cluster_stats.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/cluster_stats.py b/appscale/tools/cluster_stats.py index c9cf02cb..0c7deca2 100644 --- a/appscale/tools/cluster_stats.py +++ b/appscale/tools/cluster_stats.py @@ -17,7 +17,7 @@ def __init__(self, cpu_dict): self.idle = cpu_dict["idle"] self.system = cpu_dict["system"] self.user = cpu_dict["user"] - self.load = 100.0 - self.idle + self.load = cpu_dict["percent"] self.count = cpu_dict["count"] class Memory(object): From 7e541063c7eb29b6cc9d4f31bce67dbfb1b9441a Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Tue, 16 Jul 2019 11:32:28 -0700 Subject: [PATCH 26/63] print disk percentage per mountpoint --- appscale/tools/appscale_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 4f302c3f..0460b564 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -302,7 +302,7 @@ def _print_nodes_info(cls, nodes, invisible_nodes): "+" if n.is_loaded else "-"), "{:.1f}x{}".format(n.cpu.load, n.cpu.count), 100.0 - n.memory.available_percent, - " ".join("{:.1f}".format(p.used_percent) for p in n.disk.partitions), + " ".join('"{}" => {:.1f}'.format(p.mountpoint, p.used_percent) for p in n.disk.partitions), "{:.1f} {:.1f} {:.1f}".format( n.loadavg.last_1_min, n.loadavg.last_5_min, n.loadavg.last_15_min), " ".join(n.roles)) From 67f22f1a15cf438fc0e08dcf6779f25551372eea Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Tue, 16 Jul 2019 14:46:43 -0700 Subject: [PATCH 27/63] AppController client handle not ready error --- appscale/tools/appcontroller_client.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appscale/tools/appcontroller_client.py b/appscale/tools/appcontroller_client.py index dd213097..df968bc0 100644 --- a/appscale/tools/appcontroller_client.py +++ b/appscale/tools/appcontroller_client.py @@ -45,6 +45,10 @@ class AppControllerClient(): BAD_SECRET_MESSAGE = 'false: bad secret' + # The message that an AppController can return when not ready for requests. + NOT_READY_MESSAGE = 'false: not ready yet' + + # The number of times we should retry SOAP calls in case of failures. DEFAULT_NUM_RETRIES = 5 @@ -128,6 +132,9 @@ def timeout_handler(_, __): raise BadSecretException("Could not authenticate successfully" + \ " to the AppController. You may need to change the keyname in use.") + if retval == self.NOT_READY_MESSAGE: + raise AppControllerException("AppController not ready.") + return retval def set_parameters(self, locations, params): From 5186055dfbda3fd5f4fcc2d2a73a115477cb6df0 Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Tue, 16 Jul 2019 15:23:29 -0700 Subject: [PATCH 28/63] Add --update flag parameter to `appscale up` command. --- appscale/tools/appscale.py | 8 +++++++- appscale/tools/local_state.py | 6 ++++++ appscale/tools/parse_args.py | 7 +++++++ appscale/tools/scripts/appscale.py | 12 +++++++++++- 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 51e1bb80..4ff7e032 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -241,10 +241,12 @@ def init(self, environment=None): shutil.copy(self.TEMPLATE_APPSCALEFILE, appscalefile_location) - def up(self): + def up(self, update=""): """ Starts an AppScale deployment with the configuration options from the AppScalefile in the current directory. + Args: + update: An appscale code directory to update and build. Raises: AppScalefileException: If there is no AppScalefile in the current directory. @@ -259,6 +261,10 @@ def up(self): # Construct a run-instances command from the file's contents command = [] + if update: + command.append("--update") + command.append(str(update)) + for key, value in contents_as_yaml.items(): if key in self.DEPRECATED_ASF_ARGS: raise AppScalefileException( diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index abf1584e..c713fdc8 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -216,6 +216,12 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): } creds.update(additional_creds) + if options.update: + update_dir_creds = { + 'update': str(options.update) + } + creds.update(update_dir_creds) + if options.infrastructure: iaas_creds = { 'infrastructure': options.infrastructure, diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index b26d6140..196d7673 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -137,6 +137,10 @@ class ParseArgs(object): # explicitly provide a value, in megabytes. DEFAULT_MAX_APPSERVER_MEMORY = 400 + # The list of code directories to specify which updating the code and building it. + ALLOWED_DIR_UPDATES = ["common", "app_controller", "admin_server", "taskqueue", + "app_db", "iaas_manager", "hermes", "api_server", + "appserver_java"] def __init__(self, argv, function): """Creates a new ParseArgs for a set of acceptable flags. @@ -183,6 +187,9 @@ def add_allowed_flags(self, function): #TODO: remove arguments in appscale-run-instances that are no longer # supported. (min, max, appengine, max_memory, scp) if function == "appscale-run-instances": + self.parser.add_argument('--update', + choices=self.ALLOWED_DIR_UPDATES, + default="", help="updates specified code directory and builds it") # flags relating to how many VMs we should spawn self.parser.add_argument('--min_machines', '--min', type=int, help="the minimum number of VMs to use") diff --git a/appscale/tools/scripts/appscale.py b/appscale/tools/scripts/appscale.py index 3e9267a3..a86ac5e4 100644 --- a/appscale/tools/scripts/appscale.py +++ b/appscale/tools/scripts/appscale.py @@ -41,8 +41,18 @@ def main(): "customize it for your particular cloud or cluster.", 'green') sys.exit(0) elif command == "up": + if len(sys.argv) > 4: + cprint("Usage: appscale up [--update]", 'red') + sys.exit(1) + update_dir = "" + if len(sys.argv) == 4: + if sys.argv[2] == '--update': + update_dir = sys.argv[3] + else: + cprint("Usage: appscale up [--update] ", 'red') + sys.exit(1) try: - appscale.up() + appscale.up(update=update_dir) except Exception as exception: LocalState.generate_crash_log(exception, traceback.format_exc()) sys.exit(1) From 9bf611a7a391ab600caab882422f809b0b2c3592 Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Tue, 16 Jul 2019 15:25:30 -0700 Subject: [PATCH 29/63] Add unit tests --- test/test_appscale.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/test_appscale.py b/test/test_appscale.py index e45b4e13..12203e93 100644 --- a/test/test_appscale.py +++ b/test/test_appscale.py @@ -256,6 +256,48 @@ def testUpWithCloudAppScalefile(self): appscale.up() + def testUpWithUpdate(self): + # Calling `appscale up --update {code_dir}` where we pass + # the name of the appscale code directory to update and build. + appscale = AppScale() + + update_dir = 'common' + contents = { + 'infrastructure': 'ec2', + 'instance_type': 'm3.medium', + 'machine': 'ami-ABCDEFG', + 'keyname': 'bookey', + 'group': 'boogroup', + 'min_machines': 1, + 'max_machines': 1, + 'zone': 'my-zone-1b', + 'EC2_ACCESS_KEY': 'baz', + 'EC2_SECRET_KEY': 'baz' + } + yaml_dumped_contents = yaml.dump(contents) + self.addMockForAppScalefile(appscale, yaml_dumped_contents) + + flexmock(os.path) + os.path.should_call('exists') + os.path.should_receive('exists').with_args( + '/boo/' + appscale.APPSCALEFILE).and_return(True) + + fake_ec2 = flexmock(name="fake_ec2") + fake_ec2.should_receive('get_all_instances') + fake_ec2.should_receive('get_all_zones').with_args('my-zone-1b') \ + .and_return('anything') + fake_ec2.should_receive('get_image').with_args('ami-ABCDEFG') \ + .and_return() + flexmock(boto.ec2) + boto.ec2.should_receive('connect_to_region').with_args('my-zone-1', + aws_access_key_id='baz', + aws_secret_access_key='baz').and_return(fake_ec2) + + # finally, mock out the actual appscale-run-instances call + flexmock(AppScaleTools) + AppScaleTools.should_receive('run_instances') + appscale.up(update=update_dir) + def testSshWithNoAppScalefile(self): # calling 'appscale ssh' with no AppScalefile in the local # directory should throw up and die From 857dbb212d56eec7371553eb40cba400a348f85d Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Tue, 16 Jul 2019 15:29:37 -0700 Subject: [PATCH 30/63] Fix broken unit tests --- test/test_appscale_logger.py | 3 ++- test/test_local_state.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/test/test_appscale_logger.py b/test/test_appscale_logger.py index 980084ca..5c2dcb72 100644 --- a/test/test_appscale_logger.py +++ b/test/test_appscale_logger.py @@ -45,7 +45,7 @@ def setUp(self): argv = ["--min", "1", "--max", "1", "--infrastructure", "ec2", "--instance_type", "m3.medium", "--machine", "ami-ABCDEFG", "--group", "blargscale", "--keyname", "appscale", "--zone", "my-zone-1b", "--EC2_ACCESS_KEY", "baz", - "--EC2_SECRET_KEY", "baz"] + "--EC2_SECRET_KEY", "baz", "--update", "common"] function = "appscale-run-instances" self.options = ParseArgs(argv, function).args self.my_id = "12345" @@ -99,6 +99,7 @@ def setUp(self): 'EC2_ACCESS_KEY': 'baz', 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', + 'update': 'common' } # finally, construct a http payload for mocking that the below diff --git a/test/test_local_state.py b/test/test_local_state.py index 8af573c7..8bd75084 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -104,7 +104,7 @@ def test_generate_deployment_params(self): zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', - login_host='public1', aws_subnet_id=None, aws_vpc_id=None) + login_host='public1', aws_subnet_id=None, aws_vpc_id=None, update='code_dir') node_layout = NodeLayout({ 'table' : 'cassandra', 'infrastructure' : "ec2", @@ -141,7 +141,8 @@ def test_generate_deployment_params(self): 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', 'aws_subnet_id': None, - 'aws_vpc_id': None + 'aws_vpc_id': None, + 'update': 'code_dir' } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price':'1.23'}) @@ -160,7 +161,7 @@ def test_generate_deployment_params_no_login(self): zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', - login_host=None, aws_subnet_id=None, aws_vpc_id=None) + login_host=None, aws_subnet_id=None, aws_vpc_id=None, update='') node_layout = NodeLayout({ 'table': 'cassandra', 'infrastructure': "ec2", From c9cdce1dd0f4ce299ddc5a9bce2470bbbc185d13 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Tue, 16 Jul 2019 17:35:43 -0700 Subject: [PATCH 31/63] modify cluster_stats dictionary for test --- test/test_appscale_describe_instances.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/test/test_appscale_describe_instances.py b/test/test_appscale_describe_instances.py index e10dbbb9..18f6cdc7 100644 --- a/test/test_appscale_describe_instances.py +++ b/test/test_appscale_describe_instances.py @@ -56,10 +56,9 @@ def test_started_two_nodes(self): 'http': 1080, 'language': 'python', 'total_reqs': 24, 'appservers': 3, 'pending_appservers': 0, 'https': 1443, 'reqs_enqueued': 0}}, 'memory': {'available': 1117507584, 'total': 3839168512, 'used': 3400077312}, - 'disk': [{'/': {'total': 9687113728, 'free': 4895760384, 'used': 4364763136}}], - 'cpu': {'count': 2, 'idle': 66.7, 'system': 9.5, 'user': 19.0}, - 'loadavg': {'last_1_min': 0.64, 'last_5_min': 1.04, 'last_15_min': 0.95, - 'scheduling_entities': 381, 'runnable_entities': 3}, + 'partitions_dict': {'/': {'total': 9687113728, 'free': 4895760384, 'used': 4364763136}}, + 'cpu': {'count': 2, 'idle': 66.7, 'system': 9.5, 'user': 19.0, 'percent': 33.3}, + 'loadavg': {'last_1min': 0.64, 'last_5min': 1.04, 'last_15min': 0.95}, # Irrelevant for status bellow 'state': 'Done starting up AppScale, now in heartbeat mode', 'swap': {'used': 0, 'free': 0}, @@ -74,11 +73,10 @@ def test_started_two_nodes(self): 'is_initialized': True, 'is_loaded': True, 'apps': {}, - 'loadavg': {'last_1_min': 1.05, 'last_5_min': 0.92, 'last_15_min': 0.95, - 'scheduling_entities': 312, 'runnable_entities': 2}, + 'loadavg': {'last_1min': 1.05, 'last_5min': 0.92, 'last_15min': 0.95}, 'memory': {'available': 2891546624, 'total': 3839168512, 'used': 1951600640}, - 'disk': [{'/': {'total': 9687113728, 'free': 5160316928, 'used': 4100206592}}], - 'cpu': {'count': 2, 'idle': 100.0, 'system': 0.0, 'user': 0.0}, + 'partitions_dict': {'/': {'total': 9687113728, 'free': 5160316928, 'used': 4100206592}}, + 'cpu': {'count': 2, 'idle': 100.0, 'system': 0.0, 'user': 0.0, 'percent': 0.0}, # Irrelevant for status bellow 'state': 'Done starting up AppScale, now in heartbeat mode', From f91b82c13bcda387f2d84050d19aa855a71560e5 Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Thu, 18 Jul 2019 12:00:05 -0700 Subject: [PATCH 32/63] Allow specifying multiple directories to update and build --- appscale/tools/appscale.py | 4 +++- appscale/tools/parse_args.py | 1 + appscale/tools/scripts/appscale.py | 22 +++++++++++++++------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 4ff7e032..2597fb68 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -263,7 +263,9 @@ def up(self, update=""): command = [] if update: command.append("--update") - command.append(str(update)) + update_dirs = update.split(',') + for dir in update_dirs: + command.append(dir) for key, value in contents_as_yaml.items(): if key in self.DEPRECATED_ASF_ARGS: diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index 196d7673..a2f79460 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -188,6 +188,7 @@ def add_allowed_flags(self, function): # supported. (min, max, appengine, max_memory, scp) if function == "appscale-run-instances": self.parser.add_argument('--update', + nargs='+', choices=self.ALLOWED_DIR_UPDATES, default="", help="updates specified code directory and builds it") # flags relating to how many VMs we should spawn diff --git a/appscale/tools/scripts/appscale.py b/appscale/tools/scripts/appscale.py index a86ac5e4..9245c9eb 100644 --- a/appscale/tools/scripts/appscale.py +++ b/appscale/tools/scripts/appscale.py @@ -41,16 +41,24 @@ def main(): "customize it for your particular cloud or cluster.", 'green') sys.exit(0) elif command == "up": - if len(sys.argv) > 4: - cprint("Usage: appscale up [--update]", 'red') - sys.exit(1) update_dir = "" - if len(sys.argv) == 4: - if sys.argv[2] == '--update': - update_dir = sys.argv[3] - else: + if len(sys.argv) > 2: + if sys.argv[2] != '--update': cprint("Usage: appscale up [--update] ", 'red') sys.exit(1) + + if len(sys.argv) > 4: + dir_args = sys.argv[3:] + update_dir = ",".join(dir_args) + + else: + try: + update_dir = sys.argv[3] + except IndexError as e: + cprint("Usage: appscale up [--update] ", 'red') + cprint("Please specify the code directory to update and build", 'red') + sys.exit(1) + try: appscale.up(update=update_dir) except Exception as exception: From 846539e7a3ac90b94540187e21be77371826af61 Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Thu, 18 Jul 2019 16:58:32 -0700 Subject: [PATCH 33/63] Add 'all' parameter to specifying updating and building all directories. --- appscale/tools/parse_args.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index a2f79460..e82cf31a 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -138,9 +138,9 @@ class ParseArgs(object): DEFAULT_MAX_APPSERVER_MEMORY = 400 # The list of code directories to specify which updating the code and building it. - ALLOWED_DIR_UPDATES = ["common", "app_controller", "admin_server", "taskqueue", - "app_db", "iaas_manager", "hermes", "api_server", - "appserver_java"] + ALLOWED_DIR_UPDATES = ["all", "common", "app_controller", "admin_server", + "taskqueue", "app_db", "iaas_manager", "hermes", + "api_server", "appserver_java"] def __init__(self, argv, function): """Creates a new ParseArgs for a set of acceptable flags. From bcc420185fe802a2e49169281f6a95b1fbe1d908 Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Fri, 19 Jul 2019 11:35:33 -0700 Subject: [PATCH 34/63] Fix unit test --- test/test_appscale_logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_appscale_logger.py b/test/test_appscale_logger.py index 5c2dcb72..83703e72 100644 --- a/test/test_appscale_logger.py +++ b/test/test_appscale_logger.py @@ -99,7 +99,7 @@ def setUp(self): 'EC2_ACCESS_KEY': 'baz', 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', - 'update': 'common' + 'update': ['common'] } # finally, construct a http payload for mocking that the below From c7770fabaf088e7f34d5d34f84cb8c7676eee7e8 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Tue, 23 Jul 2019 22:36:56 -0700 Subject: [PATCH 35/63] initial commit for using "appscale deploy" to deploy a dispatch.yaml --- appscale/tools/admin_api/client.py | 32 +++++++++++++++++++ appscale/tools/appscale.py | 6 ++++ appscale/tools/appscale_tools.py | 51 ++++++++++++++++++++++++++++++ appscale/tools/utils.py | 22 +++++++++++++ 4 files changed, 111 insertions(+) diff --git a/appscale/tools/admin_api/client.py b/appscale/tools/admin_api/client.py index d558c0d1..7ded9ecb 100644 --- a/appscale/tools/admin_api/client.py +++ b/appscale/tools/admin_api/client.py @@ -321,3 +321,35 @@ def update_queues(self, project_id, queues): message = 'AdminServer returned: {}'.format(response.status_code) raise AdminError(message) + + @retry(**RETRY_POLICY) + def update_dispatch(self, project_id, dispatch_rules): + """ Updates the the project's dispatch configuration. + + Args: + project_id: A string specifying the project ID. + dispatch_rules: A dictionary containing dispatch configuration details. + Raises: + AdminError if unable to update dispatch configuration. + """ + versions_url = ('{prefix}/{project}' + .format(prefix=self.prefix, project=project_id)) + headers = { + 'AppScale-Secret': self.secret, + 'Content-Type': 'application/json' + } + params = { + 'updateMask': 'dispatchRules' + } + body = dispatch_rules + + response = requests.patch(versions_url, headers=headers, params=params, + json=body, verify=False) + + operation = self.extract_response(response) + try: + operation_id = operation['name'].split('/')[-1] + except (KeyError, IndexError): + raise AdminError('Invalid operation: {}'.format(operation)) + + return operation_id diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 51e1bb80..ea04dd19 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -5,6 +5,7 @@ import base64 import json import os +import re import shutil import subprocess import sys @@ -568,10 +569,15 @@ def deploy(self, app, project_id=None): command.append("--file") command.append(app) + if project_id is not None: command.append("--project") command.append(project_id) + if re.match(r'\/dispatch\.yaml$', app): + options = ParseArgs(command, "appscale-upload-app").args + AppScaleTools.update_dispatch(options) + # Finally, exec the command. Don't worry about validating it - # appscale-upload-app will do that for us. options = ParseArgs(command, "appscale-upload-app").args diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 4f302c3f..8ffa5af1 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1117,6 +1117,57 @@ def upload_app(cls, options): http_port = int(match.group(2)) return login_host, http_port + + + @classmethod + def update_dispatch(cls, options): + """ Updates an application's dispatch routing rules from the configuration + file. + + Args: + options: A Namespace that has fields for each parameter that can be + passed in via the command-line interface. + """ + if not options.project: + raise BadConfigurationException('Project must be specified to deploy a ' + 'dispatch.yaml file') + project_id = options.project + source_location = options.file + keyname = options.keyname + is_verbose = options.verbose + dispatch_rules = utils.dispatch_from_yaml(source_location) + + AppScaleLogger.log('Updating dispatch for {}'.format(project_id)) + + load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') + secret_key = LocalState.get_secret_key(keyname) + admin_client = AdminClient(load_balancer_ip, secret_key) + operation_id = admin_client.update_dispatch(project_id, dispatch_rules) + + # Check on the operation. + AppScaleLogger.log("Please wait for your dispatch to be updated.") + + deadline = time.time() + cls.MAX_OPERATION_TIME + while True: + if time.time() > deadline: + raise AppScaleException('The operation took too long.') + operation = admin_client.get_operation(project_id, operation_id) + if not operation['done']: + time.sleep(1) + continue + + if 'error' in operation: + raise AppScaleException(operation['error']['message']) + dispatch_rules = operation['response']['dispatchRules'] + break + + AppScaleLogger.verbose( + "The following dispatchRules have been applied to your application's " + "configuration (note: rules have been converted to be compatible " + "with AppScale) : {}".format(dispatch_rules), is_verbose) + AppScaleLogger.success('Dispatch has been updated for {}'.format( + project_id)) + @classmethod def update_cron(cls, source_location, keyname, project_id): """ Updates a project's cron jobs from the configuration file. diff --git a/appscale/tools/utils.py b/appscale/tools/utils.py index 4fb7cdfa..8426170a 100644 --- a/appscale/tools/utils.py +++ b/appscale/tools/utils.py @@ -2,6 +2,7 @@ import errno import os +import re import tarfile import yaml import zipfile @@ -9,6 +10,9 @@ from .custom_exceptions import BadConfigurationException +# The regex used to group the dispatch url into 'domain' and 'path'. +DISPATCH_URL = re.compile(r'^([^/]+)(/.*)$') + def shortest_path_from_list(file_name, name_list): """ Determines the shortest path to a file in a list of candidates. @@ -272,6 +276,24 @@ def queues_from_xml(contents): return queues +def dispatch_from_yaml(source_location): + dispatch_rules = None + with open(source_location) as config_file: + dispatch_rules = config_file.read() + + if not dispatch_rules or not dispatch_rules.get('dispatch'): + raise BadConfigurationException('Could not retrieve anything from ' + 'specified dispatch.yaml') + modified_rules = [] + for dispatch_rule in dispatch_rules['dispatch']: + rule = {} + rule['service'] = dispatch_rule['service'] + rule['domain'], rule['path'] = DISPATCH_URL.match( + dispatch_rule['url']).groups() + modified_rules.append(rule) + + return modified_rules + def mkdir(dir_path): """ Creates a directory. From f0152c0a64fd92abf0f166528bacd62bd9cb67e5 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 24 Jul 2019 10:31:24 -0700 Subject: [PATCH 36/63] dispatch.yaml uses module as keyname --- appscale/tools/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/utils.py b/appscale/tools/utils.py index 8426170a..70a55528 100644 --- a/appscale/tools/utils.py +++ b/appscale/tools/utils.py @@ -287,7 +287,7 @@ def dispatch_from_yaml(source_location): modified_rules = [] for dispatch_rule in dispatch_rules['dispatch']: rule = {} - rule['service'] = dispatch_rule['service'] + rule['service'] = dispatch_rule['module'] rule['domain'], rule['path'] = DISPATCH_URL.match( dispatch_rule['url']).groups() modified_rules.append(rule) From 19d664aa435f2c7175ac24698e237ada6fcf89ae Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 24 Jul 2019 11:30:52 -0700 Subject: [PATCH 37/63] fix regex for dispatch yaml --- appscale/tools/appscale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index ea04dd19..915bbd4f 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -574,7 +574,7 @@ def deploy(self, app, project_id=None): command.append("--project") command.append(project_id) - if re.match(r'\/dispatch\.yaml$', app): + if re.match(r'.*\/dispatch\.yaml$', app): options = ParseArgs(command, "appscale-upload-app").args AppScaleTools.update_dispatch(options) From d978557b4e385d5c105f0ed222686df75b02630d Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 24 Jul 2019 11:31:08 -0700 Subject: [PATCH 38/63] fix format of request for dispatchRules update --- appscale/tools/admin_api/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/admin_api/client.py b/appscale/tools/admin_api/client.py index 7ded9ecb..0aba5a51 100644 --- a/appscale/tools/admin_api/client.py +++ b/appscale/tools/admin_api/client.py @@ -341,7 +341,7 @@ def update_dispatch(self, project_id, dispatch_rules): params = { 'updateMask': 'dispatchRules' } - body = dispatch_rules + body = {'dispatchRules': dispatch_rules} response = requests.patch(versions_url, headers=headers, params=params, json=body, verify=False) From 4ed538bd51f446fae0196b7c559d6e4d5ad0175b Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 24 Jul 2019 11:32:33 -0700 Subject: [PATCH 39/63] load the yaml file --- appscale/tools/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appscale/tools/utils.py b/appscale/tools/utils.py index 70a55528..9d29c2dd 100644 --- a/appscale/tools/utils.py +++ b/appscale/tools/utils.py @@ -279,7 +279,7 @@ def queues_from_xml(contents): def dispatch_from_yaml(source_location): dispatch_rules = None with open(source_location) as config_file: - dispatch_rules = config_file.read() + dispatch_rules = yaml.safe_load(config_file.read()) if not dispatch_rules or not dispatch_rules.get('dispatch'): raise BadConfigurationException('Could not retrieve anything from ' From 6d7c6089ad9e723ff9e0d6b1d2adfc3b3b5cc118 Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Wed, 31 Jul 2019 19:29:59 +0300 Subject: [PATCH 40/63] Add fdb_clusterfile_content property to AppScalefile --- appscale/tools/local_state.py | 3 ++- appscale/tools/parse_args.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 3fb9ff68..cc0425c6 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -212,7 +212,8 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): "user_commands": json.dumps(options.user_commands), "verbose": str(options.verbose), "flower_password": options.flower_password, - "default_max_appserver_memory": str(options.default_max_appserver_memory) + "default_max_appserver_memory": str(options.default_max_appserver_memory), + "fdb_clusterfile_content": options.fdb_clusterfile_content } creds.update(additional_creds) diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index b26d6140..099011d0 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -314,6 +314,8 @@ def add_allowed_flags(self, function): self.parser.add_argument('--user_commands', help="a base64-encoded YAML dictating the commands to run before " + "starting each AppController") + self.parser.add_argument('--fdb_clusterfile_content', + help="a string representing content of FoundationDB clusterfile") elif function == "appscale-gather-logs": self.parser.add_argument('--keyname', '-k', default=self.DEFAULT_KEYNAME, help="the keypair name to use") From 609dbc00707b4189fc5ab66ebb1737cce423d099 Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Tue, 6 Aug 2019 16:06:10 -0700 Subject: [PATCH 41/63] Remove outdated upgrade code This won't be usable when upgrading to 4.0, so removing it now can reduce potential confusion. --- appscale/tools/appscale.py | 34 ---- appscale/tools/appscale_tools.py | 249 ----------------------------- appscale/tools/parse_args.py | 12 -- appscale/tools/scripts/appscale.py | 6 - appscale/tools/scripts/upgrade.py | 26 --- appscale/tools/version_helper.py | 16 -- setup.py | 1 - 7 files changed, 344 deletions(-) delete mode 100644 appscale/tools/scripts/upgrade.py diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 51e1bb80..e54c754c 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -113,7 +113,6 @@ class AppScale(): undeploy Removes from the current deployment. DATA ASSOCIATED WITH THE APPLICATION WILL BE LOST. - upgrade Upgrades AppScale code to its latest version. """ # Deprecated AppScaleFile arguments DEPRECATED_ASF_ARGS = ['n', 'scp', 'appengine', 'max_memory', 'min', 'max'] @@ -894,36 +893,3 @@ def register(self, deployment_id): AppScaleLogger.success( 'Registration complete for AppScale deployment {0}.' .format(deployment['name'])) - - def upgrade(self): - """ Allows users to upgrade to the latest version of AppScale.""" - contents_as_yaml = yaml.safe_load(self.read_appscalefile()) - - # Construct the appscale-upgrade command from argv and the contents of - # the AppScalefile. - command = [] - - if 'keyname' in contents_as_yaml: - command.append("--keyname") - command.append(contents_as_yaml['keyname']) - - if 'verbose' in contents_as_yaml and contents_as_yaml['verbose'] == True: - command.append("--verbose") - - if 'ips_layout' in contents_as_yaml: - command.append('--ips_layout') - command.append( - base64.b64encode(yaml.dump(contents_as_yaml['ips_layout']))) - - if 'login' in contents_as_yaml: - command.extend(['--login', contents_as_yaml['login']]) - - if 'test' in contents_as_yaml and contents_as_yaml['test'] == True: - command.append('--test') - - options = ParseArgs(command, 'appscale-upgrade').args - options.ips = yaml.safe_load(base64.b64decode(options.ips_layout)) - options.terminate = False - options.clean = False - options.instance_type = contents_as_yaml.get('instance_type') - AppScaleTools.upgrade(options) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 0460b564..591884c8 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -2,8 +2,6 @@ from __future__ import absolute_import -import Queue -import datetime import getpass import json import os @@ -11,10 +9,7 @@ import shutil import socket import sys -import threading import time -import traceback -import urllib2 import uuid from collections import Counter from itertools import chain @@ -37,27 +32,10 @@ from appscale.tools.local_state import APPSCALE_VERSION, LocalState from appscale.tools.node_layout import NodeLayout from appscale.tools.remote_helper import RemoteHelper -from appscale.tools.version_helper import latest_tools_version from appscale.agents.factory import InfrastructureAgentFactory -def async_layout_upgrade(ip, keyname, script, error_bucket, verbose=False): - """ Run a command over SSH and place exceptions in a bucket. - - Args: - ip: A string containing and IP address. - keyname: A string containing the deployment keyname. - script: A string to run as a command over SSH. - error_bucket: A thread-safe queue. - verbose: A boolean indicating whether or not to log verbosely. - """ - try: - RemoteHelper.ssh(ip, keyname, script, verbose) - except ShellException as ssh_error: - error_bucket.put(ssh_error) - - MIN_FREE_DISK_DB = 40.0 MIN_FREE_DISK = 10.0 MIN_AVAILABLE_MEMORY = 7.0 @@ -109,26 +87,6 @@ class AppScaleTools(object): ADMIN_CAPABILITIES = "upload_app" - # AppScale repository location on an AppScale image. - APPSCALE_REPO = "~/appscale" - - - # Bootstrap command to run. - BOOTSTRAP_CMD = '{}/bootstrap.sh >> /var/log/appscale/bootstrap.log'.\ - format(APPSCALE_REPO) - - - # Command to run the upgrade script from /appscale/scripts directory. - UPGRADE_SCRIPT = "python " + APPSCALE_REPO + "/scripts/upgrade.py" - - - # Template used for GitHub API calls. - GITHUB_API = 'https://api.github.com/repos/{owner}/{repo}' - - - # Location of the upgrade status file on the remote machine. - UPGRADE_STATUS_FILE_LOC = '/var/log/appscale/upgrade-status-' - @classmethod def add_instances(cls, options): """Adds additional machines to an AppScale deployment. @@ -1255,210 +1213,3 @@ def update_queues(cls, source_location, keyname, project_id): secret_key = LocalState.get_secret_key(keyname) admin_client = AdminClient(load_balancer_ip, secret_key) admin_client.update_queues(version.project_id, queues) - - @classmethod - def upgrade(cls, options): - """ Upgrades the deployment to the latest AppScale version. - Args: - options: A Namespace that has fields for each parameter that can be - passed in via the command-line interface. - """ - node_layout = NodeLayout(options) - previous_ips = LocalState.get_local_nodes_info(options.keyname) - previous_node_list = node_layout.from_locations_json_list(previous_ips) - node_layout.nodes = previous_node_list - - latest_tools = APPSCALE_VERSION - try: - AppScaleLogger.log( - 'Checking if an update is available for appscale-tools') - latest_tools = latest_tools_version() - except (URLError, ValueError): - # Prompt the user if version metadata can't be fetched. - if not options.test: - response = raw_input( - 'Unable to check for the latest version of appscale-tools. Would ' - 'you like to continue upgrading anyway? (y/N) ') - if response.lower() not in ['y', 'yes']: - raise AppScaleException('Cancelled AppScale upgrade.') - - if latest_tools > APPSCALE_VERSION: - raise AppScaleException( - "There is a newer version ({}) of appscale-tools available. Please " - "upgrade the tools package before running 'appscale upgrade'.". - format(latest_tools)) - - master_ip = node_layout.head_node().public_ip - upgrade_version_available = cls.get_upgrade_version_available() - - current_version = RemoteHelper.get_host_appscale_version( - master_ip, options.keyname, options.verbose) - - # Don't run bootstrap if current version is later that the most recent - # public one. Covers cases of revoked versions/tags and ensures we won't - # try to downgrade the code. - if current_version >= upgrade_version_available: - AppScaleLogger.log( - 'AppScale is already up to date. Skipping code upgrade.') - AppScaleLogger.log( - 'Running upgrade script to check if any other upgrades are needed.') - cls.shut_down_appscale_if_running(options) - cls.run_upgrade_script(options, node_layout) - return - - cls.shut_down_appscale_if_running(options) - cls.upgrade_appscale(options, node_layout) - - @classmethod - def run_upgrade_script(cls, options, node_layout): - """ Runs the upgrade script which checks for any upgrades needed to be performed. - Args: - options: A Namespace that has fields for each parameter that can be - passed in via the command-line interface. - node_layout: A NodeLayout object for the deployment. - """ - timestamp = datetime.datetime.now().strftime('%Y-%m-%d_%H:%M:%S') - - db_ips = [node.private_ip for node in node_layout.nodes - if node.is_role('db_master') or node.is_role('db_slave')] - zk_ips = [node.private_ip for node in node_layout.nodes - if node.is_role('zookeeper')] - - upgrade_script_command = '{script} --keyname {keyname} '\ - '--log-postfix {timestamp} '\ - '--db-master {db_master} '\ - '--zookeeper {zk_ips} '\ - '--database {db_ips} '\ - '--replication {replication}'.format( - script=cls.UPGRADE_SCRIPT, - keyname=options.keyname, - timestamp=timestamp, - db_master=node_layout.db_master().private_ip, - zk_ips=' '.join(zk_ips), - db_ips=' '.join(db_ips), - replication=node_layout.replication - ) - master_public_ip = node_layout.head_node().public_ip - - AppScaleLogger.log("Running upgrade script to check if any other upgrade is needed.") - # Run the upgrade command as a background process. - error_bucket = Queue.Queue() - threading.Thread( - target=async_layout_upgrade, - args=(master_public_ip, options.keyname, upgrade_script_command, - error_bucket, options.verbose) - ).start() - - last_message = None - while True: - # Check if the SSH thread has crashed. - try: - ssh_error = error_bucket.get(block=False) - AppScaleLogger.warn('Error executing upgrade script') - LocalState.generate_crash_log(ssh_error, traceback.format_exc()) - except Queue.Empty: - pass - - upgrade_status_file = cls.UPGRADE_STATUS_FILE_LOC + timestamp + ".json" - command = 'cat' + " " + upgrade_status_file - upgrade_status = RemoteHelper.ssh( - master_public_ip, options.keyname, command, options.verbose) - json_status = json.loads(upgrade_status) - - if 'status' not in json_status or 'message' not in json_status: - raise AppScaleException('Invalid status log format') - - if json_status['status'] == 'complete': - AppScaleLogger.success(json_status['message']) - break - - if json_status['status'] == 'inProgress': - if json_status['message'] != last_message: - AppScaleLogger.log(json_status['message']) - last_message = json_status['message'] - time.sleep(cls.SLEEP_TIME) - continue - - # Assume the message is an error. - AppScaleLogger.warn(json_status['message']) - raise AppScaleException(json_status['message']) - - @classmethod - def shut_down_appscale_if_running(cls, options): - """ Checks if AppScale is running and shuts it down as this is an offline upgrade. - Args: - options: A Namespace that has fields for each parameter that can be - passed in via the command-line interface. - """ - if os.path.exists(LocalState.get_secret_key_location(options.keyname)): - AppScaleLogger.warn("AppScale needs to be down for this upgrade. " - "Upgrade process could take a while and it is not reversible.") - - if not options.test: - response = raw_input( - 'Are you sure you want to proceed with shutting down AppScale to ' - 'continue the upgrade? (y/N) ') - if response.lower() not in ['y', 'yes']: - raise AppScaleException("Cancelled AppScale upgrade.") - - AppScaleLogger.log("Shutting down AppScale...") - cls.terminate_instances(options) - else: - AppScaleLogger.warn("Upgrade process could take a while and it is not reversible.") - - if options.test: - return - - response = raw_input( - 'Are you sure you want to proceed with the upgrade? (y/N) ') - if response.lower() not in ['y', 'yes']: - raise AppScaleException("Cancelled AppScale upgrade.") - - @classmethod - def upgrade_appscale(cls, options, node_layout): - """ Runs the bootstrap script on each of the remote machines. - Args: - options: A Namespace that has fields for each parameter that can be - passed in via the command-line interface. - node_layout: A NodeLayout object for the deployment. - """ - unique_ips = [node.public_ip for node in node_layout.nodes] - - AppScaleLogger.log("Upgrading AppScale code to the latest version on " - "these machines: {}".format(unique_ips)) - threads = [] - error_ips = [] - for ip in unique_ips: - t = threading.Thread(target=cls.run_bootstrap, args=(ip, options, error_ips)) - threads.append(t) - - for x in threads: - x.start() - - for x in threads: - x.join() - - if not error_ips: - cls.run_upgrade_script(options, node_layout) - - @classmethod - def run_bootstrap(cls, ip, options, error_ips): - try: - RemoteHelper.ssh(ip, options.keyname, cls.BOOTSTRAP_CMD, options.verbose) - AppScaleLogger.success( - 'Successfully updated and built AppScale on {}'.format(ip)) - except ShellException: - error_ips.append(ip) - AppScaleLogger.warn('Unable to upgrade AppScale code on {}.\n' - 'Please correct any errors listed in /var/log/appscale/bootstrap.log ' - 'on that machine and re-run appscale upgrade.'.format(ip)) - return error_ips - - @classmethod - def get_upgrade_version_available(cls): - """ Gets the latest release tag version available. - """ - github_api = cls.GITHUB_API.format(owner='AppScale', repo='appscale') - response = urllib2.urlopen('{}/tags'.format(github_api)) - tag_list = json.loads(response.read()) - return tag_list[0]['name'] diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index b26d6140..625667bb 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -473,16 +473,6 @@ def add_allowed_flags(self, function): help="the name of the AppController property to set") self.parser.add_argument('--property_value', help="the value of the AppController property to set") - elif function == "appscale-upgrade": - self.parser.add_argument('--keyname', '-k', default=self.DEFAULT_KEYNAME, - help="the keypair name to use") - self.parser.add_argument('--ips_layout', - help="a YAML file dictating the placement strategy") - self.parser.add_argument( - '--login_host', help='The public IP address of the head node') - self.parser.add_argument( - '--test', action='store_true', default=False, - help='Skips user input when upgrading deployment') else: raise SystemExit @@ -582,8 +572,6 @@ def validate_allowed_flags(self, function): pass elif function == "appscale-set-property": pass - elif function == "appscale-upgrade": - pass else: raise SystemExit diff --git a/appscale/tools/scripts/appscale.py b/appscale/tools/scripts/appscale.py index 3e9267a3..64f790ce 100644 --- a/appscale/tools/scripts/appscale.py +++ b/appscale/tools/scripts/appscale.py @@ -231,12 +231,6 @@ def main(): elif command in ["--version", "-v"]: print APPSCALE_VERSION sys.exit(0) - elif command == "upgrade": - try: - appscale.upgrade() - except Exception as exception: - LocalState.generate_crash_log(exception, traceback.format_exc()) - sys.exit(1) else: print(AppScale.USAGE) if command == "help": diff --git a/appscale/tools/scripts/upgrade.py b/appscale/tools/scripts/upgrade.py deleted file mode 100644 index 4dc8eb97..00000000 --- a/appscale/tools/scripts/upgrade.py +++ /dev/null @@ -1,26 +0,0 @@ -# General-purpose Python library imports -import sys -import traceback - - -# AppScale library imports -# Make sure we're on Python 2.6 or greater before importing any code -# that's incompatible with older versions. -from .. import version_helper -version_helper.ensure_valid_python_is_used() - - -from ..appscale_tools import AppScaleTools -from ..local_state import LocalState -from ..parse_args import ParseArgs - - -def main(): - """ Execute appscale-upgrade script. """ - options = ParseArgs(sys.argv[1:], "appscale-upgrade").args - try: - AppScaleTools.upgrade(options) - sys.exit(0) - except Exception as e: - LocalState.generate_crash_log(e, traceback.format_exc()) - sys.exit(1) diff --git a/appscale/tools/version_helper.py b/appscale/tools/version_helper.py index 73697f60..05f33da4 100644 --- a/appscale/tools/version_helper.py +++ b/appscale/tools/version_helper.py @@ -9,23 +9,7 @@ # First-party Python libraries -import json import sys -import urllib2 - -# The location of appscale-tools on PyPI. -PYPI_URL = 'https://pypi.python.org/pypi/appscale-tools' - - -def latest_tools_version(): - """ Fetches the latest tools version available on PyPI. - - Returns: - A string containing a version number. - """ - response = urllib2.urlopen('{}/json'.format(PYPI_URL)) - pypi_info = json.loads(response.read()) - return pypi_info['info']['version'] def ensure_valid_python_is_used(system=sys): diff --git a/setup.py b/setup.py index 795da769..7be345f6 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,6 @@ 'appscale-set-property=appscale.tools.scripts.set_property:main', 'appscale-terminate-instances=' + 'appscale.tools.scripts.terminate_instances:main', - 'appscale-upgrade=appscale.tools.scripts.upgrade:main', 'appscale-upload-app=appscale.tools.scripts.upload_app:main' ] }, From 40710bf68f81ea88583097fac68fd92dedcf771e Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 14 Aug 2019 11:24:21 -0700 Subject: [PATCH 42/63] address pr comments --- appscale/tools/appscale.py | 23 ++++++++++++++++++++++- appscale/tools/appscale_tools.py | 3 +-- appscale/tools/utils.py | 18 +++++++++++++----- 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 915bbd4f..5cfae479 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -152,6 +152,27 @@ def read_appscalefile(self): "again.") + def is_dispatch_yaml(self, source_location): + """ Checks the path passed to appscale deploy to see if it is a + dispatch yaml + + Returns: + True if source_location is a non-empty file containing a 'dispatch' key. + False if source_location is a directory, the file is empty, or the file + does not contain a 'dispatch' key. + """ + if os.path.isdir(source_location): + return False + + with open(source_location) as config_file: + dispatch_rules = yaml.safe_load(config_file) + + if dispatch_rules and dispatch_rules.get('dispatch'): + return True + + return False + + def get_locations_json_file(self, keyname): """ Returns the location where the AppScale tools writes JSON data about where each virtual machine is located in the currently running @@ -574,7 +595,7 @@ def deploy(self, app, project_id=None): command.append("--project") command.append(project_id) - if re.match(r'.*\/dispatch\.yaml$', app): + if self.is_dispatch_yaml(app): options = ParseArgs(command, "appscale-upload-app").args AppScaleTools.update_dispatch(options) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 75721c63..0980f98a 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1163,8 +1163,7 @@ def update_dispatch(cls, options): AppScaleLogger.verbose( "The following dispatchRules have been applied to your application's " - "configuration (note: rules have been converted to be compatible " - "with AppScale) : {}".format(dispatch_rules), is_verbose) + "configuration : {}".format(dispatch_rules), is_verbose) AppScaleLogger.success('Dispatch has been updated for {}'.format( project_id)) diff --git a/appscale/tools/utils.py b/appscale/tools/utils.py index 9d29c2dd..ad0d6264 100644 --- a/appscale/tools/utils.py +++ b/appscale/tools/utils.py @@ -11,7 +11,8 @@ from .custom_exceptions import BadConfigurationException # The regex used to group the dispatch url into 'domain' and 'path'. -DISPATCH_URL = re.compile(r'^([^/]+)(/.*)$') +# taken from GAE's 1.9.69 SDK (google/appengine/api/dispatchinfo.py) +_URL_SPLITTER_RE = re.compile(r'^([^/]+)(/.*)$') def shortest_path_from_list(file_name, name_list): @@ -277,9 +278,8 @@ def queues_from_xml(contents): return queues def dispatch_from_yaml(source_location): - dispatch_rules = None with open(source_location) as config_file: - dispatch_rules = yaml.safe_load(config_file.read()) + dispatch_rules = yaml.safe_load(config_file) if not dispatch_rules or not dispatch_rules.get('dispatch'): raise BadConfigurationException('Could not retrieve anything from ' @@ -287,8 +287,16 @@ def dispatch_from_yaml(source_location): modified_rules = [] for dispatch_rule in dispatch_rules['dispatch']: rule = {} - rule['service'] = dispatch_rule['module'] - rule['domain'], rule['path'] = DISPATCH_URL.match( + module = dispatch_rule.get('module') + service = dispatch_rule.get('service') + if module and service: + raise BadConfigurationException('Both module: and service: in dispatch ' + 'entry. Please use only one.') + if not (module or service): + raise BadConfigurationException("Missing required value 'service'.") + + rule['service'] = module or service + rule['domain'], rule['path'] = _URL_SPLITTER_RE.match( dispatch_rule['url']).groups() modified_rules.append(rule) From ee307c9168c0f19a72a2dc5efe13363b26abfa1d Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 14 Aug 2019 11:26:15 -0700 Subject: [PATCH 43/63] return after updating dispatch --- appscale/tools/appscale.py | 1 + 1 file changed, 1 insertion(+) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 5cfae479..9ffc5df3 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -598,6 +598,7 @@ def deploy(self, app, project_id=None): if self.is_dispatch_yaml(app): options = ParseArgs(command, "appscale-upload-app").args AppScaleTools.update_dispatch(options) + return # Finally, exec the command. Don't worry about validating it - # appscale-upload-app will do that for us. From 756a36eda8a830138dba1d748f070b16b53fff4b Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Wed, 14 Aug 2019 17:18:37 -0700 Subject: [PATCH 44/63] Fix unit tests for FDB changes --- test/test_appscale_logger.py | 1 + test/test_local_state.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/test_appscale_logger.py b/test/test_appscale_logger.py index 980084ca..aa37fdee 100644 --- a/test/test_appscale_logger.py +++ b/test/test_appscale_logger.py @@ -99,6 +99,7 @@ def setUp(self): 'EC2_ACCESS_KEY': 'baz', 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', + 'fdb_clusterfile_content': None, } # finally, construct a http payload for mocking that the below diff --git a/test/test_local_state.py b/test/test_local_state.py index 8af573c7..97f8cb5a 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -104,7 +104,8 @@ def test_generate_deployment_params(self): zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', - login_host='public1', aws_subnet_id=None, aws_vpc_id=None) + login_host='public1', aws_subnet_id=None, aws_vpc_id=None, + fdb_clusterfile_content=None) node_layout = NodeLayout({ 'table' : 'cassandra', 'infrastructure' : "ec2", @@ -141,7 +142,8 @@ def test_generate_deployment_params(self): 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', 'aws_subnet_id': None, - 'aws_vpc_id': None + 'aws_vpc_id': None, + 'fdb_clusterfile_content': None } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price':'1.23'}) @@ -160,7 +162,8 @@ def test_generate_deployment_params_no_login(self): zone='my-zone-1b', verbose=True, user_commands=[], flower_password="abc", default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', - login_host=None, aws_subnet_id=None, aws_vpc_id=None) + login_host=None, aws_subnet_id=None, aws_vpc_id=None, + fdb_clusterfile_content=None) node_layout = NodeLayout({ 'table': 'cassandra', 'infrastructure': "ec2", @@ -197,7 +200,8 @@ def test_generate_deployment_params_no_login(self): 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', 'aws_subnet_id': None, - 'aws_vpc_id': None + 'aws_vpc_id': None, + 'fdb_clusterfile_content': None } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price': '1.23'}) From 80ccf129060509a4d9f8c2fa7e72aaf5e199b771 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Thu, 15 Aug 2019 12:34:59 -0700 Subject: [PATCH 45/63] fix unit test --- test/test_appscale.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/test_appscale.py b/test/test_appscale.py index e45b4e13..a64c791c 100644 --- a/test/test_appscale.py +++ b/test/test_appscale.py @@ -429,7 +429,8 @@ def testDeployWithCloudAppScalefile(self): } yaml_dumped_contents = yaml.dump(contents) self.addMockForAppScalefile(appscale, yaml_dumped_contents) - + flexmock(appscale) + appscale.should_receive('is_dispatch_yaml').and_return(False) # finally, mock out the actual appscale-run-instances call fake_port = 8080 fake_host = 'fake_host' @@ -499,7 +500,8 @@ def testDeployWithCloudAppScalefileAndTestFlag(self): } yaml_dumped_contents = yaml.dump(contents) self.addMockForAppScalefile(appscale, yaml_dumped_contents) - + flexmock(appscale) + appscale.should_receive('is_dispatch_yaml').and_return(False) # finally, mock out the actual appscale-run-instances call fake_port = 8080 fake_host = 'fake_host' From 5c94aed125b8eb6f9c83814b1de7585fd4224267 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 21 Aug 2019 08:30:40 -0700 Subject: [PATCH 46/63] deploy dispatch.yaml when application is deployed --- appscale/tools/appscale.py | 36 +++++++------------------------- appscale/tools/appscale_tools.py | 36 +++++++++++++++++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 6f277da2..ee312f95 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -5,11 +5,9 @@ import base64 import json import os -import re import shutil import subprocess import sys - import yaml from appscale.tools.appengine_helper import AppEngineHelper @@ -151,27 +149,6 @@ def read_appscalefile(self): "again.") - def is_dispatch_yaml(self, source_location): - """ Checks the path passed to appscale deploy to see if it is a - dispatch yaml - - Returns: - True if source_location is a non-empty file containing a 'dispatch' key. - False if source_location is a directory, the file is empty, or the file - does not contain a 'dispatch' key. - """ - if os.path.isdir(source_location): - return False - - with open(source_location) as config_file: - dispatch_rules = yaml.safe_load(config_file) - - if dispatch_rules and dispatch_rules.get('dispatch'): - return True - - return False - - def get_locations_json_file(self, keyname): """ Returns the location where the AppScale tools writes JSON data about where each virtual machine is located in the currently running @@ -589,16 +566,10 @@ def deploy(self, app, project_id=None): command.append("--file") command.append(app) - if project_id is not None: command.append("--project") command.append(project_id) - if self.is_dispatch_yaml(app): - options = ParseArgs(command, "appscale-upload-app").args - AppScaleTools.update_dispatch(options) - return - # Finally, exec the command. Don't worry about validating it - # appscale-upload-app will do that for us. options = ParseArgs(command, "appscale-upload-app").args @@ -606,6 +577,13 @@ def deploy(self, app, project_id=None): AppScaleTools.update_indexes(options.file, options.keyname, options.project) AppScaleTools.update_cron(options.file, options.keyname, options.project) AppScaleTools.update_queues(options.file, options.keyname, options.project) + try: + AppScaleTools.update_dispatch(options.file, options.keyname, + options.project, options.verbose) + except AppScaleException as e: + AppScaleLogger.warn('Request to update dispatch failed, if your ' + 'dispatch references undeployed services, ignore ' + 'this exception: {}'.format(e)) return login_host, http_port diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index a360f02c..03fbe634 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1076,9 +1076,8 @@ def upload_app(cls, options): return login_host, http_port - @classmethod - def update_dispatch(cls, options): + def update_dispatch(cls, source_location, keyname, project_id, is_verbose): """ Updates an application's dispatch routing rules from the configuration file. @@ -1086,15 +1085,31 @@ def update_dispatch(cls, options): options: A Namespace that has fields for each parameter that can be passed in via the command-line interface. """ - if not options.project: - raise BadConfigurationException('Project must be specified to deploy a ' - 'dispatch.yaml file') - project_id = options.project - source_location = options.file - keyname = options.keyname - is_verbose = options.verbose - dispatch_rules = utils.dispatch_from_yaml(source_location) + if cls.TAR_GZ_REGEX.search(source_location): + fetch_function = utils.config_from_tar_gz + version = Version.from_tar_gz(source_location) + elif cls.ZIP_REGEX.search(source_location): + fetch_function = utils.config_from_zip + version = Version.from_zip(source_location) + elif os.path.isdir(source_location): + fetch_function = utils.config_from_dir + version = Version.from_directory(source_location) + elif source_location.endswith('.yaml'): + fetch_function = utils.config_from_dir + version = Version.from_yaml_file(source_location) + source_location = os.path.dirname(source_location) + else: + raise BadConfigurationException( + '{} must be a directory, tar.gz, or zip'.format(source_location)) + if project_id: + version.project_id = project_id + + dispatch_config = fetch_function('dispatch.yaml', source_location) + if dispatch_config is None: + return + + dispatch_rules = utils.dispatch_from_yaml(dispatch_config) AppScaleLogger.log('Updating dispatch for {}'.format(project_id)) load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') @@ -1125,6 +1140,7 @@ def update_dispatch(cls, options): AppScaleLogger.success('Dispatch has been updated for {}'.format( project_id)) + @classmethod def update_cron(cls, source_location, keyname, project_id): """ Updates a project's cron jobs from the configuration file. From 671d9eebe627cde049c133a7835052641a9a919f Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 21 Aug 2019 14:35:04 -0700 Subject: [PATCH 47/63] use correct project_id and modify dispatch_from_yaml method to use the fetch_function --- appscale/tools/appscale_tools.py | 14 ++++++-------- appscale/tools/utils.py | 9 ++++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 03fbe634..b469503e 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1105,17 +1105,15 @@ def update_dispatch(cls, source_location, keyname, project_id, is_verbose): if project_id: version.project_id = project_id - dispatch_config = fetch_function('dispatch.yaml', source_location) - if dispatch_config is None: + dispatch_rules = utils.dispatch_from_yaml(source_location, fetch_function) + if dispatch_rules is None: return - - dispatch_rules = utils.dispatch_from_yaml(dispatch_config) - AppScaleLogger.log('Updating dispatch for {}'.format(project_id)) + AppScaleLogger.log('Updating dispatch for {}'.format(version.project_id)) load_balancer_ip = LocalState.get_host_with_role(keyname, 'load_balancer') secret_key = LocalState.get_secret_key(keyname) admin_client = AdminClient(load_balancer_ip, secret_key) - operation_id = admin_client.update_dispatch(project_id, dispatch_rules) + operation_id = admin_client.update_dispatch(version.project_id, dispatch_rules) # Check on the operation. AppScaleLogger.log("Please wait for your dispatch to be updated.") @@ -1124,7 +1122,7 @@ def update_dispatch(cls, source_location, keyname, project_id, is_verbose): while True: if time.time() > deadline: raise AppScaleException('The operation took too long.') - operation = admin_client.get_operation(project_id, operation_id) + operation = admin_client.get_operation(version.project_id, operation_id) if not operation['done']: time.sleep(1) continue @@ -1138,7 +1136,7 @@ def update_dispatch(cls, source_location, keyname, project_id, is_verbose): "The following dispatchRules have been applied to your application's " "configuration : {}".format(dispatch_rules), is_verbose) AppScaleLogger.success('Dispatch has been updated for {}'.format( - project_id)) + version.project_id)) @classmethod diff --git a/appscale/tools/utils.py b/appscale/tools/utils.py index ad0d6264..5cb9d715 100644 --- a/appscale/tools/utils.py +++ b/appscale/tools/utils.py @@ -277,9 +277,12 @@ def queues_from_xml(contents): return queues -def dispatch_from_yaml(source_location): - with open(source_location) as config_file: - dispatch_rules = yaml.safe_load(config_file) +def dispatch_from_yaml(source_location, fetch_function): + dispatch_config = fetch_function('dispatch.yaml', source_location) + if dispatch_config is None: + return + + dispatch_rules = yaml.safe_load(dispatch_config) if not dispatch_rules or not dispatch_rules.get('dispatch'): raise BadConfigurationException('Could not retrieve anything from ' From a5ef9afc565fe48bd9e073d79d27502e15bcc58e Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 21 Aug 2019 14:35:40 -0700 Subject: [PATCH 48/63] catch AdminError for update_dispatch because it could be a false negative --- appscale/tools/appscale.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index ee312f95..a9200ff5 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -22,7 +22,7 @@ from appscale.tools.parse_args import ParseArgs from appscale.tools.registration_helper import RegistrationHelper from appscale.tools.remote_helper import RemoteHelper - +from appscale.tools.admin_api.client import AdminError class AppScale(): """ AppScale provides a configuration-file-based alternative to the @@ -580,7 +580,7 @@ def deploy(self, app, project_id=None): try: AppScaleTools.update_dispatch(options.file, options.keyname, options.project, options.verbose) - except AppScaleException as e: + except (AdminError, AppScaleException) as e: AppScaleLogger.warn('Request to update dispatch failed, if your ' 'dispatch references undeployed services, ignore ' 'this exception: {}'.format(e)) From 8099b2193b6cf713693d46127f5c61ed56db712f Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 21 Aug 2019 15:13:45 -0700 Subject: [PATCH 49/63] fix unit tests --- test/test_appscale.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/test_appscale.py b/test/test_appscale.py index a64c791c..9290bb94 100644 --- a/test/test_appscale.py +++ b/test/test_appscale.py @@ -429,8 +429,6 @@ def testDeployWithCloudAppScalefile(self): } yaml_dumped_contents = yaml.dump(contents) self.addMockForAppScalefile(appscale, yaml_dumped_contents) - flexmock(appscale) - appscale.should_receive('is_dispatch_yaml').and_return(False) # finally, mock out the actual appscale-run-instances call fake_port = 8080 fake_host = 'fake_host' @@ -440,6 +438,7 @@ def testDeployWithCloudAppScalefile(self): AppScaleTools.should_receive('update_indexes') AppScaleTools.should_receive('update_cron') AppScaleTools.should_receive('update_queues') + AppScaleTools.should_receive('update_dispatch') app = '/bar/app' (host, port) = appscale.deploy(app) self.assertEquals(fake_host, host) @@ -500,8 +499,6 @@ def testDeployWithCloudAppScalefileAndTestFlag(self): } yaml_dumped_contents = yaml.dump(contents) self.addMockForAppScalefile(appscale, yaml_dumped_contents) - flexmock(appscale) - appscale.should_receive('is_dispatch_yaml').and_return(False) # finally, mock out the actual appscale-run-instances call fake_port = 8080 fake_host = 'fake_host' @@ -511,6 +508,7 @@ def testDeployWithCloudAppScalefileAndTestFlag(self): AppScaleTools.should_receive('update_indexes') AppScaleTools.should_receive('update_cron') AppScaleTools.should_receive('update_queues') + AppScaleTools.should_receive('update_dispatch') app = '/bar/app' (host, port) = appscale.deploy(app) self.assertEquals(fake_host, host) From 19438fcc89828b52ef8769d6f299859898a4ab68 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Wed, 21 Aug 2019 13:18:38 -0700 Subject: [PATCH 50/63] Remove azure dependencies, these are now transitive via appscale-agents --- setup.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.py b/setup.py index 7be345f6..4a438abd 100644 --- a/setup.py +++ b/setup.py @@ -44,16 +44,12 @@ install_requires=[ 'adal>=0.4.7', 'appscale-agents', - 'azure==2.0.0', - 'azure-mgmt-marketplaceordering', 'cryptography>=2.3.0', 'argparse', 'boto', 'google-api-python-client==1.5.4', 'haikunator', 'httplib2', - 'keyring<19.0.0', - 'msrestazure==0.4.34', 'oauth2client==4.0.0', 'pyOpenSSL>=18.0.0', 'PyYAML', From ab71cc4b29d3f135fd3de9f61731aa3ce1b80d7c Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Fri, 30 Aug 2019 14:09:05 -0700 Subject: [PATCH 51/63] Address code review comments. --- appscale/tools/appscale.py | 8 +++----- appscale/tools/local_state.py | 2 +- appscale/tools/parse_args.py | 8 ++++---- appscale/tools/scripts/appscale.py | 16 +++++----------- 4 files changed, 13 insertions(+), 21 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index ddb9b464..372595c0 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -239,12 +239,12 @@ def init(self, environment=None): shutil.copy(self.TEMPLATE_APPSCALEFILE, appscalefile_location) - def up(self, update=""): + def up(self, update=[]): """ Starts an AppScale deployment with the configuration options from the AppScalefile in the current directory. Args: - update: An appscale code directory to update and build. + update: A list of appscale code directories to update and build. Raises: AppScalefileException: If there is no AppScalefile in the current directory. @@ -261,9 +261,7 @@ def up(self, update=""): command = [] if update: command.append("--update") - update_dirs = update.split(',') - for dir in update_dirs: - command.append(dir) + command.extend(update) for key, value in contents_as_yaml.items(): if key in self.DEPRECATED_ASF_ARGS: diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 49a21d3f..5c5b9016 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -219,7 +219,7 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): if options.update: update_dir_creds = { - 'update': str(options.update) + 'update': options.update } creds.update(update_dir_creds) diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index 184491a9..0fb17379 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -138,9 +138,9 @@ class ParseArgs(object): DEFAULT_MAX_APPSERVER_MEMORY = 400 # The list of code directories to specify which updating the code and building it. - ALLOWED_DIR_UPDATES = ["all", "common", "app_controller", "admin_server", - "taskqueue", "app_db", "iaas_manager", "hermes", - "api_server", "appserver_java"] + ALLOWED_DIR_UPDATES = ('all', 'common', 'app_controller', 'admin_server', + 'taskqueue', 'app_db', 'iaas_manager', 'hermes', + 'api_server', 'appserver_java') def __init__(self, argv, function): """Creates a new ParseArgs for a set of acceptable flags. @@ -190,7 +190,7 @@ def add_allowed_flags(self, function): self.parser.add_argument('--update', nargs='+', choices=self.ALLOWED_DIR_UPDATES, - default="", help="updates specified code directory and builds it") + default=[], help="updates specified code directory and builds it") # flags relating to how many VMs we should spawn self.parser.add_argument('--min_machines', '--min', type=int, help="the minimum number of VMs to use") diff --git a/appscale/tools/scripts/appscale.py b/appscale/tools/scripts/appscale.py index c1c415b7..98d0e571 100644 --- a/appscale/tools/scripts/appscale.py +++ b/appscale/tools/scripts/appscale.py @@ -41,23 +41,17 @@ def main(): "customize it for your particular cloud or cluster.", 'green') sys.exit(0) elif command == "up": - update_dir = "" + update_dir = [] if len(sys.argv) > 2: if sys.argv[2] != '--update': cprint("Usage: appscale up [--update] ", 'red') sys.exit(1) - if len(sys.argv) > 4: - dir_args = sys.argv[3:] - update_dir = ",".join(dir_args) + if len(sys.argv) < 4: + cprint("Usage: appscale up [--update] ", 'red') + cprint("Please specify the code directory to update and build", 'red') - else: - try: - update_dir = sys.argv[3] - except IndexError as e: - cprint("Usage: appscale up [--update] ", 'red') - cprint("Please specify the code directory to update and build", 'red') - sys.exit(1) + update_dir = sys.argv[3:] try: appscale.up(update=update_dir) From f7ec9dfa2e74908e5634c6ae896c10449027c52f Mon Sep 17 00:00:00 2001 From: Tanvi Marballi Date: Fri, 30 Aug 2019 15:52:53 -0700 Subject: [PATCH 52/63] Fix broken unit test --- test/test_appscale.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_appscale.py b/test/test_appscale.py index 63af60ae..74871591 100644 --- a/test/test_appscale.py +++ b/test/test_appscale.py @@ -261,7 +261,7 @@ def testUpWithUpdate(self): # the name of the appscale code directory to update and build. appscale = AppScale() - update_dir = 'common' + update_dir = ['common'] contents = { 'infrastructure': 'ec2', 'instance_type': 'm3.medium', From 37b6d1c057ce29e6ce9453c702d08d29f3f8b2c8 Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Mon, 9 Sep 2019 16:32:29 +0300 Subject: [PATCH 53/63] Store --verbose value in AppScaleLogger It isn't always propriate place. And this solution is not perfect but it simplifies further CLI refactoring which is predecessor for splitting infrastructure provisioning from `appscale up` command. --- appscale/tools/appscale.py | 16 +-- appscale/tools/appscale_logger.py | 22 +-- appscale/tools/appscale_stats.py | 25 ++-- appscale/tools/appscale_tools.py | 59 ++++---- appscale/tools/local_state.py | 56 +++----- appscale/tools/parse_args.py | 3 + appscale/tools/remote_helper.py | 219 +++++++++++------------------- 7 files changed, 150 insertions(+), 250 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index 372595c0..f8c89a83 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -343,7 +343,7 @@ def valid_ssh_key(self, config, run_instances_opts): BadConfigurationException: If the IPs layout was not a dictionary. """ keyname = config['keyname'] - verbose = config.get('verbose', False) + AppScaleLogger.is_verbose = config.get('verbose', False) if not isinstance(config['ips_layout'], dict) and \ not isinstance(config['ips_layout'], list): @@ -368,7 +368,7 @@ def valid_ssh_key(self, config, run_instances_opts): remote_key = '{}/ssh.key'.format(RemoteHelper.CONFIG_DIR) try: RemoteHelper.scp( - head_node.public_ip, keyname, ssh_key_location, remote_key, verbose) + head_node.public_ip, keyname, ssh_key_location, remote_key) except ShellException: return False @@ -377,13 +377,13 @@ def valid_ssh_key(self, config, run_instances_opts): .format(key=remote_key, ip=ip) try: RemoteHelper.ssh( - head_node.public_ip, keyname, ssh_to_ip, verbose, user='root') + head_node.public_ip, keyname, ssh_to_ip, user='root') except ShellException: return False return True for ip in all_ips: - if not self.can_ssh_to_ip(ip, keyname, verbose): + if not self.can_ssh_to_ip(ip, keyname): return False return True @@ -408,7 +408,7 @@ def get_ips_from_options(ips): 'Invalid IP address in {}'.format(all_ips) return all_ips - def can_ssh_to_ip(self, ip, keyname, is_verbose): + def can_ssh_to_ip(self, ip, keyname): """ Attempts to SSH into the machine located at the given IP address with the given SSH key. @@ -416,15 +416,13 @@ def can_ssh_to_ip(self, ip, keyname, is_verbose): ip: The IP address to attempt to SSH into. keyname: The name of the SSH key that uniquely identifies this AppScale deployment. - is_verbose: A bool that indicates if we should print the SSH command we - execute to stdout. Returns: A bool that indicates whether or not the given SSH key can log in without a password to the given machine. """ try: - RemoteHelper.ssh(ip, keyname, 'ls', is_verbose, user='root') + RemoteHelper.ssh(ip, keyname, 'ls', user='root') return True except ShellException: return False @@ -585,7 +583,7 @@ def deploy(self, app, project_id=None): AppScaleTools.update_queues(options.file, options.keyname, options.project) try: AppScaleTools.update_dispatch(options.file, options.keyname, - options.project, options.verbose) + options.project) except (AdminError, AppScaleException) as e: AppScaleLogger.warn('Request to update dispatch failed, if your ' 'dispatch references undeployed services, ignore ' diff --git a/appscale/tools/appscale_logger.py b/appscale/tools/appscale_logger.py index e3b2294d..db5dc7f8 100644 --- a/appscale/tools/appscale_logger.py +++ b/appscale/tools/appscale_logger.py @@ -1,11 +1,5 @@ -#!/usr/bin/env python - - -# General-purpose Python library imports import httplib - -# Third party library imports from termcolor import cprint @@ -14,16 +8,15 @@ class AppScaleLogger(): response, prints them to the user and saves them for debugging purposes. """ - # The location where we remotely dump logs to LOGS_HOST = "logs.appscale.com" - # The headers necessary for posting data to the remote logs service. HEADERS = { - 'Content-Type' : 'application/x-www-form-urlencoded' + 'Content-Type': 'application/x-www-form-urlencoded' } + is_verbose = False @classmethod def log(cls, message): @@ -34,7 +27,6 @@ def log(cls, message): """ print message - @classmethod def warn(cls, message): """Prints the specified message with red text as well as to a file. @@ -44,7 +36,6 @@ def warn(cls, message): """ cprint(message, 'red') - @classmethod def success(cls, message): """Prints the specified message with green text as well as to a file. @@ -54,18 +45,15 @@ def success(cls, message): """ cprint(message, 'green') - @classmethod - def verbose(cls, message, is_verbose): + def verbose(cls, message): """Prints the specified message if we're running in 'verbose' mode, and always logs it. Args: message: A str representing the message to log. - is_verbose: A bool that indicates whether or not the message should be - printed to stdout. """ - if is_verbose: + if cls.is_verbose: print message @@ -101,6 +89,6 @@ def remote_log_tools_state(cls, options, my_id, state, version): conn.close() except Exception as exception: cls.verbose("Unable to log {0} state: saw exception {1}".format(state, - str(exception)), options.verbose) + str(exception))) return params diff --git a/appscale/tools/appscale_stats.py b/appscale/tools/appscale_stats.py index 8a558e19..1ef0ec46 100644 --- a/appscale/tools/appscale_stats.py +++ b/appscale/tools/appscale_stats.py @@ -114,8 +114,7 @@ def show_stats(options): node_headers, node_stats = get_node_stats_rows( raw_node_stats=raw_node_stats, all_roles=all_roles, - specified_roles=options.roles, - verbose=options.verbose + specified_roles=options.roles ) print_table( table_name="NODE STATISTICS", @@ -169,7 +168,7 @@ def show_stats(options): if process_failures: failures["processes"] = process_failures - if options.verbose: + if AppScaleLogger.is_verbose: # Additionally render detailed processes stats table in verbose mode order_columns_map = { "name": PROCESSES_NAME_COLUMN_VERBOSE_NUMBER, @@ -203,7 +202,6 @@ def show_stats(options): # Prepare proxies stats table proxy_headers, proxy_stats = get_proxy_stats_rows( raw_proxy_stats=raw_proxy_stats, - verbose=options.verbose, apps_filter=options.apps_only ) proxy_stats = sort_proxy_stats_rows( @@ -247,14 +245,12 @@ def render_loadavg(loadavg): ]) -def render_partitions(partitions, verbose): +def render_partitions(partitions): """ Renders partitions information. Args: partitions: A dict representing partition values. - verbose: A boolean - render all partitions if True, - add only three the most used partitions if False. Returns: A string with information about partition values @@ -275,7 +271,7 @@ def render_partitions(partitions, verbose): for part in part_list ] - if not verbose and len(partitions_info) > 3: + if not AppScaleLogger.is_verbose and len(partitions_info) > 3: partitions_info = partitions_info[:3] + ["..."] return ", ".join(partitions_info) @@ -356,7 +352,7 @@ def get_roles(keyname): return roles_data -def get_node_stats_rows(raw_node_stats, all_roles, specified_roles, verbose): +def get_node_stats_rows(raw_node_stats, all_roles, specified_roles): """ Obtains useful information from node statistics and returns: PRIVATE IP, AVAILABLE MEMORY, LOADAVG, PARTITIONS USAGE, ROLES values. @@ -367,8 +363,6 @@ def get_node_stats_rows(raw_node_stats, all_roles, specified_roles, verbose): all_roles: A dict in which each key is an ip and value is a role list. specified_roles: A list representing specified roles that nodes should contain. - verbose: A boolean - add all partitions if True, - add only three the most used partitions if False. Returns: A list of node statistics headers. @@ -397,7 +391,7 @@ def get_node_stats_rows(raw_node_stats, all_roles, specified_roles, verbose): styled(render_memory(memory=node["memory"]), "bold", if_=is_master), styled(render_loadavg(loadavg=node["loadavg"]), "bold", if_=is_master), styled( - render_partitions(partitions=node["partitions_dict"], verbose=verbose), + render_partitions(partitions=node["partitions_dict"]), "bold", if_=is_master ), styled(u" ".join(ip_roles), "bold", if_=is_master) @@ -514,7 +508,7 @@ def get_summary_process_stats_rows(raw_process_stats, raw_node_stats): return process_stats_headers, process_stats -def get_proxy_stats_rows(raw_proxy_stats, verbose, apps_filter): +def get_proxy_stats_rows(raw_proxy_stats, apps_filter): """ Obtains useful information from proxy statistics and returns: SERVICE (ID), UNIQUE MEMORY SUM (MB), CPU PER 1 PROCESS (%), @@ -523,14 +517,13 @@ def get_proxy_stats_rows(raw_proxy_stats, verbose, apps_filter): Args: raw_proxy_stats: A dict in which each key is an ip and value is a dict of useful information. - verbose: A boolean - verbose or not verbose mode. apps_filter: A boolean - show all services or applications only. Returns: A list of proxy statistics headers. A list of proxy statistics. """ - if verbose: + if AppScaleLogger.is_verbose: headers = [ "SERVICE (ID)", "SERVERS | DOWN", "RATE | REQ TOTAL", "5xx | 4xx", "BYTES IN | BYTES OUT", "QCUR | SCUR", "QTIME | RTIME" @@ -592,7 +585,7 @@ def get_proxy_stats_rows(raw_proxy_stats, verbose, apps_filter): ) ] - if verbose: + if AppScaleLogger.is_verbose: proxy_row.append("{bin} | {bout}".format(**value)) proxy_row.append("{qcur} | {scur}".format(**value)) if "qtime" in value: diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index b469503e..8b36f1a0 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -108,7 +108,7 @@ def add_instances(cls, options): ips_to_check.extend(ip_group) for ip in ips_to_check: # throws a ShellException if the SSH key doesn't work - RemoteHelper.ssh(ip, options.keyname, "ls", options.verbose) + RemoteHelper.ssh(ip, options.keyname, "ls") # Finally, find an AppController and send it a message to add # the given nodes with the new roles. @@ -137,15 +137,14 @@ def add_keypair(cls, options): AppScaleException: If any of the machines named in the ips_layout are not running, or do not have the SSH daemon running. """ - LocalState.require_ssh_commands(options.auto, options.verbose) + LocalState.require_ssh_commands(options.auto) LocalState.make_appscale_directory() path = LocalState.LOCAL_APPSCALE_PATH + options.keyname if options.add_to_existing: private_key = path else: - _, private_key = LocalState.generate_rsa_key(options.keyname, - options.verbose) + _, private_key = LocalState.generate_rsa_key(options.keyname) if options.auto: if 'root_password' in options: @@ -162,8 +161,7 @@ def add_keypair(cls, options): all_ips = [node.public_ip for node in node_layout.nodes] for ip in all_ips: # first, make sure ssh is actually running on the host machine - if not RemoteHelper.is_port_open(ip, RemoteHelper.SSH_PORT, - options.verbose): + if not RemoteHelper.is_port_open(ip, RemoteHelper.SSH_PORT): raise AppScaleException("SSH does not appear to be running at {0}. " \ "Is the machine at {0} up and running? Make sure your IPs are " \ "correct!".format(ip)) @@ -172,10 +170,9 @@ def add_keypair(cls, options): AppScaleLogger.log("Executing ssh-copy-id for host: {0}".format(ip)) if options.auto: LocalState.shell("{0} root@{1} {2} {3}".format(cls.EXPECT_SCRIPT, ip, - private_key, password), options.verbose) + private_key, password)) else: - LocalState.shell("ssh-copy-id -i {0} root@{1}".format(private_key, ip), - options.verbose) + LocalState.shell("ssh-copy-id -i {0} root@{1}".format(private_key, ip)) AppScaleLogger.success("Generated a new SSH key for this deployment " + \ "at {0}".format(private_key)) @@ -212,7 +209,7 @@ def print_cluster_status(cls, options): nodes = [NodeStats(ip, node) for ip, node in node_stats.iteritems() if node] invisible_nodes = [ip for ip, node in node_stats.iteritems() if not node] - if options.verbose: + if AppScaleLogger.is_verbose: AppScaleLogger.log("-"*76) cls._print_nodes_info(nodes, invisible_nodes) cls._print_roles_info(nodes) @@ -482,16 +479,14 @@ def gather_logs(cls, options): try: RemoteHelper.scp_remote_to_local( - public_ip, options.keyname, log_path['remote'], - sub_dir, options.verbose + public_ip, options.keyname, log_path['remote'], sub_dir ) except ShellException as shell_exception: failures = True AppScaleLogger.warn('Unable to collect logs from {} for host {}'. format(log_path['remote'], public_ip)) AppScaleLogger.verbose( - 'Encountered exception: {}'.format(str(shell_exception)), - options.verbose) + 'Encountered exception: {}'.format(str(shell_exception))) if failures: AppScaleLogger.log("Done copying to {}. There were failures while " @@ -555,8 +550,7 @@ def relocate_app(cls, options): AppScaleLogger.success( 'Successfully issued request to move {0} to ports {1} and {2}'.format( options.appname, options.http_port, options.https_port)) - RemoteHelper.sleep_until_port_is_open( - login_host, options.http_port, options.verbose) + RemoteHelper.sleep_until_port_is_open(login_host, options.http_port) AppScaleLogger.success( 'Your app serves unencrypted traffic at: http://{0}:{1}'.format( login_host, options.http_port)) @@ -799,19 +793,18 @@ def run_instances(cls, options): # Enables root logins and SSH access on the head node. RemoteHelper.enable_root_ssh(options, head_node.public_ip) - AppScaleLogger.verbose("Node Layout: {}".format(node_layout.to_list()), - options.verbose) + AppScaleLogger.verbose("Node Layout: {}".format(node_layout.to_list())) # Ensure all nodes are compatible. RemoteHelper.ensure_machine_is_compatible( - head_node.public_ip, options.keyname, options.verbose) + head_node.public_ip, options.keyname) # Use rsync to move custom code into the deployment. if options.rsync_source: AppScaleLogger.log("Copying over local copy of AppScale from {0}". format(options.rsync_source)) RemoteHelper.rsync_files(head_node.public_ip, options.keyname, - options.rsync_source, options.verbose) + options.rsync_source) # Start services on head node. RemoteHelper.start_head_node(options, my_id, node_layout) @@ -823,7 +816,7 @@ def run_instances(cls, options): # Copy the locations.json to the head node RemoteHelper.copy_local_metadata(node_layout.head_node().public_ip, - options.keyname, options.verbose) + options.keyname) # Wait for services on head node to start. secret_key = LocalState.get_secret_key(options.keyname) @@ -838,7 +831,7 @@ def run_instances(cls, options): AppScaleLogger.warn('Unable to initialize AppController: {}'. format(socket_error.message)) message = RemoteHelper.collect_appcontroller_crashlog( - head_node, options.keyname, options.verbose) + head_node, options.keyname) raise AppControllerException(message) # Set up admin account. @@ -872,7 +865,7 @@ def run_instances(cls, options): raise AppControllerException('login property not found') RemoteHelper.sleep_until_port_is_open( - login_host, RemoteHelper.APP_DASHBOARD_PORT, options.verbose) + login_host, RemoteHelper.APP_DASHBOARD_PORT) AppScaleLogger.success("AppScale successfully started!") AppScaleLogger.success( @@ -925,8 +918,7 @@ def terminate_instances(cls, options): # Stop gracefully the AppScale deployment. try: RemoteHelper.terminate_virtualized_cluster(options.keyname, - options.clean, - options.verbose) + options.clean) except (IOError, AppScaleException, AppControllerException, BadConfigurationException) as e: if not (infrastructure in InfrastructureAgentFactory.VALID_AGENTS and @@ -936,7 +928,7 @@ def terminate_instances(cls, options): if options.test: AppScaleLogger.warn(e) else: - AppScaleLogger.verbose(e, options.verbose) + AppScaleLogger.verbose(e) if isinstance(e, AppControllerException): response = raw_input( 'AppScale may not have shut down properly, are you sure you want ' @@ -954,8 +946,7 @@ def terminate_instances(cls, options): # asked. if (infrastructure in InfrastructureAgentFactory.VALID_AGENTS and options.terminate): - RemoteHelper.terminate_cloud_infrastructure(options.keyname, - options.verbose) + RemoteHelper.terminate_cloud_infrastructure(options.keyname) elif infrastructure in InfrastructureAgentFactory.VALID_AGENTS and not \ options.terminate: AppScaleLogger.log("AppScale did not terminate any of your cloud " @@ -978,13 +969,11 @@ def upload_app(cls, options): """ custom_service_yaml = None if cls.TAR_GZ_REGEX.search(options.file): - file_location = LocalState.extract_tgz_app_to_dir(options.file, - options.verbose) + file_location = LocalState.extract_tgz_app_to_dir(options.file) created_dir = True version = Version.from_tar_gz(options.file) elif cls.ZIP_REGEX.search(options.file): - file_location = LocalState.extract_zip_app_to_dir(options.file, - options.verbose) + file_location = LocalState.extract_zip_app_to_dir(options.file) created_dir = True version = Version.from_zip(options.file) elif os.path.isdir(options.file): @@ -1038,7 +1027,7 @@ def upload_app(cls, options): admin_client = AdminClient(head_node_public_ip, secret_key) remote_file_path = RemoteHelper.copy_app_to_host( - file_location, version.project_id, options.keyname, options.verbose, + file_location, version.project_id, options.keyname, extras, custom_service_yaml) AppScaleLogger.log( @@ -1077,7 +1066,7 @@ def upload_app(cls, options): @classmethod - def update_dispatch(cls, source_location, keyname, project_id, is_verbose): + def update_dispatch(cls, source_location, keyname, project_id): """ Updates an application's dispatch routing rules from the configuration file. @@ -1134,7 +1123,7 @@ def update_dispatch(cls, source_location, keyname, project_id, is_verbose): AppScaleLogger.verbose( "The following dispatchRules have been applied to your application's " - "configuration : {}".format(dispatch_rules), is_verbose) + "configuration : {}".format(dispatch_rules)) AppScaleLogger.success('Dispatch has been updated for {}'.format( version.project_id)) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 5c5b9016..b3b2b4ba 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -210,7 +210,7 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): "autoscale": str(options.autoscale), "clear_datastore": str(False), "user_commands": json.dumps(options.user_commands), - "verbose": str(options.verbose), + "verbose": str(AppScaleLogger.is_verbose), "flower_password": options.flower_password, "default_max_appserver_memory": str(options.default_max_appserver_memory), "fdb_clusterfile_content": options.fdb_clusterfile_content @@ -298,21 +298,19 @@ def obscure_str(cls, str_to_obscure): @classmethod - def generate_ssl_cert(cls, keyname, is_verbose): + def generate_ssl_cert(cls, keyname): """Generates a self-signed SSL certificate that AppScale services can use to encrypt traffic with. Args: keyname: A str representing the SSH keypair name used for this AppScale deployment. - is_verbose: A bool that indicates if we want to print out the certificate - generation to stdout or not. """ cls.shell("openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 " + \ "-subj '/C=US/ST=Foo/L=Bar/O=AppScale/CN=appscale.com' " + \ "-keyout {0} -out {1}". \ format(LocalState.get_private_key_location(keyname), - LocalState.get_certificate_location(keyname)), is_verbose, stdin=None) + LocalState.get_certificate_location(keyname)), stdin=None) @classmethod @@ -938,15 +936,12 @@ def cleanup_appscale_files(cls, keyname, remove_locations=True): @classmethod - def shell(cls, command, is_verbose, num_retries=DEFAULT_NUM_RETRIES, - stdin=None): + def shell(cls, command, num_retries=DEFAULT_NUM_RETRIES, stdin=None): """Executes a command on this machine, retrying it up to five times if it initially fails. Args: command: A str representing the command to execute. - is_verbose: A bool that indicates if we should print the command we are - executing to stdout. num_retries: The number of times we should try to execute the given command before aborting. stdin: A str that is passes as standard input to the process @@ -960,21 +955,20 @@ def shell(cls, command, is_verbose, num_retries=DEFAULT_NUM_RETRIES, tries_left = num_retries try: while tries_left: - AppScaleLogger.verbose("shell> {0}".format(command), is_verbose) + AppScaleLogger.verbose("shell> {0}".format(command)) the_temp_file = tempfile.NamedTemporaryFile() if stdin is not None: stdin_strio = tempfile.TemporaryFile() stdin_strio.write(stdin) stdin_strio.seek(0) - AppScaleLogger.verbose(" stdin str: {0}"\ - .format(stdin), is_verbose) + AppScaleLogger.verbose(" stdin str: {0}".format(stdin)) result = subprocess.Popen(command, shell=True, stdout=the_temp_file, stdin=stdin_strio, stderr=subprocess.STDOUT) else: result = subprocess.Popen(command, shell=True, stdout=the_temp_file, stderr=subprocess.STDOUT) - AppScaleLogger.verbose(" stdout buffer: {0}"\ - .format(the_temp_file.name), is_verbose) + AppScaleLogger.verbose(" stdout buffer: {0}" + .format(the_temp_file.name)) result.wait() if stdin is not None: stdin_strio.close() @@ -986,8 +980,8 @@ def shell(cls, command, is_verbose, num_retries=DEFAULT_NUM_RETRIES, tries_left -= 1 if tries_left: the_temp_file.close() - AppScaleLogger.verbose("Command failed. Trying again momentarily." \ - .format(command), is_verbose) + AppScaleLogger.verbose("Command failed. Trying again momentarily." + .format(command)) else: the_temp_file.seek(0) output = the_temp_file.read() @@ -1009,15 +1003,13 @@ def shell(cls, command, is_verbose, num_retries=DEFAULT_NUM_RETRIES, @classmethod - def require_ssh_commands(cls, needs_expect, is_verbose): + def require_ssh_commands(cls, needs_expect): """Checks to make sure the commands needed to set up passwordless SSH access are installed on this machine. Args: needs_expect: A bool that indicates if we should also check for the 'expect' command. - is_verbose: A bool that indicates if we should print how we check for - each command to stdout. Raises: BadConfigurationException: If any of the required commands aren't present on this machine. @@ -1028,22 +1020,20 @@ def require_ssh_commands(cls, needs_expect, is_verbose): for command in required_commands: try: - cls.shell("hash {0}".format(command), is_verbose) + cls.shell("hash {0}".format(command)) except ShellException: raise BadConfigurationException("Couldn't find {0} in your PATH." .format(command)) @classmethod - def generate_rsa_key(cls, keyname, is_verbose): + def generate_rsa_key(cls, keyname): """Generates a new RSA public and private keypair, and saves it to the local filesystem. Args: keyname: The SSH keypair name that uniquely identifies this AppScale deployment. - is_verbose: A bool that indicates if we should print the ssh-keygen - command to stdout. """ private_key = cls.LOCAL_APPSCALE_PATH + keyname public_key = private_key + ".pub" @@ -1054,7 +1044,7 @@ def generate_rsa_key(cls, keyname, is_verbose): if os.path.exists(private_key): os.remove(private_key) - cls.shell("ssh-keygen -t rsa -N '' -f {0}".format(private_key), is_verbose) + cls.shell("ssh-keygen -t rsa -N '' -f {0}".format(private_key)) os.chmod(public_key, 0600) os.chmod(private_key, 0600) shutil.copy(private_key, private_key + ".key") @@ -1062,41 +1052,37 @@ def generate_rsa_key(cls, keyname, is_verbose): @classmethod - def extract_tgz_app_to_dir(cls, tar_location, is_verbose): + def extract_tgz_app_to_dir(cls, tar_location): """Extracts the given tar.gz file to a randomly generated location and returns that location. Args: archive_location: The location on the local filesystem where the tar.gz file to extract can be found. - is_verbose: A bool that indicates if we should print the command we - execute to stdout. Returns: The location on the local filesystem where the file was extracted to. """ - return cls.extract_app_to_dir(tar_location, "tar zxvf", is_verbose) + return cls.extract_app_to_dir(tar_location, "tar zxvf") @classmethod - def extract_zip_app_to_dir(cls, zip_location, is_verbose): + def extract_zip_app_to_dir(cls, zip_location): """Extracts the given zip file to a randomly generated location and returns that location. Args: archive_location: The location on the local filesystem where the zip file to extract can be found. - is_verbose: A bool that indicates if we should print the command we - execute to stdout. Returns: The location on the local filesystem where the file was extracted to. """ - return cls.extract_app_to_dir(zip_location, "unzip", is_verbose) + return cls.extract_app_to_dir(zip_location, "unzip") @classmethod - def extract_app_to_dir(cls, archive_location, extract_command, is_verbose): + def extract_app_to_dir(cls, archive_location, extract_command): """Extracts the given file to a randomly generated location and returns that location. @@ -1105,8 +1091,6 @@ def extract_app_to_dir(cls, archive_location, extract_command, is_verbose): to extract can be found. extract_command: The command and flags necessary to extract the archived file. - is_verbose: A bool that indicates if we should print the command we - execute to stdout. Returns: The location on the local filesystem where the file was extracted to. @@ -1116,7 +1100,7 @@ def extract_app_to_dir(cls, archive_location, extract_command, is_verbose): os.mkdir(extracted_location) cls.shell("cd {0} && {1} '{2}'".format(extracted_location, extract_command, - os.path.abspath(archive_location)), is_verbose) + os.path.abspath(archive_location))) file_list = os.listdir(extracted_location) if len(file_list) > 0: diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index 0fb17379..56cd5c96 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -14,6 +14,8 @@ # AppScale-specific imports +from appscale.tools.appscale_logger import AppScaleLogger + try: from appscale.agents.azure_agent import AzureAgent except ImportError: @@ -164,6 +166,7 @@ def __init__(self, argv, function): if self.args.version: raise SystemExit(APPSCALE_VERSION) + AppScaleLogger.is_verbose = self.args.verbose self.validate_allowed_flags(function) diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index c5be1c20..bd1759ae 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -248,11 +248,10 @@ def enable_root_ssh(cls, options, public_ip): public_ip: The IP address of the machine. """ AppScaleLogger.log("Enabling root ssh on {0}".format(public_ip)) - cls.sleep_until_port_is_open(public_ip, cls.SSH_PORT, options.verbose) + cls.sleep_until_port_is_open(public_ip, cls.SSH_PORT) - cls.enable_root_login(public_ip, options.keyname, options.infrastructure, - options.verbose) - cls.copy_ssh_keys_to_node(public_ip, options.keyname, options.verbose) + cls.enable_root_login(public_ip, options.keyname, options.infrastructure) + cls.copy_ssh_keys_to_node(public_ip, options.keyname) @classmethod def start_head_node(cls, options, my_id, node_layout): @@ -278,8 +277,7 @@ def start_head_node(cls, options, my_id, node_layout): The message in this exception indicates why the crash occurred. """ secret_key = LocalState.generate_secret_key(options.keyname) - AppScaleLogger.verbose("Secret key is {0}". - format(secret_key), options.verbose) + AppScaleLogger.verbose("Secret key is {0}".format(secret_key)) head_node = node_layout.head_node().public_ip AppScaleLogger.log("Log in to your head node: ssh -i {0} root@{1}".format( @@ -305,10 +303,9 @@ def start_head_node(cls, options, my_id, node_layout): cls.copy_deployment_credentials(head_node, options) - cls.run_user_commands(head_node, options.user_commands, options.keyname, - options.verbose) + cls.run_user_commands(head_node, options.user_commands, options.keyname) - cls.start_remote_appcontroller(head_node, options.keyname, options.verbose) + cls.start_remote_appcontroller(head_node, options.keyname) AppScaleLogger.log("Head node successfully initialized at {0}.". format(head_node)) AppScaleLogger.remote_log_tools_state( @@ -317,8 +314,7 @@ def start_head_node(cls, options, my_id, node_layout): # Construct serverside compatible parameters. deployment_params = LocalState.generate_deployment_params( options, node_layout, additional_params) - AppScaleLogger.verbose(str(LocalState.obscure_dict(deployment_params)), - options.verbose) + AppScaleLogger.verbose(str(LocalState.obscure_dict(deployment_params))) acc = AppControllerClient(head_node, secret_key) try: @@ -327,7 +323,7 @@ def start_head_node(cls, options, my_id, node_layout): AppScaleLogger.warn(u'Saw Exception while setting AC parameters: {0}'. format(str(exception))) message = RemoteHelper.collect_appcontroller_crashlog( - head_node, options.keyname, options.verbose) + head_node, options.keyname) raise AppControllerException(message) @@ -356,25 +352,22 @@ def spawn_nodes_in_cloud(cls, agent, params, count=1, load_balancer=False): return instance_ids, public_ips, private_ips @classmethod - def sleep_until_port_is_open(cls, host, port, is_verbose): + def sleep_until_port_is_open(cls, host, port): """Queries the given host to see if the named port is open, and if not, waits until it is. Args: host: A str representing the host whose port we should be querying. port: An int representing the port that should eventually be open. - verbose: A bool that indicates if we should print failure messages to - stdout (e.g., connection refused messages that can occur when we wait - for services to come up). Raises: TimeoutException if the port does not open in a certain amount of time. """ sleep_time = cls.MAX_WAIT_TIME while sleep_time > 0: - if cls.is_port_open(host, port, is_verbose): + if cls.is_port_open(host, port): return - AppScaleLogger.verbose("Waiting {2} second(s) for {0}:{1} to open".\ - format(host, port, cls.WAIT_TIME), is_verbose) + AppScaleLogger.verbose("Waiting {2} second(s) for {0}:{1} to open" + .format(host, port, cls.WAIT_TIME)) time.sleep(cls.WAIT_TIME) sleep_time -= cls.WAIT_TIME raise TimeoutException("Port {}:{} did not open in time. " @@ -382,15 +375,12 @@ def sleep_until_port_is_open(cls, host, port, is_verbose): @classmethod - def is_port_open(cls, host, port, is_verbose): + def is_port_open(cls, host, port): """Queries the given host to see if the named port is open. Args: host: A str representing the host whose port we should be querying. port: An int representing the port that should eventually be open. - verbose: A bool that indicates if we should print failure messages to - stdout (e.g., connection refused messages that can occur when we wait - for services to come up). Returns: True if the port is open, False otherwise. """ @@ -399,11 +389,11 @@ def is_port_open(cls, host, port, is_verbose): sock.connect((host, port)) return True except Exception as exception: - AppScaleLogger.verbose(str(exception), is_verbose) + AppScaleLogger.verbose(str(exception)) return False @classmethod - def merge_authorized_keys(cls, host, keyname, user, is_verbose): + def merge_authorized_keys(cls, host, keyname, user): """ Adds the contents of the user's authorized_keys file to the root's authorized_keys file. @@ -411,34 +401,32 @@ def merge_authorized_keys(cls, host, keyname, user, is_verbose): host: A str representing the host to enable root logins on. keyname: A str representing the name of the SSH keypair to login with. user: A str representing the name of the user to login as. - is_verbose: A bool indicating if we should print the command we execute to - enable root login to stdout. """ AppScaleLogger.log('Root login not enabled for {} - enabling it ' 'now.'.format(host)) create_root_keys = 'sudo touch /root/.ssh/authorized_keys' - cls.ssh(host, keyname, create_root_keys, is_verbose, user=user) + cls.ssh(host, keyname, create_root_keys, user=user) set_permissions = 'sudo chmod 600 /root/.ssh/authorized_keys' - cls.ssh(host, keyname, set_permissions, is_verbose, user=user) + cls.ssh(host, keyname, set_permissions, user=user) - temp_file = cls.ssh(host, keyname, 'mktemp', is_verbose, user=user) + temp_file = cls.ssh(host, keyname, 'mktemp', user=user) merge_to_tempfile = 'sudo sort -u ~/.ssh/authorized_keys '\ '/root/.ssh/authorized_keys -o {}'.format(temp_file) - cls.ssh(host, keyname, merge_to_tempfile, is_verbose, user=user) + cls.ssh(host, keyname, merge_to_tempfile, user=user) overwrite_root_keys = "sudo sed -n '/.*Please login/d; "\ "w/root/.ssh/authorized_keys' {}".format(temp_file) - cls.ssh(host, keyname, overwrite_root_keys, is_verbose, user=user) + cls.ssh(host, keyname, overwrite_root_keys, user=user) remove_tempfile = 'rm -f {0}'.format(temp_file) - cls.ssh(host, keyname, remove_tempfile, is_verbose, user=user) + cls.ssh(host, keyname, remove_tempfile, user=user) return @classmethod - def enable_root_login(cls, host, keyname, infrastructure, is_verbose): + def enable_root_login(cls, host, keyname, infrastructure): """Logs into the named host and alters its ssh configuration to enable the root user to directly log in. @@ -447,21 +435,18 @@ def enable_root_login(cls, host, keyname, infrastructure, is_verbose): keyname: A str representing the name of the SSH keypair to login with. infrastructure: A str representing the name of the cloud infrastructure we're running on. - is_verbose: A bool indicating if we should print the command we execute to - enable root login to stdout. """ # First, see if we need to enable root login at all (some VMs have it # already enabled). try: if infrastructure == "azure": - cls.merge_authorized_keys(host, keyname, 'azureuser', is_verbose) - output = cls.ssh(host, keyname, 'ls', is_verbose, user='root') + cls.merge_authorized_keys(host, keyname, 'azureuser') + output = cls.ssh(host, keyname, 'ls', user='root') except ShellException as exception: # Google Compute Engine creates a user with the same name as the currently # logged-in user, so log in as that user to enable root login. if infrastructure == "gce": - cls.merge_authorized_keys(host, keyname, getpass.getuser(), - is_verbose) + cls.merge_authorized_keys(host, keyname, getpass.getuser()) return else: raise exception @@ -471,13 +456,13 @@ def enable_root_login(cls, host, keyname, infrastructure, is_verbose): match = re.match(cls.LOGIN_AS_UBUNTU_USER, output) if match: user = match.group(1) - cls.merge_authorized_keys(host, keyname, user, is_verbose) + cls.merge_authorized_keys(host, keyname, user) else: AppScaleLogger.log("Root login already enabled for {}.".format(host)) @classmethod - def ssh(cls, host, keyname, command, is_verbose, user='root', + def ssh(cls, host, keyname, command, user='root', num_retries=LocalState.DEFAULT_NUM_RETRIES): """Logs into the named host and executes the given command. @@ -485,8 +470,6 @@ def ssh(cls, host, keyname, command, is_verbose, user='root', host: A str representing the machine that we should log into. keyname: A str representing the name of the SSH keypair to log in with. command: A str representing what to execute on the remote host. - is_verbose: A bool indicating if we should print the ssh command to - stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the remote command and a str @@ -494,13 +477,12 @@ def ssh(cls, host, keyname, command, is_verbose, user='root', """ ssh_key = LocalState.get_key_path_from_name(keyname) return LocalState.shell("ssh -F /dev/null -i {0} {1} {2}@{3} bash".format( - ssh_key, cls.SSH_OPTIONS, user, host), - is_verbose, num_retries, stdin=command) + ssh_key, cls.SSH_OPTIONS, user, host), num_retries, stdin=command) @classmethod - def scp(cls, host, keyname, source, dest, is_verbose, user='root', - num_retries=LocalState.DEFAULT_NUM_RETRIES): + def scp(cls, host, keyname, source, dest, user='root', + num_retries=LocalState.DEFAULT_NUM_RETRIES): """Securely copies a file from this machine to the named machine. Args: @@ -510,8 +492,6 @@ def scp(cls, host, keyname, source, dest, is_verbose, user='root', file should be copied from. dest: A str representing the path on the remote machine where the file should be copied to. - is_verbose: A bool that indicates if we should print the scp command to - stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the secure copy and a str @@ -521,12 +501,11 @@ def scp(cls, host, keyname, source, dest, is_verbose, user='root', command = "scp -r -i {0} {1} '{2}' {3}@{4}:'{5}'".format( ssh_key, cls.SSH_OPTIONS, source, user, host, dest.replace(" ", "\ ") ) - return LocalState.shell(command, is_verbose, num_retries) + return LocalState.shell(command, num_retries) @classmethod - def scp_remote_to_local(cls, host, keyname, source, dest, is_verbose, - user='root'): + def scp_remote_to_local(cls, host, keyname, source, dest, user='root'): """Securely copies a file from a remote machine to this machine. Args: @@ -536,8 +515,6 @@ def scp_remote_to_local(cls, host, keyname, source, dest, is_verbose, file should be copied from. dest: A str representing the path on the local machine where the file should be copied to. - is_verbose: A bool that indicates if we should print the scp command to - stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the secure copy and a str @@ -547,11 +524,11 @@ def scp_remote_to_local(cls, host, keyname, source, dest, is_verbose, command = "scp -r -i {0} {1} {2}@{3}:'{4}' '{5}'".format( ssh_key, cls.SSH_OPTIONS, user, host, source.replace(" ", "\ "), dest ) - return LocalState.shell(command, is_verbose) + return LocalState.shell(command) @classmethod - def copy_ssh_keys_to_node(cls, host, keyname, is_verbose): + def copy_ssh_keys_to_node(cls, host, keyname): """Sets the given SSH keypair as the default key for the named host, enabling it to log into other machines in the AppScale deployment without being prompted for a password or explicitly requiring the key to be @@ -560,17 +537,14 @@ def copy_ssh_keys_to_node(cls, host, keyname, is_verbose): Args: host: A str representing the machine that we should log into. keyname: A str representing the name of the SSH keypair to log in with. - is_verbose: A bool that indicates if we should print the SCP commands - needed to copy the SSH keys over to stdout. """ ssh_key = LocalState.get_key_path_from_name(keyname) - cls.scp(host, keyname, ssh_key, '/root/.ssh/id_dsa', is_verbose) - cls.scp(host, keyname, ssh_key, '/root/.ssh/id_rsa', is_verbose) - cls.scp(host, keyname, ssh_key, '{}/{}.key'.format(cls.CONFIG_DIR, keyname), - is_verbose) + cls.scp(host, keyname, ssh_key, '/root/.ssh/id_dsa') + cls.scp(host, keyname, ssh_key, '/root/.ssh/id_rsa') + cls.scp(host, keyname, ssh_key, '{}/{}.key'.format(cls.CONFIG_DIR, keyname)) @classmethod - def ensure_machine_is_compatible(cls, host, keyname, is_verbose): + def ensure_machine_is_compatible(cls, host, keyname): """Verifies that the specified host has AppScale installed on it. This also validates that the host has the right version of AppScale @@ -581,14 +555,12 @@ def ensure_machine_is_compatible(cls, host, keyname, is_verbose): AppScale-compatible. keyname: A str representing the SSH keypair name that can log into the named host. - is_verbose: A bool that indicates if we should print the commands we - execute to validate the machine to stdout. Raises: AppScaleException: If the specified host does not have AppScale installed, or has the wrong version of AppScale installed. """ # First, make sure the image is an AppScale image. - remote_version = cls.get_host_appscale_version(host, keyname, is_verbose) + remote_version = cls.get_host_appscale_version(host, keyname) if not remote_version: raise AppScaleException("The machine at {0} does not have " \ "AppScale installed.".format(host)) @@ -603,7 +575,7 @@ def ensure_machine_is_compatible(cls, host, keyname, is_verbose): @classmethod - def does_host_have_location(cls, host, keyname, location, is_verbose): + def does_host_have_location(cls, host, keyname, location): """Logs into the specified host with the given keyname and checks to see if the named directory exists there. @@ -614,21 +586,19 @@ def does_host_have_location(cls, host, keyname, location, is_verbose): the specified machine. location: The path on the remote filesystem that we should be checking for. - is_verbose: A bool that indicates if we should print the command we - execute to check the remote host's location to stdout. Returns: True if the remote host has a file or directory at the specified location, False otherwise. """ try: - cls.ssh(host, keyname, 'ls {0}'.format(location), is_verbose) + cls.ssh(host, keyname, 'ls {0}'.format(location)) return True except ShellException: return False @classmethod - def get_host_appscale_version(cls, host, keyname, is_verbose): + def get_host_appscale_version(cls, host, keyname): """Logs into the specified host with the given keyname and checks to see what version of AppScale is installed there. @@ -637,8 +607,6 @@ def get_host_appscale_version(cls, host, keyname, is_verbose): machine. keyname: A str representing the name of the SSH keypair that can log into the specified machine. - is_verbose: A bool that indicates if we should print the command we - execute to check the remote host's location to stdout. Returns: A str containing the version of AppScale installed on host, or None if (1) AppScale isn't installed on host, or (2) host has more than one @@ -647,7 +615,7 @@ def get_host_appscale_version(cls, host, keyname, is_verbose): remote_version_file = '{}/{}'.format(cls.CONFIG_DIR, 'VERSION') try: version_output = cls.ssh( - host, keyname, 'cat {}'.format(remote_version_file), is_verbose) + host, keyname, 'cat {}'.format(remote_version_file)) except ShellException: return None version = version_output.split('AppScale version')[1].strip() @@ -655,7 +623,7 @@ def get_host_appscale_version(cls, host, keyname, is_verbose): @classmethod - def rsync_files(cls, host, keyname, local_appscale_dir, is_verbose): + def rsync_files(cls, host, keyname, local_appscale_dir): """Copies over an AppScale source directory from this machine to the specified host. @@ -666,8 +634,6 @@ def rsync_files(cls, host, keyname, local_appscale_dir, is_verbose): the specified machine. local_appscale_dir: A str representing the path on the local filesystem where the AppScale source to copy over can be found. - is_verbose: A bool that indicates if we should print the rsync commands - we exec to stdout. Raises: BadConfigurationException: If local_appscale_dir does not exist locally, or if any of the standard AppScale module folders do not exist. @@ -681,7 +647,7 @@ def rsync_files(cls, host, keyname, local_appscale_dir, is_verbose): "--exclude='AppDB/logs/*' " \ "--exclude='AppDB/cassandra/cassandra/*' " \ "{2}/* root@{3}:/root/appscale/".format(ssh_key, cls.SSH_OPTIONS, - local_path, host), is_verbose) + local_path, host)) @classmethod def copy_deployment_credentials(cls, host, options): @@ -696,28 +662,28 @@ def copy_deployment_credentials(cls, host, options): """ local_secret_key = LocalState.get_secret_key_location(options.keyname) cls.scp(host, options.keyname, local_secret_key, - '{}/secret.key'.format(cls.CONFIG_DIR), options.verbose) + '{}/secret.key'.format(cls.CONFIG_DIR)) local_ssh_key = LocalState.get_key_path_from_name(options.keyname) cls.scp(host, options.keyname, local_ssh_key, - '{}/ssh.key'.format(cls.CONFIG_DIR), options.verbose) + '{}/ssh.key'.format(cls.CONFIG_DIR)) - LocalState.generate_ssl_cert(options.keyname, options.verbose) + LocalState.generate_ssl_cert(options.keyname) local_cert = LocalState.get_certificate_location(options.keyname) cls.scp(host, options.keyname, local_cert, - '{}/certs/mycert.pem'.format(cls.CONFIG_DIR), options.verbose) + '{}/certs/mycert.pem'.format(cls.CONFIG_DIR)) local_private_key = LocalState.get_private_key_location(options.keyname) cls.scp(host, options.keyname, local_private_key, - '{}/certs/mykey.pem'.format(cls.CONFIG_DIR), options.verbose) + '{}/certs/mykey.pem'.format(cls.CONFIG_DIR)) hash_id = subprocess.Popen(["openssl", "x509", "-hash", "-noout", "-in", LocalState.get_certificate_location(options.keyname)], stdout=subprocess.PIPE).communicate()[0] symlink_cert = 'ln -fs {}/certs/mycert.pem /etc/ssl/certs/{}.0'.\ format(cls.CONFIG_DIR, hash_id.rstrip()) - cls.ssh(host, options.keyname, symlink_cert, options.verbose) + cls.ssh(host, options.keyname, symlink_cert) # In Google Compute Engine, we also need to copy over our client_secrets # file and the OAuth2 file that the user has approved for use with their @@ -729,14 +695,14 @@ def copy_deployment_credentials(cls, host, options): raise AppScaleException('{} does not exist.'.format(secrets_location)) secrets_type = GCEAgent.get_secrets_type(secrets_location) cls.scp(host, options.keyname, secrets_location, - '{}/client_secrets.json'.format(cls.CONFIG_DIR), options.verbose) + '{}/client_secrets.json'.format(cls.CONFIG_DIR)) if secrets_type == CredentialTypes.OAUTH: local_oauth = LocalState.get_oauth2_storage_location(options.keyname) cls.scp(host, options.keyname, local_oauth, - '{}/oauth2.dat'.format(cls.CONFIG_DIR), options.verbose) + '{}/oauth2.dat'.format(cls.CONFIG_DIR)) @classmethod - def run_user_commands(cls, host, commands, keyname, is_verbose): + def run_user_commands(cls, host, commands, keyname): """Runs any commands specified by the user before the AppController is started. @@ -746,49 +712,45 @@ def run_user_commands(cls, host, commands, keyname, is_verbose): executed on the remote machine. keyname: A str representing the name of the SSH keypair that can log into the specified host. - is_verbose: A bool that indicates if we should print the commands needed - to start the AppController to stdout. """ if commands: AppScaleLogger.log("Running user-specified commands at {0}".format(host)) for command in commands: - cls.ssh(host, keyname, command, is_verbose) + cls.ssh(host, keyname, command) @classmethod - def start_remote_appcontroller(cls, host, keyname, is_verbose): + def start_remote_appcontroller(cls, host, keyname): """Starts the AppController daemon on the specified host. Args: host: A str representing the host to start the AppController on. keyname: A str representing the name of the SSH keypair that can log into the specified host. - is_verbose: A bool that indicates if we should print the commands needed - to start the AppController to stdout. """ AppScaleLogger.log("Starting AppController at {0}".format(host)) # Remove any previous state. TODO: Don't do this with the tools. cls.ssh(host, keyname, - 'rm -rf {}/appcontroller-state.json'.format(cls.CONFIG_DIR), is_verbose) + 'rm -rf {}/appcontroller-state.json'.format(cls.CONFIG_DIR)) # Remove any monit configuration files from previous AppScale deployments. - cls.ssh(host, keyname, 'rm -rf /etc/monit/conf.d/appscale-*.cfg', is_verbose) + cls.ssh(host, keyname, 'rm -rf /etc/monit/conf.d/appscale-*.cfg') - cls.ssh(host, keyname, 'service monit start', is_verbose) + cls.ssh(host, keyname, 'service monit start') # Start the AppController. - cls.ssh(host, keyname, 'service appscale-controller start', is_verbose) + cls.ssh(host, keyname, 'service appscale-controller start') AppScaleLogger.log("Please wait for the AppController to finish " + \ "pre-processing tasks.") - cls.sleep_until_port_is_open(host, AppControllerClient.PORT, is_verbose) + cls.sleep_until_port_is_open(host, AppControllerClient.PORT) @classmethod - def copy_local_metadata(cls, host, keyname, is_verbose): + def copy_local_metadata(cls, host, keyname): """Copies the locations.json file found locally (which contain metadata about this AppScale deployment) to the specified host. @@ -796,16 +758,14 @@ def copy_local_metadata(cls, host, keyname, is_verbose): host: The machine that we should copy the metadata files to. keyname: The name of the SSH keypair that we can use to log into the given host. - is_verbose: A bool that indicates if we should print the SCP commands we - exec to stdout. """ # and copy the json file if the tools on that box wants to use it cls.scp(host, keyname, LocalState.get_locations_json_location(keyname), - '{}/locations-{}.json'.format(cls.CONFIG_DIR, keyname), is_verbose) + '{}/locations-{}.json'.format(cls.CONFIG_DIR, keyname)) # and copy the secret file if the tools on that box wants to use it cls.scp(host, keyname, LocalState.get_secret_key_location(keyname), - cls.CONFIG_DIR, is_verbose) + cls.CONFIG_DIR) @classmethod @@ -921,7 +881,7 @@ def terminate_cloud_instance(cls, instance_id, options): AppScaleLogger.log("About to terminate instance {0}".format(instance_id)) agent = InfrastructureAgentFactory.create_agent(options.infrastructure) params = agent.get_params_from_args(options) - params['IS_VERBOSE'] = options.verbose + params['IS_VERBOSE'] = AppScaleLogger.is_verbose params[agent.PARAM_INSTANCE_IDS] = [instance_id] agent.terminate_instances(params) agent.cleanup_state(params) @@ -929,13 +889,11 @@ def terminate_cloud_instance(cls, instance_id, options): @classmethod - def terminate_cloud_infrastructure(cls, keyname, is_verbose): + def terminate_cloud_infrastructure(cls, keyname): """Powers off all machines in the currently running AppScale deployment. Args: keyname: The name of the SSH keypair used for this AppScale deployment. - is_verbose: A bool that indicates if we should print the commands executed - to stdout. """ AppScaleLogger.log("About to terminate deployment and instances with " "keyname {0}. Press Ctrl-C to stop.".format(keyname)) @@ -946,7 +904,7 @@ def terminate_cloud_infrastructure(cls, keyname, is_verbose): agent = InfrastructureAgentFactory.create_agent( LocalState.get_infrastructure(keyname)) params = agent.get_cloud_params(keyname) - params['IS_VERBOSE'] = is_verbose + params['IS_VERBOSE'] = AppScaleLogger.is_verbose params['autoscale_agent'] = False # We want to terminate also the pending instances. @@ -960,7 +918,7 @@ def terminate_cloud_infrastructure(cls, keyname, is_verbose): if node.get('disk'): AppScaleLogger.log("Unmounting persistent disk at {0}". format(node['public_ip'])) - cls.unmount_persistent_disk(node['public_ip'], keyname, is_verbose) + cls.unmount_persistent_disk(node['public_ip'], keyname) agent.detach_disk(params, node['disk'], node['instance_id']) # terminate all the machines @@ -979,7 +937,7 @@ def terminate_cloud_infrastructure(cls, keyname, is_verbose): @classmethod - def unmount_persistent_disk(cls, host, keyname, is_verbose): + def unmount_persistent_disk(cls, host, keyname): """Unmounts the persistent disk that was previously mounted on the named machine. @@ -987,26 +945,22 @@ def unmount_persistent_disk(cls, host, keyname, is_verbose): host: A str that names the IP address or FQDN where the machine whose disk needs to be unmounted can be found. keyname: The name of the SSH keypair used for this AppScale deployment. - is_verbose: A bool that indicates if we should print the commands executed - to stdout. """ try: remote_output = cls.ssh(host, keyname, 'umount {0}'.format( - cls.PERSISTENT_MOUNT_POINT), is_verbose) - AppScaleLogger.verbose(remote_output, is_verbose) + cls.PERSISTENT_MOUNT_POINT)) + AppScaleLogger.verbose(remote_output) except ShellException: pass @classmethod - def terminate_virtualized_cluster(cls, keyname, clean, is_verbose): + def terminate_virtualized_cluster(cls, keyname, clean): """Stops all API services running on all nodes in the currently running AppScale deployment. Args: keyname: The name of the SSH keypair used for this AppScale deployment. - is_verbose: A bool that indicates if we should print the commands executed - to stdout. clean: A bool representing whether clean should be ran on the nodes. """ AppScaleLogger.log("Stopping appscale deployment with keyname {0}" @@ -1050,43 +1004,40 @@ def terminate_virtualized_cluster(cls, keyname, clean, is_verbose): output=node.get("output")) AppScaleLogger.verbose(u"Output of node at {node_ip}:\n" u"{output}".format(node_ip=node.get("ip"), - output=node.get("output")), - is_verbose) + output=node.get("output"))) if not terminated_successfully or machines > 0: LocalState.generate_crash_log(AppControllerException, log_dump) raise AppScaleException("{0} node(s) failed stopping AppScale, " "head node is still running AppScale services." .format(machines)) - cls.stop_remote_appcontroller(shadow_host, keyname, is_verbose, clean) + cls.stop_remote_appcontroller(shadow_host, keyname, clean) except socket.error as socket_error: AppScaleLogger.warn(u'Unable to talk to AppController: {}'. format(socket_error.message)) raise except Exception as exception: AppScaleLogger.verbose(u'Saw Exception while stopping AppScale {0}'. - format(str(exception)), is_verbose) + format(str(exception))) raise @classmethod - def stop_remote_appcontroller(cls, host, keyname, is_verbose, clean=False): + def stop_remote_appcontroller(cls, host, keyname, clean=False): """Stops the AppController daemon on the specified host. Args: host: The location of the AppController to stop. keyname: The name of the SSH keypair used for this AppScale deployment. - is_verbose: A bool that indicates if we should print the stop commands we - exec to stdout. clean: A boolean that specifies whether or not to clean persistent state. """ terminate_cmd = 'ruby /root/appscale/AppController/terminate.rb' if clean: terminate_cmd += ' clean' - cls.ssh(host, keyname, terminate_cmd, is_verbose) + cls.ssh(host, keyname, terminate_cmd) @classmethod - def copy_app_to_host(cls, app_location, app_id, keyname, is_verbose, + def copy_app_to_host(cls, app_location, app_id, keyname, extras=None, custom_service_yaml=None): """Copies the given application to a machine running the Login service within an AppScale deployment. @@ -1097,8 +1048,6 @@ def copy_app_to_host(cls, app_location, app_id, keyname, is_verbose, app_id: The project to use for this application. keyname: The name of the SSH keypair that uniquely identifies this AppScale deployment. - is_verbose: A bool that indicates if we should print the commands we exec - to copy the app to the remote host to stdout. extras: A dictionary containing a list of files to include in the upload. custom_service_yaml: A string specifying the location of the service yaml being deployed. @@ -1140,17 +1089,15 @@ def copy_app_to_host(cls, app_location, app_id, keyname, is_verbose, AppScaleLogger.log("Copying over application") remote_app_tar = "{0}/{1}.tar.gz".format(cls.REMOTE_APP_DIR, app_id) head_node_public_ip = LocalState.get_host_with_role(keyname, 'shadow') - cls.scp(head_node_public_ip, keyname, local_tarred_app, remote_app_tar, - is_verbose) + cls.scp(head_node_public_ip, keyname, local_tarred_app, remote_app_tar) - AppScaleLogger.verbose("Removing local copy of tarred application", - is_verbose) + AppScaleLogger.verbose("Removing local copy of tarred application") os.remove(local_tarred_app) return remote_app_tar @classmethod - def collect_appcontroller_crashlog(cls, host, keyname, is_verbose): + def collect_appcontroller_crashlog(cls, host, keyname): """ Reads the crashlog that the AppController writes on its own machine indicating why it crashed, so that we can pass this information on to the user. @@ -1160,8 +1107,6 @@ def collect_appcontroller_crashlog(cls, host, keyname, is_verbose): AppController can be found. keyname: The name of the SSH keypair that uniquely identifies this AppScale deployment. - is_verbose: A bool that indicates if we should print the commands we exec - to get the crashlog info. Returns: A str corresponding to the message that indicates why the AppController @@ -1171,7 +1116,7 @@ def collect_appcontroller_crashlog(cls, host, keyname, is_verbose): local_crashlog = "{0}/appcontroller-log-{1}".format( tempfile.gettempdir(), uuid.uuid4()) cls.scp_remote_to_local(host, keyname, cls.APPCONTROLLER_CRASHLOG_PATH, - local_crashlog, is_verbose) + local_crashlog) with open(local_crashlog, 'r') as file_handle: message = u"AppController at {0} crashed because: {1}".format( host, file_handle.read()) From ea7fe87a6598da34d9cb603656c661277d4af9a6 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Mon, 9 Sep 2019 17:23:04 -0700 Subject: [PATCH 54/63] AppController service now uses systemd --- appscale/tools/remote_helper.py | 11 +---- test/test_appscale_run_instances.py | 66 ++--------------------------- test/test_remote_helper.py | 18 +------- 3 files changed, 5 insertions(+), 90 deletions(-) diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index c5be1c20..6b25104b 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -769,17 +769,8 @@ def start_remote_appcontroller(cls, host, keyname, is_verbose): """ AppScaleLogger.log("Starting AppController at {0}".format(host)) - # Remove any previous state. TODO: Don't do this with the tools. - cls.ssh(host, keyname, - 'rm -rf {}/appcontroller-state.json'.format(cls.CONFIG_DIR), is_verbose) - - # Remove any monit configuration files from previous AppScale deployments. - cls.ssh(host, keyname, 'rm -rf /etc/monit/conf.d/appscale-*.cfg', is_verbose) - - cls.ssh(host, keyname, 'service monit start', is_verbose) - # Start the AppController. - cls.ssh(host, keyname, 'service appscale-controller start', is_verbose) + cls.ssh(host, keyname, 'systemctl start appscale-controller', is_verbose) AppScaleLogger.log("Please wait for the AppController to finish " + \ "pre-processing tasks.") diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index b4c59450..d1f22cd9 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -331,25 +331,8 @@ def test_appscale_in_one_node_virt_deployment(self): .with_args(re.compile('^openssl'),False,stdin=None)\ .and_return() - # mock out removing the old json file - self.local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'),False,5,stdin=re.compile('rm -rf'))\ - .and_return() - - # assume that we started monit fine - self.local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'),False,5,stdin=re.compile('monit'))\ - .and_return() - self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') - - self.local_state.should_receive('shell').\ - with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' - '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@{} '.format(IP_1), - False, 5, - stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() + re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks(IP_1) self.setup_appcontroller_mocks(IP_1, IP_1) @@ -509,43 +492,14 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): with_args(re.compile('scp .*{0}'.format(self.keyname)), False, 5).\ and_return() - self.local_state.should_receive('shell').\ - with_args('ssh -i /root/.appscale/bookey.key -o LogLevel=quiet -o ' - 'NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@public1 ', - False, 5, - stdin='cp /root/appscale/AppController/scripts/appcontroller ' - '/etc/init.d/').\ - and_return() - - self.local_state.should_receive('shell').\ - with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' - '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@elastic-ip ', - False, 5, - stdin='cp /root/appscale/AppController/scripts/appcontroller ' - '/etc/init.d/').\ - and_return() - - self.local_state.should_receive('shell').\ - with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' - '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@elastic-ip ', - False, 5, stdin='chmod +x /etc/init.d/appcontroller').\ - and_return() - self.setup_appscale_compatibility_mocks() # mock out generating the private key self.local_state.should_receive('shell').with_args(re.compile('openssl'), False, stdin=None) - # assume that we started monit fine - self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin=re.compile('monit')) - self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks('elastic-ip') self.setup_appcontroller_mocks('elastic-ip', 'private1') @@ -702,12 +656,8 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): self.local_state.should_receive('shell').with_args(re.compile('openssl'), False, stdin=None) - # assume that we started monit fine - self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin=re.compile('monit')) - self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks('public1') self.setup_appcontroller_mocks('public1', 'private1') @@ -728,16 +678,6 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): self.local_state.should_receive('shell').with_args(re.compile('scp'), False, 5, stdin=re.compile('{0}.secret'.format(self.keyname))) - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazbargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() - - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@elastic-ip ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() - - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@elastic-ip ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() - - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/') - - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() - flexmock(RemoteHelper).should_receive('copy_deployment_credentials') flexmock(AppControllerClient) AppControllerClient.should_receive('does_user_exist').and_return(True) diff --git a/test/test_remote_helper.py b/test/test_remote_helper.py index 1cbfdfed..b29d56df 100644 --- a/test/test_remote_helper.py +++ b/test/test_remote_helper.py @@ -359,20 +359,11 @@ def test_copy_deployment_credentials_in_cloud(self): RemoteHelper.copy_deployment_credentials('public1', options) def test_start_remote_appcontroller(self): - # mock out removing the old json file local_state = flexmock(LocalState) - local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'),False,5,stdin=re.compile('rm -rf'))\ - .and_return() - - # assume we started monit on public1 fine - local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'), False, 5, stdin=re.compile('monit'))\ - .and_return() # and assume we started the AppController on public1 fine local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') # finally, assume the appcontroller comes up after a few tries # assume that ssh comes up on the third attempt @@ -382,13 +373,6 @@ def test_start_remote_appcontroller(self): .and_raise(Exception).and_return(None) socket.should_receive('socket').and_return(fake_socket) - # Mock out additional remote calls. - local_state.should_receive('shell').with_args('ssh -i /root/.appscale/bookey.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() - - local_state.should_receive('shell').with_args('ssh -i /root/.appscale/bookey.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() - - local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@elastic-ip ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() - RemoteHelper.start_remote_appcontroller('public1', 'bookey', False) From e8e7b795b5a3dfa63df45ec8b06c4253c9574f1f Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Mon, 9 Sep 2019 17:23:22 -0700 Subject: [PATCH 55/63] Systemd monit replace, do not collect monit logs --- appscale/tools/appscale_tools.py | 1 - 1 file changed, 1 deletion(-) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index b469503e..ce9b2383 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -449,7 +449,6 @@ def gather_logs(cls, options): {'remote': '/var/log/appscale'}, {'remote': '/var/log/haproxy.log*'}, {'remote': '/var/log/kern.log*'}, - {'remote': '/var/log/monit.log*'}, {'remote': '/var/log/nginx'}, {'remote': '/var/log/rabbitmq/*', 'local': 'rabbitmq'}, {'remote': '/var/log/syslog*'}, From 0cdf050cf0410602e6b25b5e0b406cdc5bcc5ea9 Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Tue, 10 Sep 2019 19:56:30 +0300 Subject: [PATCH 56/63] Make verbose flag optional --- appscale/tools/appscale.py | 14 ++- appscale/tools/appscale_logger.py | 6 +- appscale/tools/appscale_stats.py | 25 ++-- appscale/tools/appscale_tools.py | 5 +- appscale/tools/local_state.py | 58 ++++++---- appscale/tools/remote_helper.py | 182 +++++++++++++++++++----------- 6 files changed, 186 insertions(+), 104 deletions(-) diff --git a/appscale/tools/appscale.py b/appscale/tools/appscale.py index f8c89a83..bf233726 100644 --- a/appscale/tools/appscale.py +++ b/appscale/tools/appscale.py @@ -343,7 +343,7 @@ def valid_ssh_key(self, config, run_instances_opts): BadConfigurationException: If the IPs layout was not a dictionary. """ keyname = config['keyname'] - AppScaleLogger.is_verbose = config.get('verbose', False) + verbose = config.get('verbose', False) if not isinstance(config['ips_layout'], dict) and \ not isinstance(config['ips_layout'], list): @@ -368,7 +368,7 @@ def valid_ssh_key(self, config, run_instances_opts): remote_key = '{}/ssh.key'.format(RemoteHelper.CONFIG_DIR) try: RemoteHelper.scp( - head_node.public_ip, keyname, ssh_key_location, remote_key) + head_node.public_ip, keyname, ssh_key_location, remote_key, verbose) except ShellException: return False @@ -377,13 +377,13 @@ def valid_ssh_key(self, config, run_instances_opts): .format(key=remote_key, ip=ip) try: RemoteHelper.ssh( - head_node.public_ip, keyname, ssh_to_ip, user='root') + head_node.public_ip, keyname, ssh_to_ip, verbose, user='root') except ShellException: return False return True for ip in all_ips: - if not self.can_ssh_to_ip(ip, keyname): + if not self.can_ssh_to_ip(ip, keyname, verbose): return False return True @@ -408,7 +408,7 @@ def get_ips_from_options(ips): 'Invalid IP address in {}'.format(all_ips) return all_ips - def can_ssh_to_ip(self, ip, keyname): + def can_ssh_to_ip(self, ip, keyname, is_verbose=None): """ Attempts to SSH into the machine located at the given IP address with the given SSH key. @@ -416,13 +416,15 @@ def can_ssh_to_ip(self, ip, keyname): ip: The IP address to attempt to SSH into. keyname: The name of the SSH key that uniquely identifies this AppScale deployment. + is_verbose: A bool that indicates if we should print the SSH command we + execute to stdout. Returns: A bool that indicates whether or not the given SSH key can log in without a password to the given machine. """ try: - RemoteHelper.ssh(ip, keyname, 'ls', user='root') + RemoteHelper.ssh(ip, keyname, 'ls', is_verbose, user='root') return True except ShellException: return False diff --git a/appscale/tools/appscale_logger.py b/appscale/tools/appscale_logger.py index db5dc7f8..9e177377 100644 --- a/appscale/tools/appscale_logger.py +++ b/appscale/tools/appscale_logger.py @@ -46,14 +46,16 @@ def success(cls, message): cprint(message, 'green') @classmethod - def verbose(cls, message): + def verbose(cls, message, is_verbose=None): """Prints the specified message if we're running in 'verbose' mode, and always logs it. Args: message: A str representing the message to log. + is_verbose: A bool that indicates whether or not the message should be + printed to stdout. """ - if cls.is_verbose: + if is_verbose or (is_verbose is None and cls.is_verbose): print message diff --git a/appscale/tools/appscale_stats.py b/appscale/tools/appscale_stats.py index 1ef0ec46..8a558e19 100644 --- a/appscale/tools/appscale_stats.py +++ b/appscale/tools/appscale_stats.py @@ -114,7 +114,8 @@ def show_stats(options): node_headers, node_stats = get_node_stats_rows( raw_node_stats=raw_node_stats, all_roles=all_roles, - specified_roles=options.roles + specified_roles=options.roles, + verbose=options.verbose ) print_table( table_name="NODE STATISTICS", @@ -168,7 +169,7 @@ def show_stats(options): if process_failures: failures["processes"] = process_failures - if AppScaleLogger.is_verbose: + if options.verbose: # Additionally render detailed processes stats table in verbose mode order_columns_map = { "name": PROCESSES_NAME_COLUMN_VERBOSE_NUMBER, @@ -202,6 +203,7 @@ def show_stats(options): # Prepare proxies stats table proxy_headers, proxy_stats = get_proxy_stats_rows( raw_proxy_stats=raw_proxy_stats, + verbose=options.verbose, apps_filter=options.apps_only ) proxy_stats = sort_proxy_stats_rows( @@ -245,12 +247,14 @@ def render_loadavg(loadavg): ]) -def render_partitions(partitions): +def render_partitions(partitions, verbose): """ Renders partitions information. Args: partitions: A dict representing partition values. + verbose: A boolean - render all partitions if True, + add only three the most used partitions if False. Returns: A string with information about partition values @@ -271,7 +275,7 @@ def render_partitions(partitions): for part in part_list ] - if not AppScaleLogger.is_verbose and len(partitions_info) > 3: + if not verbose and len(partitions_info) > 3: partitions_info = partitions_info[:3] + ["..."] return ", ".join(partitions_info) @@ -352,7 +356,7 @@ def get_roles(keyname): return roles_data -def get_node_stats_rows(raw_node_stats, all_roles, specified_roles): +def get_node_stats_rows(raw_node_stats, all_roles, specified_roles, verbose): """ Obtains useful information from node statistics and returns: PRIVATE IP, AVAILABLE MEMORY, LOADAVG, PARTITIONS USAGE, ROLES values. @@ -363,6 +367,8 @@ def get_node_stats_rows(raw_node_stats, all_roles, specified_roles): all_roles: A dict in which each key is an ip and value is a role list. specified_roles: A list representing specified roles that nodes should contain. + verbose: A boolean - add all partitions if True, + add only three the most used partitions if False. Returns: A list of node statistics headers. @@ -391,7 +397,7 @@ def get_node_stats_rows(raw_node_stats, all_roles, specified_roles): styled(render_memory(memory=node["memory"]), "bold", if_=is_master), styled(render_loadavg(loadavg=node["loadavg"]), "bold", if_=is_master), styled( - render_partitions(partitions=node["partitions_dict"]), + render_partitions(partitions=node["partitions_dict"], verbose=verbose), "bold", if_=is_master ), styled(u" ".join(ip_roles), "bold", if_=is_master) @@ -508,7 +514,7 @@ def get_summary_process_stats_rows(raw_process_stats, raw_node_stats): return process_stats_headers, process_stats -def get_proxy_stats_rows(raw_proxy_stats, apps_filter): +def get_proxy_stats_rows(raw_proxy_stats, verbose, apps_filter): """ Obtains useful information from proxy statistics and returns: SERVICE (ID), UNIQUE MEMORY SUM (MB), CPU PER 1 PROCESS (%), @@ -517,13 +523,14 @@ def get_proxy_stats_rows(raw_proxy_stats, apps_filter): Args: raw_proxy_stats: A dict in which each key is an ip and value is a dict of useful information. + verbose: A boolean - verbose or not verbose mode. apps_filter: A boolean - show all services or applications only. Returns: A list of proxy statistics headers. A list of proxy statistics. """ - if AppScaleLogger.is_verbose: + if verbose: headers = [ "SERVICE (ID)", "SERVERS | DOWN", "RATE | REQ TOTAL", "5xx | 4xx", "BYTES IN | BYTES OUT", "QCUR | SCUR", "QTIME | RTIME" @@ -585,7 +592,7 @@ def get_proxy_stats_rows(raw_proxy_stats, apps_filter): ) ] - if AppScaleLogger.is_verbose: + if verbose: proxy_row.append("{bin} | {bout}".format(**value)) proxy_row.append("{qcur} | {scur}".format(**value)) if "qtime" in value: diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index 8b36f1a0..5d9d7cac 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -122,8 +122,7 @@ def add_instances(cls, options): # TODO(cgb): Should we wait for the new instances to come up and get # initialized? AppScaleLogger.success("Successfully sent request to add instances " + \ - "to this AppScale deployment.") - + "to this AppScale deployment.") @classmethod def add_keypair(cls, options): @@ -209,7 +208,7 @@ def print_cluster_status(cls, options): nodes = [NodeStats(ip, node) for ip, node in node_stats.iteritems() if node] invisible_nodes = [ip for ip, node in node_stats.iteritems() if not node] - if AppScaleLogger.is_verbose: + if options.verbose: AppScaleLogger.log("-"*76) cls._print_nodes_info(nodes, invisible_nodes) cls._print_roles_info(nodes) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index b3b2b4ba..1278b775 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -210,7 +210,7 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): "autoscale": str(options.autoscale), "clear_datastore": str(False), "user_commands": json.dumps(options.user_commands), - "verbose": str(AppScaleLogger.is_verbose), + "verbose": str(options.verbose), "flower_password": options.flower_password, "default_max_appserver_memory": str(options.default_max_appserver_memory), "fdb_clusterfile_content": options.fdb_clusterfile_content @@ -298,19 +298,21 @@ def obscure_str(cls, str_to_obscure): @classmethod - def generate_ssl_cert(cls, keyname): + def generate_ssl_cert(cls, keyname, is_verbose=None): """Generates a self-signed SSL certificate that AppScale services can use to encrypt traffic with. Args: keyname: A str representing the SSH keypair name used for this AppScale deployment. + is_verbose: A bool that indicates if we want to print out the certificate + generation to stdout or not. """ cls.shell("openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 " + \ "-subj '/C=US/ST=Foo/L=Bar/O=AppScale/CN=appscale.com' " + \ "-keyout {0} -out {1}". \ format(LocalState.get_private_key_location(keyname), - LocalState.get_certificate_location(keyname)), stdin=None) + LocalState.get_certificate_location(keyname)), is_verbose, stdin=None) @classmethod @@ -936,12 +938,15 @@ def cleanup_appscale_files(cls, keyname, remove_locations=True): @classmethod - def shell(cls, command, num_retries=DEFAULT_NUM_RETRIES, stdin=None): + def shell(cls, command, is_verbose=None, num_retries=DEFAULT_NUM_RETRIES, + stdin=None): """Executes a command on this machine, retrying it up to five times if it initially fails. Args: command: A str representing the command to execute. + is_verbose: A bool that indicates if we should print the command we are + executing to stdout. num_retries: The number of times we should try to execute the given command before aborting. stdin: A str that is passes as standard input to the process @@ -952,23 +957,26 @@ def shell(cls, command, num_retries=DEFAULT_NUM_RETRIES, stdin=None): ShellException: If, after five attempts, executing the named command failed. """ + if is_verbose is None: + is_verbose = AppScaleLogger.is_verbose tries_left = num_retries try: while tries_left: - AppScaleLogger.verbose("shell> {0}".format(command)) + AppScaleLogger.verbose("shell> {0}".format(command), is_verbose) the_temp_file = tempfile.NamedTemporaryFile() if stdin is not None: stdin_strio = tempfile.TemporaryFile() stdin_strio.write(stdin) stdin_strio.seek(0) - AppScaleLogger.verbose(" stdin str: {0}".format(stdin)) + AppScaleLogger.verbose(" stdin str: {0}"\ + .format(stdin), is_verbose) result = subprocess.Popen(command, shell=True, stdout=the_temp_file, stdin=stdin_strio, stderr=subprocess.STDOUT) else: result = subprocess.Popen(command, shell=True, stdout=the_temp_file, stderr=subprocess.STDOUT) - AppScaleLogger.verbose(" stdout buffer: {0}" - .format(the_temp_file.name)) + AppScaleLogger.verbose(" stdout buffer: {0}"\ + .format(the_temp_file.name), is_verbose) result.wait() if stdin is not None: stdin_strio.close() @@ -980,8 +988,8 @@ def shell(cls, command, num_retries=DEFAULT_NUM_RETRIES, stdin=None): tries_left -= 1 if tries_left: the_temp_file.close() - AppScaleLogger.verbose("Command failed. Trying again momentarily." - .format(command)) + AppScaleLogger.verbose("Command failed. Trying again momentarily." \ + .format(command), is_verbose) else: the_temp_file.seek(0) output = the_temp_file.read() @@ -1003,13 +1011,15 @@ def shell(cls, command, num_retries=DEFAULT_NUM_RETRIES, stdin=None): @classmethod - def require_ssh_commands(cls, needs_expect): + def require_ssh_commands(cls, needs_expect, is_verbose=None): """Checks to make sure the commands needed to set up passwordless SSH access are installed on this machine. Args: needs_expect: A bool that indicates if we should also check for the 'expect' command. + is_verbose: A bool that indicates if we should print how we check for + each command to stdout. Raises: BadConfigurationException: If any of the required commands aren't present on this machine. @@ -1020,20 +1030,22 @@ def require_ssh_commands(cls, needs_expect): for command in required_commands: try: - cls.shell("hash {0}".format(command)) + cls.shell("hash {0}".format(command), is_verbose) except ShellException: raise BadConfigurationException("Couldn't find {0} in your PATH." .format(command)) @classmethod - def generate_rsa_key(cls, keyname): + def generate_rsa_key(cls, keyname, is_verbose=None): """Generates a new RSA public and private keypair, and saves it to the local filesystem. Args: keyname: The SSH keypair name that uniquely identifies this AppScale deployment. + is_verbose: A bool that indicates if we should print the ssh-keygen + command to stdout. """ private_key = cls.LOCAL_APPSCALE_PATH + keyname public_key = private_key + ".pub" @@ -1044,7 +1056,7 @@ def generate_rsa_key(cls, keyname): if os.path.exists(private_key): os.remove(private_key) - cls.shell("ssh-keygen -t rsa -N '' -f {0}".format(private_key)) + cls.shell("ssh-keygen -t rsa -N '' -f {0}".format(private_key), is_verbose) os.chmod(public_key, 0600) os.chmod(private_key, 0600) shutil.copy(private_key, private_key + ".key") @@ -1052,37 +1064,41 @@ def generate_rsa_key(cls, keyname): @classmethod - def extract_tgz_app_to_dir(cls, tar_location): + def extract_tgz_app_to_dir(cls, tar_location, is_verbose=None): """Extracts the given tar.gz file to a randomly generated location and returns that location. Args: archive_location: The location on the local filesystem where the tar.gz file to extract can be found. + is_verbose: A bool that indicates if we should print the command we + execute to stdout. Returns: The location on the local filesystem where the file was extracted to. """ - return cls.extract_app_to_dir(tar_location, "tar zxvf") + return cls.extract_app_to_dir(tar_location, "tar zxvf", is_verbose) @classmethod - def extract_zip_app_to_dir(cls, zip_location): + def extract_zip_app_to_dir(cls, zip_location, is_verbose=None): """Extracts the given zip file to a randomly generated location and returns that location. Args: archive_location: The location on the local filesystem where the zip file to extract can be found. + is_verbose: A bool that indicates if we should print the command we + execute to stdout. Returns: The location on the local filesystem where the file was extracted to. """ - return cls.extract_app_to_dir(zip_location, "unzip") + return cls.extract_app_to_dir(zip_location, "unzip", is_verbose) @classmethod - def extract_app_to_dir(cls, archive_location, extract_command): + def extract_app_to_dir(cls, archive_location, extract_command, is_verbose=None): """Extracts the given file to a randomly generated location and returns that location. @@ -1091,6 +1107,8 @@ def extract_app_to_dir(cls, archive_location, extract_command): to extract can be found. extract_command: The command and flags necessary to extract the archived file. + is_verbose: A bool that indicates if we should print the command we + execute to stdout. Returns: The location on the local filesystem where the file was extracted to. @@ -1100,7 +1118,7 @@ def extract_app_to_dir(cls, archive_location, extract_command): os.mkdir(extracted_location) cls.shell("cd {0} && {1} '{2}'".format(extracted_location, extract_command, - os.path.abspath(archive_location))) + os.path.abspath(archive_location)), is_verbose) file_list = os.listdir(extracted_location) if len(file_list) > 0: diff --git a/appscale/tools/remote_helper.py b/appscale/tools/remote_helper.py index bd1759ae..319e3ef3 100644 --- a/appscale/tools/remote_helper.py +++ b/appscale/tools/remote_helper.py @@ -352,22 +352,25 @@ def spawn_nodes_in_cloud(cls, agent, params, count=1, load_balancer=False): return instance_ids, public_ips, private_ips @classmethod - def sleep_until_port_is_open(cls, host, port): + def sleep_until_port_is_open(cls, host, port, is_verbose=None): """Queries the given host to see if the named port is open, and if not, waits until it is. Args: host: A str representing the host whose port we should be querying. port: An int representing the port that should eventually be open. + verbose: A bool that indicates if we should print failure messages to + stdout (e.g., connection refused messages that can occur when we wait + for services to come up). Raises: TimeoutException if the port does not open in a certain amount of time. """ sleep_time = cls.MAX_WAIT_TIME while sleep_time > 0: - if cls.is_port_open(host, port): + if cls.is_port_open(host, port, is_verbose): return AppScaleLogger.verbose("Waiting {2} second(s) for {0}:{1} to open" - .format(host, port, cls.WAIT_TIME)) + .format(host, port, cls.WAIT_TIME), is_verbose) time.sleep(cls.WAIT_TIME) sleep_time -= cls.WAIT_TIME raise TimeoutException("Port {}:{} did not open in time. " @@ -375,12 +378,15 @@ def sleep_until_port_is_open(cls, host, port): @classmethod - def is_port_open(cls, host, port): + def is_port_open(cls, host, port, is_verbose=None): """Queries the given host to see if the named port is open. Args: host: A str representing the host whose port we should be querying. port: An int representing the port that should eventually be open. + verbose: A bool that indicates if we should print failure messages to + stdout (e.g., connection refused messages that can occur when we wait + for services to come up). Returns: True if the port is open, False otherwise. """ @@ -389,11 +395,11 @@ def is_port_open(cls, host, port): sock.connect((host, port)) return True except Exception as exception: - AppScaleLogger.verbose(str(exception)) + AppScaleLogger.verbose(str(exception), is_verbose) return False @classmethod - def merge_authorized_keys(cls, host, keyname, user): + def merge_authorized_keys(cls, host, keyname, user, is_verbose=None): """ Adds the contents of the user's authorized_keys file to the root's authorized_keys file. @@ -401,32 +407,34 @@ def merge_authorized_keys(cls, host, keyname, user): host: A str representing the host to enable root logins on. keyname: A str representing the name of the SSH keypair to login with. user: A str representing the name of the user to login as. + is_verbose: A bool indicating if we should print the command we execute to + enable root login to stdout. """ AppScaleLogger.log('Root login not enabled for {} - enabling it ' 'now.'.format(host)) create_root_keys = 'sudo touch /root/.ssh/authorized_keys' - cls.ssh(host, keyname, create_root_keys, user=user) + cls.ssh(host, keyname, create_root_keys, is_verbose, user=user) set_permissions = 'sudo chmod 600 /root/.ssh/authorized_keys' - cls.ssh(host, keyname, set_permissions, user=user) + cls.ssh(host, keyname, set_permissions, is_verbose, user=user) - temp_file = cls.ssh(host, keyname, 'mktemp', user=user) + temp_file = cls.ssh(host, keyname, 'mktemp', is_verbose, user=user) merge_to_tempfile = 'sudo sort -u ~/.ssh/authorized_keys '\ '/root/.ssh/authorized_keys -o {}'.format(temp_file) - cls.ssh(host, keyname, merge_to_tempfile, user=user) + cls.ssh(host, keyname, merge_to_tempfile, is_verbose, user=user) overwrite_root_keys = "sudo sed -n '/.*Please login/d; "\ "w/root/.ssh/authorized_keys' {}".format(temp_file) - cls.ssh(host, keyname, overwrite_root_keys, user=user) + cls.ssh(host, keyname, overwrite_root_keys, is_verbose, user=user) remove_tempfile = 'rm -f {0}'.format(temp_file) - cls.ssh(host, keyname, remove_tempfile, user=user) + cls.ssh(host, keyname, remove_tempfile, is_verbose, user=user) return @classmethod - def enable_root_login(cls, host, keyname, infrastructure): + def enable_root_login(cls, host, keyname, infrastructure, is_verbose=None): """Logs into the named host and alters its ssh configuration to enable the root user to directly log in. @@ -435,18 +443,21 @@ def enable_root_login(cls, host, keyname, infrastructure): keyname: A str representing the name of the SSH keypair to login with. infrastructure: A str representing the name of the cloud infrastructure we're running on. + is_verbose: A bool indicating if we should print the command we execute to + enable root login to stdout. """ # First, see if we need to enable root login at all (some VMs have it # already enabled). try: if infrastructure == "azure": - cls.merge_authorized_keys(host, keyname, 'azureuser') - output = cls.ssh(host, keyname, 'ls', user='root') + cls.merge_authorized_keys(host, keyname, 'azureuser', is_verbose) + output = cls.ssh(host, keyname, 'ls', is_verbose, user='root') except ShellException as exception: # Google Compute Engine creates a user with the same name as the currently # logged-in user, so log in as that user to enable root login. if infrastructure == "gce": - cls.merge_authorized_keys(host, keyname, getpass.getuser()) + cls.merge_authorized_keys(host, keyname, getpass.getuser(), + is_verbose) return else: raise exception @@ -456,13 +467,13 @@ def enable_root_login(cls, host, keyname, infrastructure): match = re.match(cls.LOGIN_AS_UBUNTU_USER, output) if match: user = match.group(1) - cls.merge_authorized_keys(host, keyname, user) + cls.merge_authorized_keys(host, keyname, user, is_verbose) else: AppScaleLogger.log("Root login already enabled for {}.".format(host)) @classmethod - def ssh(cls, host, keyname, command, user='root', + def ssh(cls, host, keyname, command, is_verbose=None, user='root', num_retries=LocalState.DEFAULT_NUM_RETRIES): """Logs into the named host and executes the given command. @@ -470,6 +481,8 @@ def ssh(cls, host, keyname, command, user='root', host: A str representing the machine that we should log into. keyname: A str representing the name of the SSH keypair to log in with. command: A str representing what to execute on the remote host. + is_verbose: A bool indicating if we should print the ssh command to + stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the remote command and a str @@ -477,11 +490,12 @@ def ssh(cls, host, keyname, command, user='root', """ ssh_key = LocalState.get_key_path_from_name(keyname) return LocalState.shell("ssh -F /dev/null -i {0} {1} {2}@{3} bash".format( - ssh_key, cls.SSH_OPTIONS, user, host), num_retries, stdin=command) + ssh_key, cls.SSH_OPTIONS, user, host), + is_verbose, num_retries, stdin=command) @classmethod - def scp(cls, host, keyname, source, dest, user='root', + def scp(cls, host, keyname, source, dest, is_verbose=None, user='root', num_retries=LocalState.DEFAULT_NUM_RETRIES): """Securely copies a file from this machine to the named machine. @@ -492,6 +506,8 @@ def scp(cls, host, keyname, source, dest, user='root', file should be copied from. dest: A str representing the path on the remote machine where the file should be copied to. + is_verbose: A bool that indicates if we should print the scp command to + stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the secure copy and a str @@ -501,11 +517,12 @@ def scp(cls, host, keyname, source, dest, user='root', command = "scp -r -i {0} {1} '{2}' {3}@{4}:'{5}'".format( ssh_key, cls.SSH_OPTIONS, source, user, host, dest.replace(" ", "\ ") ) - return LocalState.shell(command, num_retries) + return LocalState.shell(command, is_verbose, num_retries) @classmethod - def scp_remote_to_local(cls, host, keyname, source, dest, user='root'): + def scp_remote_to_local(cls, host, keyname, source, dest, is_verbose=None, + user='root'): """Securely copies a file from a remote machine to this machine. Args: @@ -515,6 +532,8 @@ def scp_remote_to_local(cls, host, keyname, source, dest, user='root'): file should be copied from. dest: A str representing the path on the local machine where the file should be copied to. + is_verbose: A bool that indicates if we should print the scp command to + stdout. user: A str representing the user to log in as. Returns: A str representing the standard output of the secure copy and a str @@ -524,11 +543,11 @@ def scp_remote_to_local(cls, host, keyname, source, dest, user='root'): command = "scp -r -i {0} {1} {2}@{3}:'{4}' '{5}'".format( ssh_key, cls.SSH_OPTIONS, user, host, source.replace(" ", "\ "), dest ) - return LocalState.shell(command) + return LocalState.shell(command, is_verbose) @classmethod - def copy_ssh_keys_to_node(cls, host, keyname): + def copy_ssh_keys_to_node(cls, host, keyname, is_verbose=None): """Sets the given SSH keypair as the default key for the named host, enabling it to log into other machines in the AppScale deployment without being prompted for a password or explicitly requiring the key to be @@ -537,14 +556,17 @@ def copy_ssh_keys_to_node(cls, host, keyname): Args: host: A str representing the machine that we should log into. keyname: A str representing the name of the SSH keypair to log in with. + is_verbose: A bool that indicates if we should print the SCP commands + needed to copy the SSH keys over to stdout. """ ssh_key = LocalState.get_key_path_from_name(keyname) - cls.scp(host, keyname, ssh_key, '/root/.ssh/id_dsa') - cls.scp(host, keyname, ssh_key, '/root/.ssh/id_rsa') - cls.scp(host, keyname, ssh_key, '{}/{}.key'.format(cls.CONFIG_DIR, keyname)) + cls.scp(host, keyname, ssh_key, '/root/.ssh/id_dsa', is_verbose) + cls.scp(host, keyname, ssh_key, '/root/.ssh/id_rsa', is_verbose) + cls.scp(host, keyname, ssh_key, '{}/{}.key'.format(cls.CONFIG_DIR, keyname), + is_verbose) @classmethod - def ensure_machine_is_compatible(cls, host, keyname): + def ensure_machine_is_compatible(cls, host, keyname, is_verbose=None): """Verifies that the specified host has AppScale installed on it. This also validates that the host has the right version of AppScale @@ -555,12 +577,14 @@ def ensure_machine_is_compatible(cls, host, keyname): AppScale-compatible. keyname: A str representing the SSH keypair name that can log into the named host. + is_verbose: A bool that indicates if we should print the commands we + execute to validate the machine to stdout. Raises: AppScaleException: If the specified host does not have AppScale installed, or has the wrong version of AppScale installed. """ # First, make sure the image is an AppScale image. - remote_version = cls.get_host_appscale_version(host, keyname) + remote_version = cls.get_host_appscale_version(host, keyname, is_verbose) if not remote_version: raise AppScaleException("The machine at {0} does not have " \ "AppScale installed.".format(host)) @@ -575,7 +599,7 @@ def ensure_machine_is_compatible(cls, host, keyname): @classmethod - def does_host_have_location(cls, host, keyname, location): + def does_host_have_location(cls, host, keyname, location, is_verbose=None): """Logs into the specified host with the given keyname and checks to see if the named directory exists there. @@ -586,19 +610,21 @@ def does_host_have_location(cls, host, keyname, location): the specified machine. location: The path on the remote filesystem that we should be checking for. + is_verbose: A bool that indicates if we should print the command we + execute to check the remote host's location to stdout. Returns: True if the remote host has a file or directory at the specified location, False otherwise. """ try: - cls.ssh(host, keyname, 'ls {0}'.format(location)) + cls.ssh(host, keyname, 'ls {0}'.format(location), is_verbose) return True except ShellException: return False @classmethod - def get_host_appscale_version(cls, host, keyname): + def get_host_appscale_version(cls, host, keyname, is_verbose=None): """Logs into the specified host with the given keyname and checks to see what version of AppScale is installed there. @@ -607,6 +633,8 @@ def get_host_appscale_version(cls, host, keyname): machine. keyname: A str representing the name of the SSH keypair that can log into the specified machine. + is_verbose: A bool that indicates if we should print the command we + execute to check the remote host's location to stdout. Returns: A str containing the version of AppScale installed on host, or None if (1) AppScale isn't installed on host, or (2) host has more than one @@ -615,7 +643,7 @@ def get_host_appscale_version(cls, host, keyname): remote_version_file = '{}/{}'.format(cls.CONFIG_DIR, 'VERSION') try: version_output = cls.ssh( - host, keyname, 'cat {}'.format(remote_version_file)) + host, keyname, 'cat {}'.format(remote_version_file), is_verbose) except ShellException: return None version = version_output.split('AppScale version')[1].strip() @@ -623,7 +651,7 @@ def get_host_appscale_version(cls, host, keyname): @classmethod - def rsync_files(cls, host, keyname, local_appscale_dir): + def rsync_files(cls, host, keyname, local_appscale_dir, is_verbose=None): """Copies over an AppScale source directory from this machine to the specified host. @@ -634,6 +662,8 @@ def rsync_files(cls, host, keyname, local_appscale_dir): the specified machine. local_appscale_dir: A str representing the path on the local filesystem where the AppScale source to copy over can be found. + is_verbose: A bool that indicates if we should print the rsync commands + we exec to stdout. Raises: BadConfigurationException: If local_appscale_dir does not exist locally, or if any of the standard AppScale module folders do not exist. @@ -647,7 +677,7 @@ def rsync_files(cls, host, keyname, local_appscale_dir): "--exclude='AppDB/logs/*' " \ "--exclude='AppDB/cassandra/cassandra/*' " \ "{2}/* root@{3}:/root/appscale/".format(ssh_key, cls.SSH_OPTIONS, - local_path, host)) + local_path, host), is_verbose) @classmethod def copy_deployment_credentials(cls, host, options): @@ -702,7 +732,7 @@ def copy_deployment_credentials(cls, host, options): '{}/oauth2.dat'.format(cls.CONFIG_DIR)) @classmethod - def run_user_commands(cls, host, commands, keyname): + def run_user_commands(cls, host, commands, keyname, is_verbose=None): """Runs any commands specified by the user before the AppController is started. @@ -712,45 +742,49 @@ def run_user_commands(cls, host, commands, keyname): executed on the remote machine. keyname: A str representing the name of the SSH keypair that can log into the specified host. + is_verbose: A bool that indicates if we should print the commands needed + to start the AppController to stdout. """ if commands: AppScaleLogger.log("Running user-specified commands at {0}".format(host)) for command in commands: - cls.ssh(host, keyname, command) + cls.ssh(host, keyname, command, is_verbose) @classmethod - def start_remote_appcontroller(cls, host, keyname): + def start_remote_appcontroller(cls, host, keyname, is_verbose=None): """Starts the AppController daemon on the specified host. Args: host: A str representing the host to start the AppController on. keyname: A str representing the name of the SSH keypair that can log into the specified host. + is_verbose: A bool that indicates if we should print the commands needed + to start the AppController to stdout. """ AppScaleLogger.log("Starting AppController at {0}".format(host)) # Remove any previous state. TODO: Don't do this with the tools. cls.ssh(host, keyname, - 'rm -rf {}/appcontroller-state.json'.format(cls.CONFIG_DIR)) + 'rm -rf {}/appcontroller-state.json'.format(cls.CONFIG_DIR), is_verbose) # Remove any monit configuration files from previous AppScale deployments. - cls.ssh(host, keyname, 'rm -rf /etc/monit/conf.d/appscale-*.cfg') + cls.ssh(host, keyname, 'rm -rf /etc/monit/conf.d/appscale-*.cfg', is_verbose) - cls.ssh(host, keyname, 'service monit start') + cls.ssh(host, keyname, 'service monit start', is_verbose) # Start the AppController. - cls.ssh(host, keyname, 'service appscale-controller start') + cls.ssh(host, keyname, 'service appscale-controller start', is_verbose) AppScaleLogger.log("Please wait for the AppController to finish " + \ "pre-processing tasks.") - cls.sleep_until_port_is_open(host, AppControllerClient.PORT) + cls.sleep_until_port_is_open(host, AppControllerClient.PORT, is_verbose) @classmethod - def copy_local_metadata(cls, host, keyname): + def copy_local_metadata(cls, host, keyname, is_verbose=None): """Copies the locations.json file found locally (which contain metadata about this AppScale deployment) to the specified host. @@ -758,14 +792,16 @@ def copy_local_metadata(cls, host, keyname): host: The machine that we should copy the metadata files to. keyname: The name of the SSH keypair that we can use to log into the given host. + is_verbose: A bool that indicates if we should print the SCP commands we + exec to stdout. """ # and copy the json file if the tools on that box wants to use it cls.scp(host, keyname, LocalState.get_locations_json_location(keyname), - '{}/locations-{}.json'.format(cls.CONFIG_DIR, keyname)) + '{}/locations-{}.json'.format(cls.CONFIG_DIR, keyname), is_verbose) # and copy the secret file if the tools on that box wants to use it cls.scp(host, keyname, LocalState.get_secret_key_location(keyname), - cls.CONFIG_DIR) + cls.CONFIG_DIR, is_verbose) @classmethod @@ -881,7 +917,7 @@ def terminate_cloud_instance(cls, instance_id, options): AppScaleLogger.log("About to terminate instance {0}".format(instance_id)) agent = InfrastructureAgentFactory.create_agent(options.infrastructure) params = agent.get_params_from_args(options) - params['IS_VERBOSE'] = AppScaleLogger.is_verbose + params['IS_VERBOSE'] = options.verbose params[agent.PARAM_INSTANCE_IDS] = [instance_id] agent.terminate_instances(params) agent.cleanup_state(params) @@ -889,11 +925,13 @@ def terminate_cloud_instance(cls, instance_id, options): @classmethod - def terminate_cloud_infrastructure(cls, keyname): + def terminate_cloud_infrastructure(cls, keyname, is_verbose=None): """Powers off all machines in the currently running AppScale deployment. Args: keyname: The name of the SSH keypair used for this AppScale deployment. + is_verbose: A bool that indicates if we should print the commands executed + to stdout. """ AppScaleLogger.log("About to terminate deployment and instances with " "keyname {0}. Press Ctrl-C to stop.".format(keyname)) @@ -904,7 +942,10 @@ def terminate_cloud_infrastructure(cls, keyname): agent = InfrastructureAgentFactory.create_agent( LocalState.get_infrastructure(keyname)) params = agent.get_cloud_params(keyname) - params['IS_VERBOSE'] = AppScaleLogger.is_verbose + if is_verbose is not None: + params['IS_VERBOSE'] = is_verbose + else: + params['IS_VERBOSE'] = AppScaleLogger.is_verbose params['autoscale_agent'] = False # We want to terminate also the pending instances. @@ -918,7 +959,7 @@ def terminate_cloud_infrastructure(cls, keyname): if node.get('disk'): AppScaleLogger.log("Unmounting persistent disk at {0}". format(node['public_ip'])) - cls.unmount_persistent_disk(node['public_ip'], keyname) + cls.unmount_persistent_disk(node['public_ip'], keyname, is_verbose) agent.detach_disk(params, node['disk'], node['instance_id']) # terminate all the machines @@ -937,7 +978,7 @@ def terminate_cloud_infrastructure(cls, keyname): @classmethod - def unmount_persistent_disk(cls, host, keyname): + def unmount_persistent_disk(cls, host, keyname, is_verbose=None): """Unmounts the persistent disk that was previously mounted on the named machine. @@ -945,22 +986,26 @@ def unmount_persistent_disk(cls, host, keyname): host: A str that names the IP address or FQDN where the machine whose disk needs to be unmounted can be found. keyname: The name of the SSH keypair used for this AppScale deployment. + is_verbose: A bool that indicates if we should print the commands executed + to stdout. """ try: remote_output = cls.ssh(host, keyname, 'umount {0}'.format( - cls.PERSISTENT_MOUNT_POINT)) - AppScaleLogger.verbose(remote_output) + cls.PERSISTENT_MOUNT_POINT), is_verbose) + AppScaleLogger.verbose(remote_output, is_verbose) except ShellException: pass @classmethod - def terminate_virtualized_cluster(cls, keyname, clean): + def terminate_virtualized_cluster(cls, keyname, clean, is_verbose=None): """Stops all API services running on all nodes in the currently running AppScale deployment. Args: keyname: The name of the SSH keypair used for this AppScale deployment. + is_verbose: A bool that indicates if we should print the commands executed + to stdout. clean: A bool representing whether clean should be ran on the nodes. """ AppScaleLogger.log("Stopping appscale deployment with keyname {0}" @@ -1004,40 +1049,43 @@ def terminate_virtualized_cluster(cls, keyname, clean): output=node.get("output")) AppScaleLogger.verbose(u"Output of node at {node_ip}:\n" u"{output}".format(node_ip=node.get("ip"), - output=node.get("output"))) + output=node.get("output")), + is_verbose) if not terminated_successfully or machines > 0: LocalState.generate_crash_log(AppControllerException, log_dump) raise AppScaleException("{0} node(s) failed stopping AppScale, " "head node is still running AppScale services." .format(machines)) - cls.stop_remote_appcontroller(shadow_host, keyname, clean) + cls.stop_remote_appcontroller(shadow_host, keyname, is_verbose, clean) except socket.error as socket_error: AppScaleLogger.warn(u'Unable to talk to AppController: {}'. format(socket_error.message)) raise except Exception as exception: AppScaleLogger.verbose(u'Saw Exception while stopping AppScale {0}'. - format(str(exception))) + format(str(exception)), is_verbose) raise @classmethod - def stop_remote_appcontroller(cls, host, keyname, clean=False): + def stop_remote_appcontroller(cls, host, keyname, is_verbose=None, clean=False): """Stops the AppController daemon on the specified host. Args: host: The location of the AppController to stop. keyname: The name of the SSH keypair used for this AppScale deployment. + is_verbose: A bool that indicates if we should print the stop commands we + exec to stdout. clean: A boolean that specifies whether or not to clean persistent state. """ terminate_cmd = 'ruby /root/appscale/AppController/terminate.rb' if clean: terminate_cmd += ' clean' - cls.ssh(host, keyname, terminate_cmd) + cls.ssh(host, keyname, terminate_cmd, is_verbose) @classmethod - def copy_app_to_host(cls, app_location, app_id, keyname, + def copy_app_to_host(cls, app_location, app_id, keyname, is_verbose=None, extras=None, custom_service_yaml=None): """Copies the given application to a machine running the Login service within an AppScale deployment. @@ -1048,6 +1096,8 @@ def copy_app_to_host(cls, app_location, app_id, keyname, app_id: The project to use for this application. keyname: The name of the SSH keypair that uniquely identifies this AppScale deployment. + is_verbose: A bool that indicates if we should print the commands we exec + to copy the app to the remote host to stdout. extras: A dictionary containing a list of files to include in the upload. custom_service_yaml: A string specifying the location of the service yaml being deployed. @@ -1089,15 +1139,17 @@ def copy_app_to_host(cls, app_location, app_id, keyname, AppScaleLogger.log("Copying over application") remote_app_tar = "{0}/{1}.tar.gz".format(cls.REMOTE_APP_DIR, app_id) head_node_public_ip = LocalState.get_host_with_role(keyname, 'shadow') - cls.scp(head_node_public_ip, keyname, local_tarred_app, remote_app_tar) + cls.scp(head_node_public_ip, keyname, local_tarred_app, remote_app_tar, + is_verbose) - AppScaleLogger.verbose("Removing local copy of tarred application") + AppScaleLogger.verbose("Removing local copy of tarred application", + is_verbose) os.remove(local_tarred_app) return remote_app_tar @classmethod - def collect_appcontroller_crashlog(cls, host, keyname): + def collect_appcontroller_crashlog(cls, host, keyname, is_verbose=None): """ Reads the crashlog that the AppController writes on its own machine indicating why it crashed, so that we can pass this information on to the user. @@ -1107,6 +1159,8 @@ def collect_appcontroller_crashlog(cls, host, keyname): AppController can be found. keyname: The name of the SSH keypair that uniquely identifies this AppScale deployment. + is_verbose: A bool that indicates if we should print the commands we exec + to get the crashlog info. Returns: A str corresponding to the message that indicates why the AppController @@ -1116,7 +1170,7 @@ def collect_appcontroller_crashlog(cls, host, keyname): local_crashlog = "{0}/appcontroller-log-{1}".format( tempfile.gettempdir(), uuid.uuid4()) cls.scp_remote_to_local(host, keyname, cls.APPCONTROLLER_CRASHLOG_PATH, - local_crashlog) + local_crashlog, is_verbose) with open(local_crashlog, 'r') as file_handle: message = u"AppController at {0} crashed because: {1}".format( host, file_handle.read()) From cd107f0d116c212c587b4ef54da9e9b801b8291d Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Tue, 10 Sep 2019 23:56:30 +0300 Subject: [PATCH 57/63] Fix unit tests to match new is_verbose logic --- test/test_appscale_run_instances.py | 133 +++++++++++++++++----------- test/test_appscale_upload_app.py | 2 +- test/test_remote_helper.py | 4 +- 3 files changed, 84 insertions(+), 55 deletions(-) diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index b4c59450..cfaf446a 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -268,7 +268,7 @@ def test_appscale_in_one_node_virt_deployment(self): with_args("ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet " "-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no " "-o UserKnownHostsFile=/dev/null root@public1 ", - False, 5, + None, 5, stdin="cp /root/appscale/AppController/scripts/appcontroller " "/etc/init.d/") @@ -276,13 +276,13 @@ def test_appscale_in_one_node_virt_deployment(self): with_args("ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet " "-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no " "-o UserKnownHostsFile=/dev/null root@{} ".format(IP_1), - False, 5, stdin="chmod +x /etc/init.d/appcontroller") + None, 5, stdin="chmod +x /etc/init.d/appcontroller") self.local_state.should_receive('shell').\ with_args("ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet " "-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no " "-o UserKnownHostsFile=/dev/null root@public1 ", - False, 5, + None, 5, stdin="cp /root/appscale/AppController/scripts/appcontroller " "/etc/init.d/") @@ -322,33 +322,33 @@ def test_appscale_in_one_node_virt_deployment(self): # mock out copying over the keys self.local_state.should_receive('shell')\ - .with_args(re.compile('^scp .*.key'),False,5) + .with_args(re.compile('^scp .*.key'), None, 5) self.setup_appscale_compatibility_mocks() # mock out generating the private key self.local_state.should_receive('shell')\ - .with_args(re.compile('^openssl'),False,stdin=None)\ + .with_args(re.compile('^openssl'), None, stdin=None)\ .and_return() # mock out removing the old json file self.local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'),False,5,stdin=re.compile('rm -rf'))\ + .with_args(re.compile('^ssh'), None, 5, stdin=re.compile('rm -rf'))\ .and_return() # assume that we started monit fine self.local_state.should_receive('shell')\ - .with_args(re.compile('^ssh'),False,5,stdin=re.compile('monit'))\ + .with_args(re.compile('^ssh'), None, 5, stdin=re.compile('monit'))\ .and_return() self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), None, 5, stdin='service appscale-controller start') self.local_state.should_receive('shell').\ with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' '-o UserKnownHostsFile=/dev/null root@{} '.format(IP_1), - False, 5, + None, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() self.setup_socket_mocks(IP_1) @@ -366,23 +366,23 @@ def test_appscale_in_one_node_virt_deployment(self): locations_file = '{}/locations-bookey.yaml'.\ format(RemoteHelper.CONFIG_DIR) self.local_state.should_receive('shell')\ - .with_args(re.compile('^scp .*{}'.format(locations_file)), False, 5)\ + .with_args(re.compile('^scp .*{}'.format(locations_file)), None, 5)\ .and_return() locations_json = '{}/locations-bookey.json'.\ format(RemoteHelper.CONFIG_DIR) self.local_state.should_receive('shell')\ - .with_args(re.compile('^scp .*{}'.format(locations_json)), False, 5)\ + .with_args(re.compile('^scp .*{}'.format(locations_json)), None, 5)\ .and_return() user_locations = '/root/.appscale/locations-bookey.json' self.local_state.should_receive('shell')\ - .with_args(re.compile('^scp .*{}'.format(user_locations)), False, 5)\ + .with_args(re.compile('^scp .*{}'.format(user_locations)), None, 5)\ .and_return() # Assume the secret key was copied successfully. self.local_state.should_receive('shell')\ - .with_args(re.compile('^scp .*.secret'), False, 5)\ + .with_args(re.compile('^scp .*.secret'), None, 5)\ .and_return() flexmock(AppControllerClient) @@ -468,30 +468,30 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): # assume that root login is not enabled self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin='ls').and_return( + None, 5, stdin='ls').and_return( 'Please login as the user "ubuntu" rather than the user "root"') # assume that we can enable root login self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin='sudo touch /root/.ssh/authorized_keys').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin='sudo chmod 600 /root/.ssh/authorized_keys').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, stdin='mktemp').and_return() + re.compile('ssh'), None, 5, stdin='mktemp').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin=re.compile( 'sudo sort -u ~/.ssh/authorized_keys /root/.ssh/authorized_keys -o ' ) ).and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin=re.compile( 'sudo sed -n ' '\'\/\.\*Please login\/d; w\/root\/\.ssh\/authorized_keys\' ' @@ -499,21 +499,23 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): ).and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, stdin=re.compile('rm -f ') + re.compile('ssh'), None, 5, stdin=re.compile('rm -f ') + ).and_return() + self.local_state.should_receive('shell').with_args( + re.compile('ssh'), None, 5, stdin=re.compile('rm -rf ') ).and_return() # and assume that we can copy over our ssh keys fine self.local_state.should_receive('shell').\ - with_args(re.compile('scp .*[r|d]sa'), False, 5).and_return() + with_args(re.compile('scp .*[r|d]sa'), None, 5).and_return() self.local_state.should_receive('shell').\ - with_args(re.compile('scp .*{0}'.format(self.keyname)), False, 5).\ + with_args(re.compile('scp .*{0}'.format(self.keyname)), None, 5).\ and_return() self.local_state.should_receive('shell').\ with_args('ssh -i /root/.appscale/bookey.key -o LogLevel=quiet -o ' 'NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@public1 ', - False, 5, + '-o UserKnownHostsFile=/dev/null root@public1 ', None, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller ' '/etc/init.d/').\ and_return() @@ -521,8 +523,7 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): self.local_state.should_receive('shell').\ with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@elastic-ip ', - False, 5, + '-o UserKnownHostsFile=/dev/null root@elastic-ip ', None, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller ' '/etc/init.d/').\ and_return() @@ -530,22 +531,22 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): self.local_state.should_receive('shell').\ with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' - '-o UserKnownHostsFile=/dev/null root@elastic-ip ', - False, 5, stdin='chmod +x /etc/init.d/appcontroller').\ + '-o UserKnownHostsFile=/dev/null root@elastic-ip ', None, 5, + stdin='chmod +x /etc/init.d/appcontroller').\ and_return() self.setup_appscale_compatibility_mocks() # mock out generating the private key self.local_state.should_receive('shell').with_args(re.compile('openssl'), - False, stdin=None) + stdin=None) # assume that we started monit fine self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin=re.compile('monit')) + None, 5, stdin=re.compile('monit')) self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), None, 5, stdin='service appscale-controller start') self.setup_socket_mocks('elastic-ip') self.setup_appcontroller_mocks('elastic-ip', 'private1') @@ -560,11 +561,11 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): # copying over the locations yaml and json files should be fine self.local_state.should_receive('shell').with_args(re.compile('scp'), - False, 5, stdin=re.compile('locations-{0}'.format(self.keyname))) + None, 5, stdin=re.compile('locations-{0}'.format(self.keyname))) # same for the secret key self.local_state.should_receive('shell').with_args(re.compile('scp'), - False, 5, stdin=re.compile('{0}.secret'.format(self.keyname))) + None, 5, stdin=re.compile('{0}.secret'.format(self.keyname))) flexmock(RemoteHelper).should_receive('copy_deployment_credentials') flexmock(AppControllerClient) @@ -656,30 +657,30 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): # assume that root login is not enabled self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin='ls').and_return( + None, 5, stdin='ls').and_return( 'Please login as the user "ubuntu" rather than the user "root"') # assume that we can enable root login self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin='sudo touch /root/.ssh/authorized_keys').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin='sudo chmod 600 /root/.ssh/authorized_keys').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, stdin='mktemp').and_return() + re.compile('ssh'), None, 5, stdin='mktemp').and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin=re.compile( 'sudo sort -u ~/.ssh/authorized_keys /root/.ssh/authorized_keys -o ' ) ).and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, + re.compile('ssh'), None, 5, stdin=re.compile( 'sudo sed -n ' '\'\/\.\*Please login\/d; w\/root\/\.ssh\/authorized_keys\' ' @@ -687,27 +688,30 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): ).and_return() self.local_state.should_receive('shell').with_args( - re.compile('ssh'), False, 5, stdin=re.compile('rm -f ') + re.compile('ssh'), None, 5, stdin=re.compile('rm -f ') + ).and_return() + self.local_state.should_receive('shell').with_args( + re.compile('ssh'), None, 5, stdin=re.compile('rm -rf ') ).and_return() # and assume that we can copy over our ssh keys fine self.local_state.should_receive('shell').with_args(re.compile('scp .*[r|d]sa'), - False, 5).and_return() + None, 5).and_return() self.local_state.should_receive('shell').with_args(re.compile('scp .*{0}' - .format(self.keyname)), False, 5).and_return() + .format(self.keyname)), None, 5).and_return() self.setup_appscale_compatibility_mocks() # mock out generating the private key self.local_state.should_receive('shell').with_args(re.compile('openssl'), - False, stdin=None) + None, stdin=None) # assume that we started monit fine self.local_state.should_receive('shell').with_args(re.compile('ssh'), - False, 5, stdin=re.compile('monit')) + None, 5, stdin=re.compile('monit')) self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='service appscale-controller start') + re.compile('^ssh'), None, 5, stdin='service appscale-controller start') self.setup_socket_mocks('public1') self.setup_appcontroller_mocks('public1', 'private1') @@ -722,21 +726,46 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): # copying over the locations json file should be fine self.local_state.should_receive('shell').with_args(re.compile('scp'), - False, 5, stdin=re.compile('locations-{0}'.format(self.keyname))) + None, 5, stdin=re.compile('locations-{0}'.format(self.keyname))) # same for the secret key self.local_state.should_receive('shell').with_args(re.compile('scp'), - False, 5, stdin=re.compile('{0}.secret'.format(self.keyname))) + None, 5, stdin=re.compile('{0}.secret'.format(self.keyname))) - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazbargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() + self.local_state.should_receive('shell').with_args( + 'ssh -i /root/.appscale/boobazbargfoo.key -o LogLevel=quiet ' + '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' + '-o UserKnownHostsFile=/dev/null root@public1 ', None, 5, + stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/' + ).and_return() - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@elastic-ip ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/').and_return() + self.local_state.should_receive('shell').with_args( + 'ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' + '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' + '-o UserKnownHostsFile=/dev/null root@elastic-ip ', None, 5, + stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/' + ).and_return() - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@elastic-ip ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() + self.local_state.should_receive('shell').with_args( + 'ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' + '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' + '-o UserKnownHostsFile=/dev/null root@elastic-ip ', None, 5, + stdin='chmod +x /etc/init.d/appcontroller' + ).and_return() - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/') + self.local_state.should_receive('shell').with_args( + 'ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' + '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' + '-o UserKnownHostsFile=/dev/null root@public1 ', None, 5, + stdin='cp /root/appscale/AppController/scripts/appcontroller /etc/init.d/' + ).and_return() - self.local_state.should_receive('shell').with_args('ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet -o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no -o UserKnownHostsFile=/dev/null root@public1 ', False, 5, stdin='chmod +x /etc/init.d/appcontroller').and_return() + self.local_state.should_receive('shell').with_args( + 'ssh -i /root/.appscale/boobazblargfoo.key -o LogLevel=quiet ' + '-o NumberOfPasswordPrompts=0 -o StrictHostkeyChecking=no ' + '-o UserKnownHostsFile=/dev/null root@public1 ', None, 5, + stdin='chmod +x /etc/init.d/appcontroller' + ).and_return() flexmock(RemoteHelper).should_receive('copy_deployment_credentials') flexmock(AppControllerClient) diff --git a/test/test_appscale_upload_app.py b/test/test_appscale_upload_app.py index 21cb834e..e1fb029e 100644 --- a/test/test_appscale_upload_app.py +++ b/test/test_appscale_upload_app.py @@ -260,7 +260,7 @@ def test_upload_app(self): and_return(head_node) flexmock(LocalState).should_receive('get_secret_key').and_return(secret) flexmock(RemoteHelper).should_receive('copy_app_to_host').\ - with_args(extracted_dir, app_id, self.keyname, False, {}, None).\ + with_args(extracted_dir, app_id, self.keyname, {}, None).\ and_return(source_path) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) diff --git a/test/test_remote_helper.py b/test/test_remote_helper.py index 1cbfdfed..ae517159 100644 --- a/test/test_remote_helper.py +++ b/test/test_remote_helper.py @@ -277,12 +277,12 @@ def test_start_head_node(self): flexmock(RemoteHelper).\ should_receive('run_user_commands').\ with_args('some IP', self.options.user_commands, - self.options.keyname, self.options.verbose).\ + self.options.keyname).\ and_return() flexmock(RemoteHelper).\ should_receive('start_remote_appcontroller').\ - with_args('some IP', self.options.keyname, self.options.verbose).\ + with_args('some IP', self.options.keyname).\ and_return() layout = {} From 4045fc28458f9ee38e145180875f78d4f5b9725c Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Thu, 19 Sep 2019 17:04:24 +0300 Subject: [PATCH 58/63] Add postgres_dsn option --- appscale/tools/local_state.py | 3 ++- appscale/tools/parse_args.py | 2 ++ test/test_appscale_logger.py | 5 +---- test/test_local_state.py | 8 +++++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 1278b775..4771dcc7 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -213,7 +213,8 @@ def generate_deployment_params(cls, options, node_layout, additional_creds): "verbose": str(options.verbose), "flower_password": options.flower_password, "default_max_appserver_memory": str(options.default_max_appserver_memory), - "fdb_clusterfile_content": options.fdb_clusterfile_content + "fdb_clusterfile_content": options.fdb_clusterfile_content, + "postgres_dsn": options.postgres_dsn } creds.update(additional_creds) diff --git a/appscale/tools/parse_args.py b/appscale/tools/parse_args.py index 56cd5c96..4ca26900 100644 --- a/appscale/tools/parse_args.py +++ b/appscale/tools/parse_args.py @@ -327,6 +327,8 @@ def add_allowed_flags(self, function): "starting each AppController") self.parser.add_argument('--fdb_clusterfile_content', help="a string representing content of FoundationDB clusterfile") + self.parser.add_argument('--postgres_dsn', + help="a string representing Postgres DSN string") elif function == "appscale-gather-logs": self.parser.add_argument('--keyname', '-k', default=self.DEFAULT_KEYNAME, help="the keypair name to use") diff --git a/test/test_appscale_logger.py b/test/test_appscale_logger.py index bc1c6db6..755188cb 100644 --- a/test/test_appscale_logger.py +++ b/test/test_appscale_logger.py @@ -1,9 +1,5 @@ -#!/usr/bin/env python - - # General-purpose Python library imports import httplib -import os import sys import unittest @@ -100,6 +96,7 @@ def setUp(self): 'EC2_SECRET_KEY': 'baz', 'EC2_URL': '', 'fdb_clusterfile_content': None, + 'postgres_dsn': None, 'update': ['common'] } diff --git a/test/test_local_state.py b/test/test_local_state.py index 4b4bbf0c..2a6aa862 100644 --- a/test/test_local_state.py +++ b/test/test_local_state.py @@ -105,7 +105,7 @@ def test_generate_deployment_params(self): default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', login_host='public1', aws_subnet_id=None, aws_vpc_id=None, - fdb_clusterfile_content=None, update='code_dir') + fdb_clusterfile_content=None, postgres_dsn=None, update='code_dir') node_layout = NodeLayout({ 'table' : 'cassandra', 'infrastructure' : "ec2", @@ -144,6 +144,7 @@ def test_generate_deployment_params(self): 'aws_subnet_id': None, 'aws_vpc_id': None, 'fdb_clusterfile_content': None, + 'postgres_dsn': None, 'update': 'code_dir' } actual = LocalState.generate_deployment_params(options, node_layout, @@ -164,7 +165,7 @@ def test_generate_deployment_params_no_login(self): default_max_appserver_memory=ParseArgs.DEFAULT_MAX_APPSERVER_MEMORY, EC2_ACCESS_KEY='baz', EC2_SECRET_KEY='baz', EC2_URL='', login_host=None, aws_subnet_id=None, aws_vpc_id=None, - fdb_clusterfile_content=None, update='') + fdb_clusterfile_content=None, postgres_dsn=None, update='') node_layout = NodeLayout({ 'table': 'cassandra', 'infrastructure': "ec2", @@ -202,7 +203,8 @@ def test_generate_deployment_params_no_login(self): 'EC2_URL': '', 'aws_subnet_id': None, 'aws_vpc_id': None, - 'fdb_clusterfile_content': None + 'fdb_clusterfile_content': None, + 'postgres_dsn': None } actual = LocalState.generate_deployment_params(options, node_layout, {'max_spot_price': '1.23'}) From 76d385d76f238d9fcd36a884e54afe89a0f5d0b4 Mon Sep 17 00:00:00 2001 From: Anton Leonov Date: Fri, 20 Sep 2019 17:56:40 +0300 Subject: [PATCH 59/63] Add fdb_clusterfile_content and postgres_dsn to template --- appscale/tools/templates/AppScalefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/appscale/tools/templates/AppScalefile b/appscale/tools/templates/AppScalefile index 92535865..8ed4fda0 100644 --- a/appscale/tools/templates/AppScalefile +++ b/appscale/tools/templates/AppScalefile @@ -190,6 +190,13 @@ ips_layout: # By default, this value is set to 'appscale'. #flower_password: 'appscale' +# Content of FoundationDB clusterfile to be used for establishing connection +# to FoundationDB cluster. +#fdb_clusterfile_content: 'EwFiSLfA:vRPUR2qe@10.10.0.15:4500' + +# DSN string for establishing connection to Postgres database. +#postgres_dsn: 'dbname=pullqueues-db user=appscale password=secure-pwd host=10.10.0.16' + ####################################### From 319bc24121e09f58d886f7263f73cfad4fb08f01 Mon Sep 17 00:00:00 2001 From: Steve Jones Date: Fri, 27 Sep 2019 09:51:35 -0700 Subject: [PATCH 60/63] Systemd monit replace, update run instance tests for changes from master --- test/test_appscale_run_instances.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/test_appscale_run_instances.py b/test/test_appscale_run_instances.py index eb68fb4f..eb84b637 100644 --- a/test/test_appscale_run_instances.py +++ b/test/test_appscale_run_instances.py @@ -333,7 +333,7 @@ def test_appscale_in_one_node_virt_deployment(self): .and_return() self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') + re.compile('^ssh'), None, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks(IP_1) self.setup_appcontroller_mocks(IP_1, IP_1) @@ -503,7 +503,7 @@ def test_appscale_in_one_node_cloud_deployment_auto_spot_price(self): stdin=None) self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') + re.compile('^ssh'), None, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks('elastic-ip') self.setup_appcontroller_mocks('elastic-ip', 'private1') @@ -664,7 +664,7 @@ def test_appscale_in_one_node_cloud_deployment_manual_spot_price(self): None, stdin=None) self.local_state.should_receive('shell').with_args( - re.compile('^ssh'), False, 5, stdin='systemctl start appscale-controller') + re.compile('^ssh'), None, 5, stdin='systemctl start appscale-controller') self.setup_socket_mocks('public1') self.setup_appcontroller_mocks('public1', 'private1') @@ -764,6 +764,9 @@ def test_appscale_in_one_node_virt_deployment_with_login_override(self): AppControllerClient.should_receive('get_property').\ and_return({'login': IP_1}) + flexmock(AppScaleLogger) + AppScaleLogger.should_receive('remote_log_tools_state').and_return() + # don't use a 192.168.X.Y IP here, since sometimes we set our virtual # machines to boot with those addresses (and that can mess up our tests). ips_layout = ONE_NODE_CLUSTER From 813d4a4098fd5e9165680affd5c87c73d766d62a Mon Sep 17 00:00:00 2001 From: Chris Donati Date: Sun, 20 Oct 2019 14:51:46 -0700 Subject: [PATCH 61/63] Bump version to 3.8.1 to match release --- appscale/tools/local_state.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appscale/tools/local_state.py b/appscale/tools/local_state.py index 4771dcc7..26a00578 100644 --- a/appscale/tools/local_state.py +++ b/appscale/tools/local_state.py @@ -31,7 +31,7 @@ # The version of the AppScale Tools we're running on. -APPSCALE_VERSION = "3.8.0" +APPSCALE_VERSION = "3.8.1" class LocalState(object): diff --git a/setup.py b/setup.py index 4a438abd..9636cb97 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ setup( name='appscale-tools', - version='3.8.0', + version='3.8.1', description='A set of command-line tools for interacting with AppScale', long_description=long_description, author='AppScale Systems, Inc.', From 5c6b330a4a8a537b2454e4ecd58e086027fc56ad Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 20 Nov 2019 10:46:18 -0800 Subject: [PATCH 62/63] use keyword params since number of args mismatches --- appscale/tools/appscale_tools.py | 2 +- test/test_appscale_upload_app.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/appscale/tools/appscale_tools.py b/appscale/tools/appscale_tools.py index c79c4f09..a4caa9a0 100644 --- a/appscale/tools/appscale_tools.py +++ b/appscale/tools/appscale_tools.py @@ -1026,7 +1026,7 @@ def upload_app(cls, options): remote_file_path = RemoteHelper.copy_app_to_host( file_location, version.project_id, options.keyname, - extras, custom_service_yaml) + extras=extras, custom_service_yaml=custom_service_yaml) AppScaleLogger.log( 'Deploying service {} for {}'.format(version.service_id, diff --git a/test/test_appscale_upload_app.py b/test/test_appscale_upload_app.py index e1fb029e..09a1b83e 100644 --- a/test/test_appscale_upload_app.py +++ b/test/test_appscale_upload_app.py @@ -260,7 +260,7 @@ def test_upload_app(self): and_return(head_node) flexmock(LocalState).should_receive('get_secret_key').and_return(secret) flexmock(RemoteHelper).should_receive('copy_app_to_host').\ - with_args(extracted_dir, app_id, self.keyname, {}, None).\ + with_args(extracted_dir, app_id, self.keyname, None, {}, None).\ and_return(source_path) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) From 25b56974eb4a661d533a1f5ccb3ea23421f239b4 Mon Sep 17 00:00:00 2001 From: whoarethebritons Date: Wed, 20 Nov 2019 17:16:19 -0800 Subject: [PATCH 63/63] use keywords in test as well --- test/test_appscale_upload_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_appscale_upload_app.py b/test/test_appscale_upload_app.py index 09a1b83e..9f7b91c2 100644 --- a/test/test_appscale_upload_app.py +++ b/test/test_appscale_upload_app.py @@ -260,7 +260,7 @@ def test_upload_app(self): and_return(head_node) flexmock(LocalState).should_receive('get_secret_key').and_return(secret) flexmock(RemoteHelper).should_receive('copy_app_to_host').\ - with_args(extracted_dir, app_id, self.keyname, None, {}, None).\ + with_args(extracted_dir, app_id, self.keyname, extras={}, custom_service_yaml=None).\ and_return(source_path) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id)