def update_dispatch(cls, source_location, keyname, project_id): """ 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 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_rules = utils.dispatch_from_yaml(source_location, fetch_function) if dispatch_rules is None: return 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(version.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(version.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 : {}".format(dispatch_rules)) AppScaleLogger.success('Dispatch has been updated for {}'.format( version.project_id))
def update_queues(cls, source_location, keyname, project_id): """ Updates a project's queues from the configuration file. Args: source_location: A string specifying the location of the source code. keyname: A string specifying the key name. project_id: A string specifying the project ID. """ 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 queue_config = fetch_function('queue.yaml', source_location) if queue_config is None: queue_config = fetch_function('queue.xml', source_location) # If the source does not have a queue configuration file, do nothing. if queue_config is None: return queues = utils.queues_from_xml(queue_config) else: 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 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) admin_client.update_queues(version.project_id, queues)
def test_upload_app(self): app_id = 'guestbook' source_path = '{}.tar.gz'.format(app_id) extracted_dir = '/tmp/{}'.format(app_id) head_node = '192.168.33.10' secret = 'secret-key' operation_id = 'operation-1' port = 8080 version_url = 'http://{}:{}'.format(head_node, port) argv = ['--keyname', self.keyname, '--file', source_path, '--test'] options = ParseArgs(argv, self.function).args version = Version('python27', 'app.yaml') version.project_id = app_id flexmock(LocalState).should_receive('extract_tgz_app_to_dir').\ 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_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).\ and_return(source_path) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) flexmock(AdminClient).should_receive('get_operation').\ and_return({'done': True, 'response': {'versionUrl': version_url}}) flexmock(shutil).should_receive('rmtree').with_args(extracted_dir) flexmock(AppEngineHelper).should_receive('warn_if_version_defined') given_host, given_port = AppScaleTools.upload_app(options) self.assertEquals(given_host, head_node) self.assertEquals(given_port, port) # If provided user is not app admin, deployment should fail. flexmock(AdminClient).should_receive('create_version').\ and_raise(AdminError) self.assertRaises(AdminError, AppScaleTools.upload_app, options) # An application with the PHP runtime should be deployed successfully. version = Version('php', 'app.yaml') version.project_id = app_id flexmock(Version).should_receive('from_tar_gz').and_return(version) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) given_host, given_port = AppScaleTools.upload_app(options) self.assertEquals(given_host, head_node) self.assertEquals(given_port, port)
def test_from_directory(self): # Ensure an exception is raised if there are no configuration candidates. shortest_path_func = 'appscale.tools.admin_api.version.shortest_directory_path' with patch(shortest_path_func, side_effect=lambda fn, path: None): with self.assertRaises(AppEngineConfigException): Version.from_directory('/example/guestbook') with patch(shortest_path_func, side_effect=lambda fn, path: '/example/guestbook/app.yaml'): open_path = 'appscale.tools.admin_api.version.open' with patch(open_path, mock_open(read_data=SIMPLE_APP_YAML)): version = Version.from_yaml_file('/example/app.yaml') self.assertEqual(version.runtime, 'python27')
def test_from_xml_file(self): tree = MagicMock() tree.getroot.return_value = ElementTree.fromstring(SIMPLE_AE_WEB_XML) with patch.object(ElementTree, 'parse', return_value=tree): version = Version.from_xml_file('/example/appengine-web.xml') self.assertEqual(version.runtime, 'java')
def update_cron(cls, source_location, keyname, project_id): """ Updates a project's cron jobs from the configuration file. Args: source_location: A string specifying the location of the source code. keyname: A string specifying the key name. project_id: A string specifying the project ID. """ 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 cron_config = fetch_function('cron.yaml', source_location) if cron_config is None: cron_config = fetch_function('cron.xml', source_location) # If the source does not have a cron configuration file, do nothing. if cron_config is None: return cron_jobs = utils.cron_from_xml(cron_config) else: cron_jobs = yaml.safe_load(cron_config) AppScaleLogger.log('Updating cron jobs') 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) admin_client.update_cron(version.project_id, cron_jobs)
def start_service(cls, options): """Instructs AppScale to start the named service. This is applicable for services using manual scaling. Args: options: A Namespace that has fields for each parameter that can be passed in via the command-line interface. Raises: AppScaleException: If the named service isn't running in this AppScale cloud, or if start is not valid for the service. """ load_balancer_ip = LocalState.get_host_with_role( options.keyname, 'load_balancer') secret = LocalState.get_secret_key(options.keyname) admin_client = AdminClient(load_balancer_ip, secret) version = Version(None, None) version.project_id = options.project_id version.service_id = options.service_id or DEFAULT_SERVICE version.id = DEFAULT_VERSION version.serving_status = 'SERVING' admin_client.patch_version(version, ['servingStatus']) AppScaleLogger.success('Start requested for {}.'.format(options.project_id))
def stop_service(cls, options): """Instructs AppScale to stop the named service. This is applicable for services using manual scaling. Args: options: A Namespace that has fields for each parameter that can be passed in via the command-line interface. Raises: AppScaleException: If the named service isn't running in this AppScale cloud, or if stop is not valid for the service. """ if not options.confirm: response = raw_input( 'Are you sure you want to stop this service? (y/N) ') if response.lower() not in ['y', 'yes']: raise AppScaleException("Cancelled service stop.") load_balancer_ip = LocalState.get_host_with_role( options.keyname, 'load_balancer') secret = LocalState.get_secret_key(options.keyname) admin_client = AdminClient(load_balancer_ip, secret) version = Version(None, None) version.project_id = options.project_id version.service_id = options.service_id or DEFAULT_SERVICE version.id = DEFAULT_VERSION version.serving_status = 'STOPPED' admin_client.patch_version(version, ['servingStatus']) AppScaleLogger.success('Stop requested for {}.'.format(options.project_id))
def test_upload_app(self): app_id = 'guestbook' source_path = '{}.tar.gz'.format(app_id) extracted_dir = '/tmp/{}'.format(app_id) head_node = '192.168.33.10' secret = 'secret-key' operation_id = 'operation-1' port = 8080 version_url = 'http://{}:{}'.format(head_node, port) argv = ['--keyname', self.keyname, '--file', source_path, '--test'] options = ParseArgs(argv, self.function).args version = Version('python27', 'app.yaml') version.project_id = app_id flexmock(LocalState).should_receive('extract_tgz_app_to_dir').\ 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_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, {}, None).\ and_return(source_path) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) flexmock(AdminClient).should_receive('get_operation').\ and_return({'done': True, 'response': {'versionUrl': version_url}}) flexmock(shutil).should_receive('rmtree').with_args(extracted_dir) flexmock(AppEngineHelper).should_receive('warn_if_version_defined') given_host, given_port = AppScaleTools.upload_app(options) self.assertEquals(given_host, head_node) self.assertEquals(given_port, port) # If provided user is not app admin, deployment should fail. flexmock(AdminClient).should_receive('create_version').\ and_raise(AdminError) self.assertRaises(AdminError, AppScaleTools.upload_app, options) # An application with the PHP runtime should be deployed successfully. version = Version('php', 'app.yaml') version.project_id = app_id flexmock(Version).should_receive('from_tar_gz').and_return(version) flexmock(AdminClient).should_receive('create_version').\ and_return(operation_id) given_host, given_port = AppScaleTools.upload_app(options) self.assertEquals(given_host, head_node) self.assertEquals(given_port, port)
def test_from_contents(self): version = Version.from_contents(SIMPLE_APP_YAML, 'app.yaml') self.assertEqual(version.runtime, 'python27') version = Version.from_contents(SIMPLE_AE_WEB_XML, 'appengine-web.xml') self.assertEqual(version.runtime, 'java')
def test_from_yaml(self): # Ensure an exception is raised if runtime is missing. with self.assertRaises(AppEngineConfigException): Version.from_yaml({}) # Ensure runtime string is parsed successfully. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.runtime, 'python27') # Ensure project is parsed successfully. yaml_with_project = SIMPLE_APP_YAML + 'application: guestbook\n' app_yaml = yaml.safe_load(yaml_with_project) version = Version.from_yaml(app_yaml) self.assertEqual(version.runtime, 'python27') self.assertEqual(version.project_id, 'guestbook') # Ensure a default service ID is set. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.service_id, 'default') # Ensure service ID is parsed correctly. yaml_with_module = SIMPLE_APP_YAML + 'module: service1\n' app_yaml = yaml.safe_load(yaml_with_module) version = Version.from_yaml(app_yaml) self.assertEqual(version.service_id, 'service1') # Ensure omitted environment variables are handled correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertDictEqual(version.env_variables, {}) # Ensure environment variables are parsed correctly. env_vars = """ env_variables: VAR1: 'foo' """.lstrip() app_yaml = yaml.safe_load(SIMPLE_APP_YAML + env_vars) version = Version.from_yaml(app_yaml) self.assertDictEqual(version.env_variables, {'VAR1': 'foo'}) # Ensure omitted inbound services are handled correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertListEqual(version.inbound_services, []) # Ensure inbound services are parsed correctly. inbound_services = """ inbound_services: - mail - warmup """.lstrip() app_yaml = yaml.safe_load(SIMPLE_APP_YAML + inbound_services) version = Version.from_yaml(app_yaml) self.assertListEqual(version.inbound_services, ['mail', 'warmup']) # Check empty threadsafe value for non-applicable runtime. app_yaml = yaml.safe_load( 'runtime: go\nhandlers:\n- url: .*\n script: _go_app\n') version = Version.from_yaml(app_yaml) self.assertIsNone(version.threadsafe) # Check empty threadsafe value for applicable runtime. app_yaml = yaml.safe_load('runtime: python27\n') with self.assertRaises(AppEngineConfigException): Version.from_yaml(app_yaml) # Ensure threadsafe value is parsed correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.threadsafe, True)
def test_from_xml(self): # Check the default runtime string for Java apps. # TODO: This should be updated when the Admin API accepts 'java7'. appengine_web_xml = ElementTree.fromstring(SIMPLE_AE_WEB_XML) version = Version.from_xml(appengine_web_xml) self.assertEqual(version.runtime, 'java') xml_with_project = AE_WEB_XML_TEMPLATE.format( '<application>guestbook</application>') appengine_web_xml = ElementTree.fromstring(xml_with_project) version = Version.from_xml(appengine_web_xml) self.assertEqual(version.runtime, 'java') self.assertEqual(version.project_id, 'guestbook') # Ensure a default service ID is set. appengine_web_xml = ElementTree.fromstring(SIMPLE_AE_WEB_XML) version = Version.from_xml(appengine_web_xml) self.assertEqual(version.service_id, 'default') # Ensure service ID is parsed correctly. xml_with_module = AE_WEB_XML_TEMPLATE.format('<module>service1</module>') appengine_web_xml = ElementTree.fromstring(xml_with_module) version = Version.from_xml(appengine_web_xml) self.assertEqual(version.service_id, 'service1') # Ensure omitted environment variables are handled correctly. appengine_web_xml = ElementTree.fromstring(SIMPLE_AE_WEB_XML) version = Version.from_xml(appengine_web_xml) self.assertDictEqual(version.env_variables, {}) # Ensure environment variables are parsed correctly. env_vars = """ <env-variables> <env-var name="VAR1" value="foo" /> </env-variables> """.lstrip() appengine_web_xml = ElementTree.fromstring( AE_WEB_XML_TEMPLATE.format(env_vars)) version = Version.from_xml(appengine_web_xml) self.assertDictEqual(version.env_variables, {'VAR1': 'foo'}) # Ensure omitted inbound services are handled correctly. appengine_web_xml = ElementTree.fromstring(SIMPLE_AE_WEB_XML) version = Version.from_xml(appengine_web_xml) self.assertListEqual(version.inbound_services, []) # Ensure inbound services are parsed correctly. env_vars = """ <inbound-services> <service>mail</service> </inbound-services> """.lstrip() appengine_web_xml = ElementTree.fromstring( AE_WEB_XML_TEMPLATE.format(env_vars)) version = Version.from_xml(appengine_web_xml) self.assertListEqual(version.inbound_services, ['mail']) # Check empty threadsafe value. appengine_web_xml = ElementTree.fromstring( '<?xml version="1.0" encoding="utf-8"?>' '<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">' '</appengine-web-app>') with self.assertRaises(AppEngineConfigException): Version.from_xml(appengine_web_xml) # Ensure threadsafe value is parsed correctly. appengine_web_xml = ElementTree.fromstring(SIMPLE_AE_WEB_XML) version = Version.from_xml(appengine_web_xml) self.assertEqual(version.threadsafe, True)
def test_from_yaml(self): # Ensure an exception is raised if runtime is missing. with self.assertRaises(AppEngineConfigException): Version.from_yaml({}) # Ensure runtime string is parsed successfully. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.runtime, 'python27') # Ensure project is parsed successfully. yaml_with_project = SIMPLE_APP_YAML + 'application: guestbook\n' app_yaml = yaml.safe_load(yaml_with_project) version = Version.from_yaml(app_yaml) self.assertEqual(version.runtime, 'python27') self.assertEqual(version.project_id, 'guestbook') # Ensure a default service ID is set. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.service_id, 'default') # Ensure service ID is parsed correctly. yaml_with_module = SIMPLE_APP_YAML + 'module: service1\n' app_yaml = yaml.safe_load(yaml_with_module) version = Version.from_yaml(app_yaml) self.assertEqual(version.service_id, 'service1') # Ensure omitted environment variables are handled correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertDictEqual(version.env_variables, {}) # Ensure environment variables are parsed correctly. env_vars = """ env_variables: VAR1: 'foo' """.lstrip() app_yaml = yaml.safe_load(SIMPLE_APP_YAML + env_vars) version = Version.from_yaml(app_yaml) self.assertDictEqual(version.env_variables, {'VAR1': 'foo'}) # Ensure omitted inbound services are handled correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertListEqual(version.inbound_services, []) # Ensure inbound services are parsed correctly. inbound_services = """ inbound_services: - mail - warmup """.lstrip() app_yaml = yaml.safe_load(SIMPLE_APP_YAML + inbound_services) version = Version.from_yaml(app_yaml) self.assertListEqual(version.inbound_services, ['mail', 'warmup']) # Check empty threadsafe value for non-applicable runtime. app_yaml = yaml.safe_load('runtime: go\n') version = Version.from_yaml(app_yaml) self.assertIsNone(version.threadsafe) # Check empty threadsafe value for applicable runtime. app_yaml = yaml.safe_load('runtime: python27\n') with self.assertRaises(AppEngineConfigException): Version.from_yaml(app_yaml) # Ensure threadsafe value is parsed correctly. app_yaml = yaml.safe_load(SIMPLE_APP_YAML) version = Version.from_yaml(app_yaml) self.assertEqual(version.threadsafe, True)
def test_from_yaml_file(self): open_path = 'appscale.tools.admin_api.version.open' with patch(open_path, mock_open(read_data=SIMPLE_APP_YAML)): version = Version.from_yaml_file('/example/app.yaml') self.assertEqual(version.runtime, 'python27')
def upload_app(cls, options): """Uploads the given App Engine application into AppScale. Args: options: A Namespace that has fields for each parameter that can be passed in via the command-line interface. Returns: A tuple containing the host and port where the application is serving traffic from. """ custom_service_yaml = None if cls.TAR_GZ_REGEX.search(options.file): 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) created_dir = True version = Version.from_zip(options.file) elif os.path.isdir(options.file): file_location = options.file created_dir = False version = Version.from_directory(options.file) elif options.file.endswith('.yaml'): file_location = os.path.dirname(options.file) created_dir = False version = Version.from_yaml_file(options.file) custom_service_yaml = options.file else: raise AppEngineConfigException('{0} is not a tar.gz file, a zip file, ' \ 'or a directory. Please try uploading either a tar.gz file, a zip ' \ 'file, or a directory.'.format(options.file)) if options.project: if version.runtime == 'java': raise BadConfigurationException("AppScale doesn't support --project for" "Java yet. Please specify the application id in appengine-web.xml.") version.project_id = options.project if version.project_id is None: if version.config_type == 'app.yaml': message = 'Specify --project or define "application" in your app.yaml' else: message = 'Define "application" in your appengine-web.xml' raise AppEngineConfigException(message) # Let users know that versions are not supported yet. AppEngineHelper.warn_if_version_defined(version, options.test) AppEngineHelper.validate_app_id(version.project_id) extras = {} if version.runtime == 'go': extras = LocalState.get_extra_go_dependencies(options.file, options.test) if (version.runtime == 'java' and AppEngineHelper.is_sdk_mismatch(file_location)): AppScaleLogger.warn( 'AppScale did not find the correct SDK jar versions in your app. The ' 'current supported SDK version is ' '{}.'.format(AppEngineHelper.SUPPORTED_SDK_VERSION)) head_node_public_ip = LocalState.get_host_with_role( options.keyname, 'shadow') secret_key = LocalState.get_secret_key(options.keyname) admin_client = AdminClient(head_node_public_ip, secret_key) remote_file_path = RemoteHelper.copy_app_to_host( file_location, version.project_id, options.keyname, extras, custom_service_yaml) AppScaleLogger.log( 'Deploying service {} for {}'.format(version.service_id, version.project_id)) operation_id = admin_client.create_version(version, remote_file_path) # now that we've told the AppController to start our app, find out what port # the app is running on and wait for it to start serving AppScaleLogger.log("Please wait for your app to start serving.") deadline = time.time() + cls.MAX_OPERATION_TIME while True: if time.time() > deadline: raise AppScaleException('The deployment operation took too long.') operation = admin_client.get_operation(version.project_id, operation_id) if not operation['done']: time.sleep(1) continue if 'error' in operation: raise AppScaleException(operation['error']['message']) version_url = operation['response']['versionUrl'] break AppScaleLogger.success( 'Your app can be reached at the following URL: {}'.format(version_url)) if created_dir: shutil.rmtree(file_location) match = re.match('http://(.+):(\d+)', version_url) login_host = match.group(1) http_port = int(match.group(2)) return login_host, http_port