def _get_new_billing_account(console: io.IO,
                                 existing_billing_accounts: List[Dict[str,
                                                                      Any]],
                                 billing_client: billing.BillingClient) -> str:
        """Ask the user to create a new billing account and return name of it.

        Args:
            console: Object to use for user I/O.
            existing_billing_accounts: User's billing accounts before creation
                of new accounts.
            billing_client: A client to query user's existing billing accounts.

        Returns:
            Name of the user's newly created billing account.
        """
        webbrowser.open('https://console.cloud.google.com/billing/create')
        existing_billing_account_names = [
            account['name'] for account in existing_billing_accounts
        ]
        console.tell('Waiting for billing account to be created.')
        while True:
            billing_accounts = billing_client.list_billing_accounts(
                only_open_accounts=True)
            if len(existing_billing_accounts) != len(billing_accounts):
                billing_account_names = [
                    account['name'] for account in billing_accounts
                ]
                diff = list(
                    set(billing_account_names) -
                    set(existing_billing_account_names))
                return diff[0]
            time.sleep(2)
    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 _is_valid_passed_arg(self, console: io.IO, step: str,
                             value: Optional[str],
                             validate: Callable[[str], None]) -> bool:
        """Checks if the passed in argument via the command line is valid.

        All prompts that collect a parameter should call this function first.
        It uses the validate function of the prompt. The code also
        will process a passed in paramater as a step. This is used to have a
        static amount of steps that is easier to manage.

        Returns:
            A boolean indicating if the passed in argument is valid.
        """
        if value is None:
            return False

        try:
            validate(value)
        except ValueError as e:
            console.error(e)
            quit()

        msg = '{} {}: {}'.format(step, self.PARAMETER, value)
        console.tell(msg)
        return True
    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 enter a password.

        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.
        """
        console_prompt = cls._get_prompt(arguments)
        console.tell(('{} {}').format(step_prompt, console_prompt))
        while True:
            password = console.getpass('Postgres password: ')
            try:
                cls.validate(password)
            except ValueError as e:
                console.error(e)
                continue
            return password
Exemple #6
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
 def _handle_existing_project(self, console: io.IO, step: str,
                              args: Dict[str, Any]) -> str:
     assert 'project_id' in args, 'project_id must be set'
     project_id = args['project_id']
     project_name = self.project_client.get_project(project_id)['name']
     message = '{} {}: {}'.format(step, self.PARAMETER, project_name)
     console.tell(message)
     return project_name
Exemple #8
0
def main(args: argparse.Namespace, console: io.IO = io.ConsoleIO()):

    if not tool_requirements.check_and_handle_requirements(
            console, args.backend):
        return

    prompt_order = [
        'credentials',
        'database_password',
        'django_directory_path',
    ]

    required_parameters_to_prompt = {
        'credentials': prompt.CredentialsPrompt,
        'database_password': prompt.PostgresPasswordUpdatePrompt,
        'django_directory_path': prompt.DjangoFilesystemPathUpdate,
    }

    # Parameters that were *not* provided as command flags.
    remaining_parameters_to_prompt = {}

    actual_parameters = {}

    for parameter_name, prompter in required_parameters_to_prompt.items():
        value = getattr(args, parameter_name, None)
        if value is not None:
            try:
                prompter.validate(value)
            except ValueError as e:
                print(e, file=sys.stderr)
                sys.exit(1)
            actual_parameters[parameter_name] = value
        else:
            remaining_parameters_to_prompt[parameter_name] = prompter

    if remaining_parameters_to_prompt:

        num_steps = len(remaining_parameters_to_prompt)
        console.tell('<b>{} steps to update project</b>'.format(num_steps))
        console.tell()
        parameter_and_prompt = sorted(
            remaining_parameters_to_prompt.items(),
            key=lambda i: prompt_order.index(i[0]))

        for step, (parameter_name, prompter) in enumerate(parameter_and_prompt):
            step = '<b>[{}/{}]</b>'.format(step + 1, num_steps)
            actual_parameters[parameter_name] = prompter.prompt(
                console, step, actual_parameters)

    workflow_manager = workflow.WorkflowManager(
        actual_parameters['credentials'], args.backend)
    workflow_manager.update_project(
        actual_parameters['django_directory_path'],
        actual_parameters['database_password'],
        backend=args.backend)
    def _has_existing_billing_account(self, console: io.IO, step: str,
                                      args: Dict[str, Any]) -> (Optional[str]):
        assert 'project_id' in args, 'project_id must be set'
        project_id = args['project_id']
        billing_account = (self.billing_client.get_billing_account(project_id))
        if not billing_account.get('billingEnabled', False):
            return None

        msg = ('{} Billing is already enabled on this project.'.format(step))
        console.tell(msg)
        return billing_account.get('billingAccountName')
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 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 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:

        if ('project_creation_mode' not in arguments
                or (arguments['project_creation_mode'] !=
                    workflow.ProjectCreationMode.MUST_EXIST)):
            return (super().prompt(console, step_prompt, arguments,
                                   credentials))

        assert 'project_id' in arguments, 'project_id must be set'
        project_id = arguments['project_id']
        project_client = project.ProjectClient.from_credentials(credentials)
        project_name = project_client.get_project(project_id)['name']
        message = 'Project name found: {}'.format(project_name)
        console.tell(('{} {}').format(step_prompt, message))
        return project_name
    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 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 _password_prompt(question: str, console: io.IO) -> str:
    """Used to prompt user to choose a password field.

    Args:
        console: Object to use for user I/O.
        question: Question shown to the user on the console.

    Returns:
        The password provided by the user.
    """
    console.tell(question)
    while True:
        password1 = console.getpass('Password: '******'Password (again): ')
        if password1 != password2:
            console.error('Passwords do not match, please try again')
            continue
        return password1
    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),
                                     lambda x: x):
            return new_args

        console.tell(
            ('{} In order to deploy your application, you must allow Django '
             'Deploy to access your Google account.').format(step))
        create_new_credentials = True
        active_account = self.auth_client.get_active_account()

        if active_account:  # The user has already logged in before
            msg = ('You have logged in with account [{}]. Do you want to '
                   'use it? [Y/n]: ').format(active_account)
            use_active_credentials = _binary_prompt(msg, console, default='Y')
            create_new_credentials = not use_active_credentials

        if create_new_credentials:
            creds = self.auth_client.create_default_credentials()
        else:
            creds = self.auth_client.get_default_credentials()

        new_args[self.PARAMETER] = creds
        return new_args
Exemple #19
0
def main(args: argparse.Namespace, console: io.IO = io.ConsoleIO()):

    if not tool_requirements.check_and_handle_requirements(
            console, args.backend):
        return

    prompt_order = [
        'credentials',
        'project_id',
        'project_name',
        'billing_account_name',
        'database_password',
        'django_directory_path',
        'django_project_name',
        'django_app_name',
        'django_superuser_login',
        'django_superuser_password',
        'django_superuser_email',
    ]

    required_parameters_to_prompt = {
        'credentials': prompt.CredentialsPrompt,
        'project_id': prompt.ProjectIdPrompt,
        'project_name': prompt.GoogleCloudProjectNamePrompt,
        'billing_account_name': prompt.BillingPrompt,
        'database_password': prompt.PostgresPasswordPrompt,
        'django_directory_path': prompt.DjangoFilesystemPath,
        'django_project_name': prompt.DjangoProjectNamePrompt,
        'django_app_name': prompt.DjangoAppNamePrompt,
        'django_superuser_login': prompt.DjangoSuperuserLoginPrompt,
        'django_superuser_password': prompt.DjangoSuperuserPasswordPrompt,
        'django_superuser_email': prompt.DjangoSuperuserEmailPrompt
    }

    # Parameters that were *not* provided as command flags.
    remaining_parameters_to_prompt = {}

    actual_parameters = {
        'project_creation_mode': workflow.ProjectCreationMode.CREATE,
        'bucket_name': getattr(args, 'bucket_name', None),
        'service_accounts': getattr(args, 'service_accounts', None),
        'services': getattr(args, 'services', None)
    }

    for parameter_name, prompter in required_parameters_to_prompt.items():
        value = getattr(args, parameter_name, None)
        if value is not None:
            try:
                prompter.validate(value)
            except ValueError as e:
                print(e, file=sys.stderr)
                sys.exit(1)
            actual_parameters[parameter_name] = value
        else:
            remaining_parameters_to_prompt[parameter_name] = prompter

    if args.use_existing_project:
        actual_parameters['project_creation_mode'] = (
            workflow.ProjectCreationMode.MUST_EXIST)
        remaining_parameters_to_prompt['project_name'] = (
            prompt.GoogleCloudProjectNamePrompt)
        remaining_parameters_to_prompt['project_id'] = (
            prompt.ExistingProjectIdPrompt)

    if remaining_parameters_to_prompt:
        num_steps = len(remaining_parameters_to_prompt)
        console.tell(
            '<b>{} steps to setup your new project</b>'.format(num_steps))
        console.tell()
        parameter_and_prompt = sorted(remaining_parameters_to_prompt.items(),
                                      key=lambda i: prompt_order.index(i[0]))

        for step, (parameter_name,
                   prompter) in enumerate(parameter_and_prompt):
            step = '<b>[{}/{}]</b>'.format(step + 1, num_steps)
            actual_parameters[parameter_name] = prompter.prompt(
                console, step, actual_parameters,
                actual_parameters.get('credentials', None))

    workflow_manager = workflow.WorkflowManager(
        actual_parameters['credentials'], args.backend)

    try:
        admin_url = workflow_manager.create_and_deploy_new_project(
            project_name=actual_parameters['project_name'],
            project_id=actual_parameters['project_id'],
            project_creation_mode=actual_parameters['project_creation_mode'],
            billing_account_name=actual_parameters['billing_account_name'],
            django_project_name=actual_parameters['django_project_name'],
            django_app_name=actual_parameters['django_app_name'],
            django_superuser_name=actual_parameters['django_superuser_login'],
            django_superuser_email=actual_parameters['django_superuser_email'],
            django_superuser_password=actual_parameters[
                'django_superuser_password'],
            django_directory_path=actual_parameters['django_directory_path'],
            database_password=actual_parameters['database_password'],
            required_services=actual_parameters['services'],
            required_service_accounts=actual_parameters['service_accounts'],
            cloud_storage_bucket_name=actual_parameters['bucket_name'],
            backend=args.backend)
        return admin_url
    except workflow.ProjectExistsError:
        console.error('A project with id "{}" already exists'.format(
            actual_parameters['project_id']))
    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)