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
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
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
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)