def _ask_prompt(question: str, console: io.IO, validate: Optional[Callable[[str], None]] = None, default: Optional[str] = None) -> str: """Used to ask for a single string value. Args: question: Question shown to the user on the console. console: Object to use for user I/O. validate: Function used to check if value provided is valid. It should raise a ValueError if the the value fails to validate. default: Default value if user provides no value. (Presses enter) If default is None, the user must provide an answer that is valid. Returns: The value entered by the user. """ validate = validate or (lambda x: None) while True: answer = console.ask(question) if default and not answer: answer = default try: validate(answer) break except ValueError as e: console.error(e) return answer
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to enter some sort of name. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ default_name = cls._default_name(arguments) while True: console.tell(('{} {}').format(step_prompt, cls._PROMPT)) project_name = console.ask('[{}]: '.format(default_name)) if not project_name.strip(): project_name = default_name try: cls.validate(project_name) except ValueError as e: console.error(e) continue return project_name
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to a Google Cloud Platform project id. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ default_project_id = cls._generate_default_project_id( arguments.get('project_name', None)) while True: console.tell(('{} Enter a Google Cloud Platform Project ID, ' 'or leave blank to use').format(step_prompt)) project_id = console.ask('[{}]: '.format(default_project_id)) if not project_id.strip(): return default_project_id try: cls.validate(project_id) except ValueError as e: console.error(e) continue return project_id
def _binary_prompt(question: str, console: io.IO, default: Optional[bool] = None) -> bool: """Used to prompt user to choose from a yes or no question. Args: question: Question shown to the user on the console. console: Object to use for user I/O. default: Default value if user provides no value. (Presses enter) If default is None the user is forced to choose a value (y/n). Returns: The bool representation of the choice of the user. Yes is True. """ while True: answer = console.ask(question).lower() if default is not None and not answer: return default try: _binary_validate(answer) break except ValueError as e: console.error(e) return answer == 'y'
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to a Google Cloud Platform project id. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ while True: console.tell( ('{} Enter the existing Google Cloud Platform Project ID ' 'to use.').format(step_prompt)) project_id = console.ask('Project ID: ') try: cls.validate(project_id) except ValueError as e: console.error(e) continue return project_id
def handle(cls, console: io.IO): """Attempts to install the requirement. Raises: UnableToAutomaticallyInstall: If the installation fails. """ if shutil.which('gcloud') is None: msg = "Gcloud is needed to install Cloud Sql Proxy" raise UnableToAutomaticallyInstallError(cls.NAME, msg) while True: answer = console.ask('Cloud Sql Proxy is required by Django Cloud ' 'Deploy. Would you like us to install ' 'automatically (Y/n)? ').lower().strip() if answer not in ['y', 'n']: continue if answer == 'n': raise NotImplementedError break try: args = ['components', 'install', 'cloud_sql_proxy'] process = pexpect.spawn('gcloud', args) process.expect('Do you want to continue (Y/n)?') process.sendline('Y') process.expect('Update done!') except (pexpect.exceptions.TIMEOUT, pexpect.exceptions.EOF): dl_link = 'https://cloud.google.com/sql/docs/mysql/sql-proxy' msg = ('Unable to download Cloud Sql Proxy directly from Gcloud. ' 'This is caused when Gcloud was not downloaded directly from' ' https://cloud.google.com/sdk/docs/downloads-interactive\n' 'Please install Cloud SQL Proxy from {}').format(dl_link) raise UnableToAutomaticallyInstallError(cls.NAME, msg) finally: process.close()
def handle(cls, console: io.IO): """Attempts to install the requirement. Raises: UnableToAutomaticallyInstall: If the installation fails. """ if shutil.which('gcloud') is None: msg = "gcloud is needed to install Cloud SQL Proxy" raise UnableToAutomaticallyInstallError(cls.NAME, msg) while True: answer = console.ask('Cloud SQL Proxy is required by Django ' 'Deploy. Would you like us to install it ' 'automatically (Y/n)? ').lower().strip() if answer == 'n': raise NotImplementedError elif answer in ['y', '']: break command = ['gcloud', '-q', 'components', 'install', 'cloud_sql_proxy'] if subprocess.call(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0: raise UnableToAutomaticallyInstallError( cls.NAME, cls._AUTOMATIC_INSTALLATION_ERROR)
def prompt(self, console: io.IO, step: str, args: Dict[str, Any]) -> Dict[str, Any]: """Extracts user arguments through the command-line. Args: console: Object to use for user I/O. step: Message to present to user regarding what step they are on. args: Dictionary holding prompts answered by user and set up command-line arguments. Returns: A Copy of args + the new parameter collected. """ new_args = copy.deepcopy(args) if self._is_valid_passed_arg(console, step, args.get(self.PARAMETER), self._validate): return new_args project_creation_mode = args.get('project_creation_mode') if self._does_project_exist(project_creation_mode): billing_account = self._has_existing_billing_account( console, step, args) if billing_account is not None: new_args[self.PARAMETER] = billing_account return new_args billing_accounts = self.billing_client.list_billing_accounts( only_open_accounts=True) console.tell( ('{} In order to deploy your application, you must enable billing ' 'for your Google Cloud Project.').format(step)) # If the user has existing billing accounts, we let the user pick one if billing_accounts: val = self._handle_existing_billing_accounts( console, billing_accounts) new_args[self.PARAMETER] = val return new_args # If the user does not have existing billing accounts, we direct # the user to create a new one. console.tell('You do not have existing billing accounts.') console.ask('Press [Enter] to create a new billing account.') val = self._get_new_billing_account(console, billing_accounts) new_args[self.PARAMETER] = val return new_args
def handle_crash(err: Exception, command: str, console: io.IO = io.ConsoleIO()): """The tool's crashing handler. Args: err: The exception that was raised. command: The command causing the exception to get thrown, e.g. 'django-cloud-deploy new'. console: Object to use for user I/O. """ # Only handle crashes caused by our code, not user's code. # When deploying, our tool will run the code of user's Django project. # If user's code has a bug, then an UserError will be raised. In this case, # we do not want users to create a Github issue. if any( isinstance(err, exception_class) for exception_class in _DISPLAYABLE_EXCEPTIONS): # https://github.com/google/pytype/issues/225 raise err.__cause__ # pytype: disable=attribute-error log_fd, log_file_path = tempfile.mkstemp( prefix='django-deploy-bug-report-') issue_content = _create_issue_body(command) issue_title = _create_issue_title(err, command) log_file = os.fdopen(log_fd, 'wt') log_file.write(issue_content) log_file.close() console.tell( ('Your "{}" failed due to an internal error.' '\n\n' 'You can report this error by filing a bug on Github. If you agree,\n' 'a browser window will open and an Github issue will be\n' 'pre-populated with the details of this crash.\n' 'For more details, see: {}').format(command, log_file_path)) while True: ans = console.ask('Would you like to file a bug? [y/N]: ') ans = ans.strip().lower() if not ans: # 'N' is default. break if ans in ['y', 'n']: break if ans.lower() == 'y': _create_issue(issue_title, issue_content)
def _multiple_choice_prompt(question: str, options: List[str], console: io.IO, default: Optional[int] = None) -> Optional[int]: """Used to prompt user to choose from a list of values. Args: question: Question shown to the user on the console. Should have a {} to insert a list of enumerated options. options: Possible values user should choose from. console: Object to use for user I/O. default: Default value if user provides no value. (Presses enter) If default is None the user is forced to choose a value in the option list. Typical usage: # User can press enter if user doesn't want anything. choice = _multiple_choice_prompt('Choose an option:\n{}\n', ['Chicken', 'Salad', 'Burger'], console, default=None) Returns: The choice made by the user. If default is none, it is guaranteed to be an index in the options, else it can possible be the default value. """ assert '{}' in question assert len(options) > 0 options_formatted = [ '{}. {}'.format(str(i), opt) for i, opt in enumerate(options, 1) ] options = '\n'.join(options_formatted) while True: answer = console.ask(question.format(options)) if not answer and default: return default try: _multiple_choice_validate(answer, len(options)) break except ValueError as e: console.error(e) return int(answer) - 1
def prompt( cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None ) -> credentials.Credentials: """Prompt the user for access to the Google credentials. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The user's credentials. """ console.tell( ('{} In order to deploy your application, you must allow Django ' 'Deploy to access your Google account.').format(step_prompt)) auth_client = auth.AuthClient() create_new_credentials = True active_account = auth_client.get_active_account() if active_account: # The user has already logged in before while True: ans = console.ask( ('You have logged in with account [{}]. Do you want to ' 'use it? [Y/n]: ').format(active_account)) ans = ans.lower() if ans not in ['y', 'n', '']: continue elif ans in ['y', '']: create_new_credentials = False break if not create_new_credentials: cred = auth_client.get_default_credentials() if cred: return cred return auth_client.create_default_credentials()
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to enter a file system path for their project. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ home_dir = os.path.expanduser('~') # TODO: Remove filesystem-unsafe characters. Implement a validation # method that checks for these. default_directory = os.path.join( home_dir, arguments.get('project_name', 'django-project').lower().replace(' ', '-')) while True: console.tell( ('{} Enter a new directory path to store project source, ' 'or leave blank to use').format(step_prompt)) directory = console.ask('[{}]: '.format(default_directory)) if not directory.strip(): directory = default_directory try: cls.validate(directory) except ValueError as e: console.error(e) continue if os.path.exists(directory): if not cls._prompt_replace(console, directory): continue return directory
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to a Google Cloud Platform project id. If the user supplies the project_id as a flag we want to validate that it exists. We tell the user to supply a new one if it does not. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ project_id = arguments.get('project_id', None) valid_project_id = False while not valid_project_id: if not project_id: console.tell( ('{} Enter the existing Google Cloud Platform Project ID ' 'to use.').format(step_prompt)) project_id = console.ask('Project ID: ') try: cls.validate(project_id, credentials) if (arguments.get( 'project_creation_mode', False) == workflow.ProjectCreationMode.MUST_EXIST): console.tell(('{} Google Cloud Platform Project ID {}' ' is valid').format(step_prompt, project_id)) valid_project_id = True except ValueError as e: console.error(e) project_id = None continue return project_id
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user to enter a file system path for their project. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The value entered by the user. """ home_dir = os.path.expanduser('~') default_directory = os.path.join( home_dir, arguments.get('project_name', 'django-project').lower().replace(' ', '-')) while True: console.tell( ('{} Enter the directory of the Django project you want to ' 'update:'.format(step_prompt))) directory = console.ask('[{}]: '.format(default_directory)) if not directory.strip(): directory = default_directory directory = os.path.abspath(os.path.expanduser(directory)) try: cls.validate(directory) except ValueError as e: console.error(e) continue return directory
def handle(cls, console: io.IO): """Attempts to install the requirement. Raises: UnableToAutomaticallyInstall: If the installation fails. """ gcloud_path = shutil.which('gcloud') if gcloud_path is None: msg = "gcloud is needed to install Cloud SQL Proxy" raise UnableToAutomaticallyInstallError(cls.NAME, msg) while True: answer = console.ask('Cloud SQL Proxy is required by Django ' 'Deploy. Would you like us to install it ' 'automatically (Y/n)? ').lower().strip() if answer == 'n': raise NotImplementedError elif answer in ['y', '']: break command = [ gcloud_path, '-q', 'components', 'install', 'cloud_sql_proxy' ] install_result = subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, universal_newlines=True) if install_result.returncode != 0: if 'gcloud components update' in install_result.stderr: raise UnableToAutomaticallyInstallError( cls.NAME, cls._OLD_GCLOUD_VERSION) elif 'non-interactive mode' in install_result.stderr: raise UnableToAutomaticallyInstallError( cls.NAME, cls._MUST_INSTALL_INTERACTIVE) else: raise UnableToAutomaticallyInstallError( cls.NAME, cls._AUTOMATIC_INSTALLATION_ERROR)
def prompt(cls, console: io.IO, step_prompt: str, arguments: Dict[str, Any], credentials: Optional[credentials.Credentials] = None) -> str: """Prompt the user for a billing account to use for deployment. Args: console: Object to use for user I/O. step_prompt: A prefix showing the current step number e.g. "[1/3]". arguments: The arguments that have already been collected from the user e.g. {"project_id", "project-123"} credentials: The OAuth2 Credentials object to use for api calls during prompt. Returns: The user's billing account name. """ billing_client = billing.BillingClient.from_credentials(credentials) if ('project_creation_mode' in arguments and (arguments['project_creation_mode'] == workflow.ProjectCreationMode.MUST_EXIST)): assert 'project_id' in arguments, 'project_id must be set' project_id = arguments['project_id'] billing_account = (billing_client.get_billing_account(project_id)) if billing_account.get('billingEnabled', False): msg = ('{} Billing is already enabled on this project.'.format( step_prompt)) console.tell(msg) return billing_account.get('billingAccountName') billing_accounts = billing_client.list_billing_accounts( only_open_accounts=True) console.tell( ('{} In order to deploy your application, you must enable billing ' 'for your Google Cloud Project.').format(step_prompt)) # If the user has existing billing accounts, we let the user pick one if billing_accounts: console.tell('You have the following existing billing accounts: ') for i, account_info in enumerate(billing_accounts): console.tell('{}. {}'.format(i + 1, account_info['displayName'])) choice = console.ask( ('Please enter your numeric choice or press [Enter] to create ' 'a new billing account: ')) while True: if not choice: return cls._get_new_billing_account( console, billing_accounts, billing_client) if (not choice.isdigit() or int(choice) <= 0 or int(choice) > len(billing_accounts)): if len(billing_accounts) == 1: choice = console.ask( ('Please enter "1" to use "{}" or press ' '[Enter] to create a new account: ').format( billing_accounts[0]['displayName'])) else: choice = console.ask( ('Please enter a value between 1 and {} or press ' '[Enter] to create a new account: ').format( len(billing_accounts))) else: return billing_accounts[int(choice) - 1]['name'] else: # If the user does not have existing billing accounts, we direct # the user to create a new one. console.tell('You do not have existing billing accounts.') console.ask('Press [Enter] to create a new billing account.') return cls._get_new_billing_account(console, billing_accounts, billing_client)