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'
Example #5
0
    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
Example #6
0
    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)