feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,351 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Contacts utilties for Cloud Domains commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import sys
from apitools.base.protorpclite import messages as _messages
from googlecloudsdk.api_lib.domains import registrations
from googlecloudsdk.command_lib.domains import flags
from googlecloudsdk.command_lib.domains import util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.resource import resource_printer
def ParseContactData(api_version, path):
"""Parses contact data from a yaml file."""
domains_messages = registrations.GetMessagesModule(api_version)
class ContactData(_messages.Message):
"""Message that should be present in YAML file with contacts data."""
# pylint: disable=invalid-name
allContacts = _messages.MessageField(domains_messages.Contact, 1)
registrantContact = _messages.MessageField(domains_messages.Contact, 2)
adminContact = _messages.MessageField(domains_messages.Contact, 3)
technicalContact = _messages.MessageField(domains_messages.Contact, 4)
contacts = util.ParseMessageFromYamlFile(
path, ContactData,
'Contact data file \'{}\' does not contain valid contact messages'.format(
path))
if not contacts:
return None
parsed_contact = None
if contacts.allContacts:
for field in ['registrantContact', 'adminContact', 'technicalContact']:
if contacts.get_assigned_value(field):
raise exceptions.Error(
('Contact data file \'{}\' cannot contain both '
'allContacts and {} fields.').format(path, field))
parsed_contact = domains_messages.ContactSettings(
registrantContact=contacts.allContacts,
adminContact=contacts.allContacts,
technicalContact=contacts.allContacts)
else:
parsed_contact = domains_messages.ContactSettings(
registrantContact=contacts.registrantContact,
adminContact=contacts.adminContact,
technicalContact=contacts.technicalContact)
return parsed_contact
def PromptForContacts(api_version, current_contacts=None):
"""Interactively prompts for Whois Contact information."""
domains_messages = registrations.GetMessagesModule(api_version)
create_call = (current_contacts is None)
if not console_io.PromptContinue(
'Contact data not provided using the --contact-data-from-file flag.',
prompt_string='Do you want to enter it interactively',
default=create_call):
return None
if create_call:
contact = _PromptForSingleContact(domains_messages)
return domains_messages.ContactSettings(
registrantContact=contact,
adminContact=contact,
technicalContact=contact)
choices = [
'all the contacts to the same value', 'registrant contact',
'admin contact', 'technical contact'
]
# TODO(b/166210862): Make it a loop.
index = console_io.PromptChoice(
options=choices,
cancel_option=True,
default=0,
message='Which contact do you want to change?')
if index == 0:
contact = _PromptForSingleContact(domains_messages,
current_contacts.registrantContact)
return domains_messages.ContactSettings(
registrantContact=contact,
adminContact=contact,
technicalContact=contact)
if index == 1:
contact = _PromptForSingleContact(domains_messages,
current_contacts.registrantContact)
return domains_messages.ContactSettings(registrantContact=contact)
if index == 2:
contact = _PromptForSingleContact(domains_messages,
current_contacts.adminContact)
return domains_messages.ContactSettings(adminContact=contact)
if index == 3:
contact = _PromptForSingleContact(domains_messages,
current_contacts.technicalContact)
return domains_messages.ContactSettings(technicalContact=contact)
return None
def _PromptForSingleContact(domains_messages, unused_current_contact=None):
"""Asks a user for a single contact data."""
contact = domains_messages.Contact()
contact.postalAddress = domains_messages.PostalAddress()
# TODO(b/166210862): Use defaults from current_contact.
# But then: How to clear a value?
# TODO(b/166210862): Better validation: Call validate_only after each prompt.
contact.postalAddress.recipients.append(
util.PromptWithValidator(
validator=util.ValidateNonEmpty,
error_message=' Name must not be empty.',
prompt_string='Full name: '))
contact.postalAddress.organization = console_io.PromptResponse(
'Organization (if applicable): ')
contact.email = util.PromptWithValidator(
validator=util.ValidateEmail,
error_message=' Invalid email address.',
prompt_string='Email',
default=properties.VALUES.core.account.Get())
contact.phoneNumber = util.PromptWithValidator(
validator=util.ValidateNonEmpty,
error_message=' Phone number must not be empty.',
prompt_string='Phone number: ',
message='Enter phone number with country code, e.g. "+1.8005550123".')
contact.faxNumber = util.Prompt(
prompt_string='Fax number (if applicable): ',
message='Enter fax number with country code, e.g. "+1.8005550123".')
contact.postalAddress.regionCode = util.PromptWithValidator(
validator=util.ValidateRegionCode,
error_message=(
' Country / Region code must be in ISO 3166-1 format, e.g. "US" or '
'"PL".\n See https://support.google.com/business/answer/6270107 for a'
' list of valid choices.'),
prompt_string='Country / Region code: ',
message='Enter two-letter Country / Region code, e.g. "US" or "PL".')
if contact.postalAddress.regionCode != 'US':
log.status.Print('Refer to the guidelines for entering address field '
'information at '
'https://support.google.com/business/answer/6397478.')
contact.postalAddress.postalCode = console_io.PromptResponse(
'Postal / ZIP code: ')
contact.postalAddress.administrativeArea = console_io.PromptResponse(
'State / Administrative area (if applicable): ')
contact.postalAddress.locality = console_io.PromptResponse(
'City / Locality: ')
contact.postalAddress.addressLines.append(
util.PromptWithValidator(
validator=util.ValidateNonEmpty,
error_message=' Address Line 1 must not be empty.',
prompt_string='Address Line 1: '))
optional_address_lines = []
address_line_num = 2
while len(optional_address_lines) < 4:
address_line_num = 2 + len(optional_address_lines)
address_line = console_io.PromptResponse(
'Address Line {} (if applicable): '.format(address_line_num))
if not address_line:
break
optional_address_lines += [address_line]
if optional_address_lines:
contact.postalAddress.addressLines.extend(optional_address_lines)
return contact
def ParseContactPrivacy(api_version, contact_privacy):
domains_messages = registrations.GetMessagesModule(api_version)
if contact_privacy is None:
return None
return flags.ContactPrivacyEnumMapper(domains_messages).GetEnumForChoice(
contact_privacy)
def PromptForContactPrivacy(api_version, choices, current_privacy=None):
"""Asks a user for Contacts Privacy.
Args:
api_version: Cloud Domains API version to call.
choices: List of privacy choices.
current_privacy: Current privacy. Should be nonempty in update calls.
Returns:
Privacy enum or None if the user cancelled.
"""
if not choices:
raise exceptions.Error('Could not find supported contact privacy.')
domains_messages = registrations.GetMessagesModule(api_version)
# Sort the choices according to the privacy strength.
choices.sort(key=flags.PrivacyChoiceStrength, reverse=True)
if current_privacy:
if len(choices) == 1:
log.status.Print(
'Your current contact privacy is {}. It cannot be changed.'.format(
current_privacy))
return None
else:
update = console_io.PromptContinue(
'Your current contact privacy is {}.'.format(current_privacy),
'Do you want to change it',
default=False)
if not update:
return None
current_choice = 0
for ix, privacy in enumerate(choices):
if privacy == flags.ContactPrivacyEnumMapper(
domains_messages).GetChoiceForEnum(current_privacy):
current_choice = ix
else:
current_choice = 0 # The strongest available privacy
if len(choices) == 1:
ack = console_io.PromptContinue(
'The only supported contact privacy is {}.'.format(choices[0]),
default=True)
if not ack:
return None
return ParseContactPrivacy(api_version, choices[0])
else:
index = console_io.PromptChoice(
options=choices,
default=current_choice,
message='Specify contact privacy')
return ParseContactPrivacy(api_version, choices[index])
def ParsePublicContactsAck(api_version, notices):
"""Parses Contact Notices. Returns public_contact_ack enum or None."""
domains_messages = registrations.GetMessagesModule(api_version)
if notices is None:
return False
for notice in notices:
enum = flags.ContactNoticeEnumMapper(domains_messages).GetEnumForChoice(
notice)
# pylint: disable=line-too-long
if enum == domains_messages.ConfigureContactSettingsRequest.ContactNoticesValueListEntryValuesEnum.PUBLIC_CONTACT_DATA_ACKNOWLEDGEMENT:
return enum
return None
def MergeContacts(api_version, prev_contacts, new_contacts):
domains_messages = registrations.GetMessagesModule(api_version)
if new_contacts is None:
new_contacts = domains_messages.ContactSettings()
return domains_messages.ContactSettings(
registrantContact=(new_contacts.registrantContact or
prev_contacts.registrantContact),
adminContact=(new_contacts.adminContact or prev_contacts.adminContact),
technicalContact=(new_contacts.technicalContact or
prev_contacts.technicalContact))
def _SimplifyContacts(contacts):
"""Returns one contact if all 3 contacts are equal, and all 3 contacts otherwise."""
if contacts.registrantContact == contacts.adminContact and contacts.registrantContact == contacts.technicalContact:
return contacts.registrantContact
return contacts
def PromptForPublicContactsAck(domain, contacts, print_format='default'):
"""Asks a user for Public Contacts Ack.
Args:
domain: Domain name.
contacts: Current Contacts. All 3 contacts should be present.
print_format: Print format, e.g. 'default' or 'yaml'.
Returns:
Boolean: whether the user accepted the notice or not.
"""
log.status.Print(
'You choose to make contact data of domain {} public.\n'
'Anyone who looks it up in the WHOIS directory will be able to see info\n'
'for the domain owner and administrative and technical contacts.\n'
'Make sure it\'s ok with them that their contact data is public.\n'
'This info will be publicly available:'.format(domain))
contacts = _SimplifyContacts(contacts)
resource_printer.Print(contacts, print_format, out=sys.stderr)
return console_io.PromptContinue(
message=None, default=False, throw_if_unattended=True, cancel_on_no=True)
# TODO(b/110398579): Integrate with ARI.
def PromptForPublicContactsUpdateAck(domain, contacts, print_format='default'):
"""Asks a user for Public Contacts Ack when the user updates contact settings.
Args:
domain: Domain name.
contacts: Current Contacts. All 3 contacts should be present.
print_format: Print format, e.g. 'default' or 'yaml'.
Returns:
Boolean: whether the user accepted the notice or not.
"""
log.status.Print(
'You choose to make contact data of domain {} public.\n'
'Anyone who looks it up in the WHOIS directory will be able to see info\n'
'for the domain owner and administrative and technical contacts.\n'
'Make sure it\'s ok with them that their contact data is public.\n'
'\n'
'Please consider carefully any changes to contact privacy settings when\n'
'changing from "redacted-contact-data" to "public-contact-data."\n'
'There may be a delay in reflecting updates you make to registrant\n'
'contact information such that any changes you make to contact privacy\n'
'(including from "redacted-contact-data" to "public-contact-data")\n'
'will be applied without delay but changes to registrant contact\n'
'information may take a limited time to be publicized. This means that\n'
'changes to contact privacy from "redacted-contact-data" to\n'
'"public-contact-data" may make the previous registrant contact\n'
'data public until the modified registrant contact details are '
'published.\n'
'\n'
'This info will be publicly available:'.format(domain))
contacts = _SimplifyContacts(contacts)
resource_printer.Print(contacts, print_format, out=sys.stderr)
return console_io.PromptContinue(
message=None, default=False, throw_if_unattended=True, cancel_on_no=True)
# TODO(b/110398579): Integrate with ARI.

View File

@@ -0,0 +1,500 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""DNS utilties for Cloud Domains commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
import sys
from apitools.base.py import exceptions as apitools_exceptions
from googlecloudsdk.api_lib.dns import util as dns_api_util
from googlecloudsdk.api_lib.domains import registrations
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import exceptions as calliope_exceptions
from googlecloudsdk.command_lib.domains import util
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.resource import resource_printer
import six
@enum.unique
class DNSSECUpdate(enum.Enum):
"""DNSSEC update options."""
ENABLE = enum.auto()
DISABLE = enum.auto()
NO_CHANGE = enum.auto()
class DnsUpdateMask(object):
"""Class with information which parts of dns_settings should be updated."""
def __init__(self,
name_servers=False,
glue_records=False,
google_domains_dnssec=False,
custom_dnssec=False):
self.name_servers = name_servers
self.glue_records = glue_records
self.google_domains_dnssec = google_domains_dnssec
self.custom_dnssec = custom_dnssec
def ParseDNSSettings(api_version,
name_servers,
cloud_dns_zone,
use_google_domains_dns,
dns_settings_from_file,
domain,
dnssec_update=DNSSECUpdate.NO_CHANGE,
dns_settings=None):
"""Parses DNS settings from a flag.
At most one of the arguments (except domain) should be non-empty.
Args:
api_version: Cloud Domains API version to call.
name_servers: List of name servers
cloud_dns_zone: Cloud DNS Zone name
use_google_domains_dns: Information that Google Domains name servers should
be used.
dns_settings_from_file: Path to a yaml file with dns_settings.
domain: Domain name corresponding to the DNS settings.
dnssec_update: DNSSECUpdate operation.
dns_settings: Current DNS settings. Used during Configure DNS only.
Returns:
A pair: (messages.DnsSettings, DnsUpdateMask) to be updated, or (None, None)
if all the arguments are empty.
"""
domains_messages = registrations.GetMessagesModule(api_version)
if name_servers is not None:
return _CustomNameServers(domains_messages, name_servers)
if cloud_dns_zone is not None:
nameservers, ds_records = _GetCloudDnsDetails(domains_messages,
cloud_dns_zone, domain,
dnssec_update, dns_settings)
return _CustomNameServers(domains_messages, nameservers, ds_records)
if use_google_domains_dns:
return _GoogleDomainsNameServers(
domains_messages, dnssec_update, dns_settings
)
if dns_settings_from_file is not None:
return _ParseDnsSettingsFromFile(domains_messages, dns_settings_from_file)
if dns_settings is not None and dnssec_update == DNSSECUpdate.DISABLE:
return _DisableDnssec(domains_messages, dns_settings)
return None, None
def _CustomNameServers(domains_messages, name_servers, ds_records=None):
"""Validates name servers and returns (dns_settings, update_mask)."""
if not ds_records:
ds_records = []
normalized_name_servers = list(map(util.NormalizeDomainName, name_servers))
for ns, normalized in zip(name_servers, normalized_name_servers):
if not util.ValidateDomainName(normalized):
raise exceptions.Error('Invalid name server: \'{}\'.'.format(ns))
update_mask = DnsUpdateMask(name_servers=True, custom_dnssec=True)
dns_settings = domains_messages.DnsSettings(
customDns=domains_messages.CustomDns(
nameServers=normalized_name_servers, dsRecords=ds_records))
return dns_settings, update_mask
def _GoogleDomainsNameServers(
domains_messages, dnssec_update, dns_settings=None
):
"""Enable Google Domains name servers and returns (dns_settings, update_mask)."""
update_mask = DnsUpdateMask(name_servers=True, google_domains_dnssec=True)
ds_state = (
domains_messages.GoogleDomainsDns.DsStateValueValuesEnum
.DS_RECORDS_UNPUBLISHED)
if dnssec_update == DNSSECUpdate.ENABLE:
ds_state = (
domains_messages.GoogleDomainsDns.DsStateValueValuesEnum
.DS_RECORDS_PUBLISHED)
elif dnssec_update == DNSSECUpdate.NO_CHANGE:
# If GoogleDomainsDNS is currently used, keep the current DNSSEC value.
# Otherwise keep the default value to disable DNSSEC.
if dns_settings is not None and dns_settings.googleDomainsDns is not None:
ds_state = dns_settings.googleDomainsDns.dsState
dns_settings = domains_messages.DnsSettings(
googleDomainsDns=domains_messages.GoogleDomainsDns(dsState=ds_state))
return dns_settings, update_mask
def _ParseDnsSettingsFromFile(domains_messages, path):
"""Parses dns_settings from a yaml file.
Args:
domains_messages: Cloud Domains messages module.
path: YAML file path.
Returns:
Pair (DnsSettings, DnsUpdateMask) or (None, None) if path is None.
"""
dns_settings = util.ParseMessageFromYamlFile(
path, domains_messages.DnsSettings,
'DNS settings file \'{}\' does not contain valid dns_settings message'
.format(path))
if not dns_settings:
return None, None
update_mask = None
if dns_settings.googleDomainsDns is not None:
update_mask = DnsUpdateMask(
name_servers=True, google_domains_dnssec=True, glue_records=True)
elif dns_settings.customDns is not None:
update_mask = DnsUpdateMask(
name_servers=True, custom_dnssec=True, glue_records=True)
else:
raise exceptions.Error(
'dnsProvider is not present in DNS settings file \'{}\'.'.format(path))
return dns_settings, update_mask
def _GetCloudDnsDetails(
domains_messages, cloud_dns_zone, domain, dnssec_update, dns_settings=None
):
"""Fetches list of name servers from provided Cloud DNS Managed Zone.
Args:
domains_messages: Cloud Domains messages module.
cloud_dns_zone: Cloud DNS Zone resource reference.
domain: Domain name.
dnssec_update: If ENABLE, try to read DNSSEC information from the Zone.
dns_settings: Current DNS configuration (or None if resource is not yet
created).
Returns:
A pair: List of name servers and a list of Ds records (or [] if e.g. the
Zone is not signed).
"""
# Get the managed-zone.
dns_api_version = 'v1'
dns = apis.GetClientInstance('dns', dns_api_version)
dns_messages = dns.MESSAGES_MODULE
zone_ref = dns_api_util.GetRegistry(dns_api_version).Parse(
cloud_dns_zone,
params={
'project': properties.VALUES.core.project.GetOrFail,
},
collection='dns.managedZones',
)
try:
zone = dns.managedZones.Get(
dns_messages.DnsManagedZonesGetRequest(
project=zone_ref.project, managedZone=zone_ref.managedZone
)
)
except apitools_exceptions.HttpError as error:
raise calliope_exceptions.HttpException(error)
domain_with_dot = domain + '.'
if zone.dnsName != domain_with_dot:
raise exceptions.Error(
"The dnsName '{}' of specified Cloud DNS zone '{}' does not match the "
"registration domain '{}'".format(
zone.dnsName, cloud_dns_zone, domain_with_dot
)
)
if (
zone.visibility
!= dns_messages.ManagedZone.VisibilityValueValuesEnum.public
):
raise exceptions.Error(
"Cloud DNS Zone '{}' is not public.".format(cloud_dns_zone)
)
if dnssec_update == DNSSECUpdate.DISABLE:
return zone.nameServers, []
if dnssec_update == DNSSECUpdate.NO_CHANGE:
# If the DNS Zone is already in use keep the current config.
if (
dns_settings is not None
and dns_settings.customDns is not None
and set(dns_settings.customDns.nameServers) == set(zone.nameServers)
):
return (
dns_settings.customDns.nameServers,
dns_settings.customDns.dsRecords,
)
# Otherwise disable DNSSEC
return zone.nameServers, []
signed = dns_messages.ManagedZoneDnsSecConfig.StateValueValuesEnum.on
if not zone.dnssecConfig or zone.dnssecConfig.state != signed:
log.status.Print(
'Cloud DNS Zone \'{}\' is not signed. DNSSEC won\'t be enabled.'.format(
cloud_dns_zone))
return zone.nameServers, []
try:
dns_keys = []
req = dns_messages.DnsDnsKeysListRequest(
project=zone_ref.project,
managedZone=zone_ref.managedZone)
while True:
resp = dns.dnsKeys.List(req)
dns_keys += resp.dnsKeys
req.pageToken = resp.nextPageToken
if not resp.nextPageToken:
break
except apitools_exceptions.HttpError as error:
log.status.Print('Cannot read DS records from Cloud DNS Zone \'{}\': {}. '
'DNSSEC won\'t be enabled.'.format(cloud_dns_zone, error))
ds_records = _ConvertDnsKeys(domains_messages, dns_messages, dns_keys)
if not ds_records:
log.status.Print('No supported DS records found in Cloud DNS Zone \'{}\'. '
'DNSSEC won\'t be enabled.'.format(cloud_dns_zone))
return zone.nameServers, []
return zone.nameServers, ds_records
def _ConvertDnsKeys(domains_messages, dns_messages, dns_keys):
"""Converts DnsKeys to DsRecords."""
ds_records = []
for key in dns_keys:
if key.type != dns_messages.DnsKey.TypeValueValuesEnum.keySigning:
continue
if not key.isActive:
continue
try:
algorithm = domains_messages.DsRecord.AlgorithmValueValuesEnum(
six.text_type(key.algorithm).upper())
for d in key.digests:
digest_type = domains_messages.DsRecord.DigestTypeValueValuesEnum(
six.text_type(d.type).upper())
ds_records.append(
domains_messages.DsRecord(
keyTag=key.keyTag,
digest=d.digest,
algorithm=algorithm,
digestType=digest_type))
except TypeError:
continue # Ignore unsupported algorithms and digest types.
return ds_records
def _DisableDnssec(domains_messages, dns_settings):
"""Returns DNS settings (and update mask) with DNSSEC disabled."""
if dns_settings is None:
return None, None
if dns_settings.googleDomainsDns is not None:
updated_dns_settings = domains_messages.DnsSettings(
googleDomainsDns=domains_messages.GoogleDomainsDns(
dsState=domains_messages.GoogleDomainsDns.DsStateValueValuesEnum
.DS_RECORDS_UNPUBLISHED))
update_mask = DnsUpdateMask(google_domains_dnssec=True)
elif dns_settings.customDns is not None:
updated_dns_settings = domains_messages.DnsSettings(
customDns=domains_messages.CustomDns(dsRecords=[]))
update_mask = DnsUpdateMask(custom_dnssec=True)
else:
return None, None
return updated_dns_settings, update_mask
def PromptForNameServers(api_version,
domain,
dnssec_update=DNSSECUpdate.NO_CHANGE,
dns_settings=None,
print_format='default'):
"""Asks the user to provide DNS settings interactively.
Args:
api_version: Cloud Domains API version to call.
domain: Domain name corresponding to the DNS settings.
dnssec_update: DNSSECUpdate operation.
dns_settings: Current DNS configuration (or None if resource is not yet
created).
print_format: Print format to use when showing current dns_settings.
Returns:
A pair: (messages.DnsSettings, DnsUpdateMask) to be updated, or (None, None)
if the user cancelled.
"""
domains_messages = registrations.GetMessagesModule(api_version)
options = [
'Provide name servers list', 'Provide Cloud DNS Managed Zone name',
'Use free name servers provided by Google Domains'
]
if dns_settings is not None: # Update
log.status.Print('Your current DNS settings are:')
resource_printer.Print(dns_settings, print_format, out=sys.stderr)
message = (
'You can provide your DNS settings by specifying name servers, '
'a Cloud DNS Managed Zone name or by choosing '
'free name servers provided by Google Domains'
)
cancel_option = True
default = len(options) # Additional 'cancel' option.
else:
options = options[:2]
message = (
'You can provide your DNS settings by specifying name servers '
'or a Cloud DNS Managed Zone name'
)
cancel_option = False
default = 1 # Cloud DNS Zone.
index = console_io.PromptChoice(
message=message,
options=options,
cancel_option=cancel_option,
default=default)
name_servers = []
if index == 0: # name servers.
while len(name_servers) < 2:
while True:
ns = console_io.PromptResponse('Name server (empty line to finish): ')
if not ns:
break
if not util.ValidateDomainName(ns):
log.status.Print('Invalid name server: \'{}\'.'.format(ns))
else:
name_servers += [ns]
if len(name_servers) < 2:
log.status.Print('You have to provide at least 2 name servers.')
return _CustomNameServers(domains_messages, name_servers)
elif index == 1: # Cloud DNS.
while True:
zone = util.PromptWithValidator(
validator=util.ValidateNonEmpty,
error_message=' Cloud DNS Managed Zone name must not be empty.',
prompt_string='Cloud DNS Managed Zone name: ',
)
try:
name_servers, ds_records = _GetCloudDnsDetails(
domains_messages, zone, domain, dnssec_update, dns_settings
)
except (exceptions.Error, calliope_exceptions.HttpException) as e:
log.status.Print(six.text_type(e))
else:
break
return _CustomNameServers(domains_messages, name_servers, ds_records)
elif index == 2: # Google Domains name servers.
return _GoogleDomainsNameServers(
domains_messages, dnssec_update, dns_settings
)
else:
return None, None # Cancel.
def PromptForNameServersTransfer(api_version, domain):
"""Asks the user to provide DNS settings interactively for Transfers.
Args:
api_version: Cloud Domains API version to call.
domain: Domain name corresponding to the DNS settings.
Returns:
A triple: (messages.DnsSettings, DnsUpdateMask, _) to be updated, or
(None, None, _) if the user cancelled. The third value returns true when
keeping the current DNS settings during Transfer.
"""
domains_messages = registrations.GetMessagesModule(api_version)
options = [
'Provide Cloud DNS Managed Zone name',
'Use free name servers provided by Google Domains',
'Keep current DNS settings from current registrar'
]
message = ('You can provide your DNS settings in one of several ways:\n'
'You can specify a Cloud DNS Managed Zone name. To avoid '
'downtime following transfer, make sure the zone is configured '
'correctly before proceeding.\n'
'You can select free name servers provided by Google Domains. '
'This blank-slate option cannot be configured before transfer.\n'
'You can also choose to keep the domain\'s DNS settings '
'from its current registrar. Use this option only if you are '
'sure that the domain\'s current DNS service will not cease upon '
'transfer, as is often the case for DNS services provided for '
'free by the registrar.')
cancel_option = False
default = 2 # Keep current DNS settings.
# It's not safe to change name servers and enable DNSSEC during transfers at
# once, so we just mark DNSSEC as disabled. However, this option is only used
# when the customer is changing the existing DNS configuration.
dnssec_update = DNSSECUpdate.DISABLE
index = console_io.PromptChoice(
message=message,
options=options,
cancel_option=cancel_option,
default=default)
if index == 0: # Cloud DNS.
while True:
zone = util.PromptWithValidator(
validator=util.ValidateNonEmpty,
error_message=' Cloud DNS Managed Zone name must not be empty.',
prompt_string='Cloud DNS Managed Zone name: ')
try:
name_servers, ds_records = _GetCloudDnsDetails(domains_messages, zone,
domain, dnssec_update)
except (exceptions.Error, calliope_exceptions.HttpException) as e:
log.status.Print(six.text_type(e))
else:
break
dns_settings, update_mask = _CustomNameServers(domains_messages,
name_servers, ds_records)
return dns_settings, update_mask, False
elif index == 1: # Google Domains name servers.
dns_settings, update_mask = _GoogleDomainsNameServers(
domains_messages, dnssec_update)
return dns_settings, update_mask, False
else: # Keep current DNS settings (Transfer).
return None, None, True
def NameServersEquivalent(prev_dns_settings, new_dns_settings):
"""Checks if dns settings have equivalent name servers."""
if prev_dns_settings.googleDomainsDns:
return bool(new_dns_settings.googleDomainsDns)
if prev_dns_settings.customDns:
if not new_dns_settings.customDns:
return False
prev_ns = sorted(
map(util.NormalizeDomainName, prev_dns_settings.customDns.nameServers))
new_ns = sorted(
map(util.NormalizeDomainName, new_dns_settings.customDns.nameServers))
return prev_ns == new_ns
return False
def PromptForUnsafeDnsUpdate():
console_io.PromptContinue(
'This operation is not safe.',
default=False,
throw_if_unattended=True,
cancel_on_no=True)
def DnssecEnabled(dns_settings):
ds_records = []
if dns_settings.googleDomainsDns is not None:
ds_records = dns_settings.googleDomainsDns.dsRecords
if dns_settings.customDns is not None:
ds_records = dns_settings.customDns.dsRecords
return bool(ds_records)

View File

@@ -0,0 +1,624 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared flags for Cloud Domains commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
from googlecloudsdk.api_lib.domains import registrations
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.util.apis import arg_utils
# Alpha API = Beta API = GA API
# It doesn't matter which one is used to generate flags.
API_VERSION_FOR_FLAGS = registrations.GA_API_VERSION
class MutationOp(enum.Enum):
"""Different types of mutation operations."""
REGISTER = 1
UPDATE = 2
TRANSFER = 3
RENEWAL = 4
def AddConfigureDNSSettingsFlagsToParser(parser):
"""Get flags for changing DNS settings.
Args:
parser: argparse parser to which to add these flags.
"""
_AddDNSSettingsFlagsToParser(parser, mutation_op=MutationOp.UPDATE)
base.Argument( # This is not a go/gcloud-style#commonly-used-flags.
'--unsafe-dns-update',
default=False,
help='Use this flag to allow DNS changes that may make '
'your domain stop serving.',
action=actions.DeprecationAction(
'--unsafe-dns-update',
warn=('The {flag_name} option is deprecated. To complete an unsafe '
'DNS operation first disable DNSSEC, then change name servers, '
'then (optionally) enable DNSSEC.'),
removed=False,
action='store_true'),
).AddToParser(parser)
def AddConfigureContactsSettingsFlagsToParser(parser):
"""Get flags for changing contact settings.
Args:
parser: argparse parser to which to add these flags.
"""
_AddContactSettingsFlagsToParser(parser, mutation_op=MutationOp.UPDATE)
messages = apis.GetMessagesModule('domains', API_VERSION_FOR_FLAGS)
base.Argument( # This is not a go/gcloud-style#commonly-used-flags.
'--notices',
help='Notices about special properties of contacts.',
metavar='NOTICE',
type=arg_parsers.ArgList(
element_type=str, choices=ContactNoticeEnumMapper(
messages).choices)).AddToParser(parser)
def AddTransferFlagsToParser(parser):
"""Get flags for transferring a domain.
Args:
parser: argparse parser to which to add these flags.
"""
_AddDNSSettingsFlagsToParser(parser, mutation_op=MutationOp.TRANSFER)
_AddContactSettingsFlagsToParser(parser, mutation_op=MutationOp.TRANSFER)
AddPriceFlagsToParser(parser, MutationOp.TRANSFER)
help_text = """\
A file containing the authorizaton code. In most cases, you must provide an
authorization code from the domain's current registrar to transfer the
domain.
Examples of file contents:
```
5YcCd!X&W@q0Xozj
```
"""
base.Argument(
'--authorization-code-from-file',
help=help_text,
metavar='AUTHORIZATION_CODE_FILE_NAME',
category=base.COMMONLY_USED_FLAGS).AddToParser(parser)
messages = apis.GetMessagesModule('domains', API_VERSION_FOR_FLAGS)
notice_choices = ContactNoticeEnumMapper(messages).choices.copy()
base.Argument( # This is not a go/gcloud-style#commonly-used-flags.
'--notices',
help='Notices about special properties of certain domains or contacts.',
metavar='NOTICE',
type=arg_parsers.ArgList(element_type=str,
choices=notice_choices)).AddToParser(parser)
def AddRegisterFlagsToParser(parser):
"""Get flags for registering a domain.
Args:
parser: argparse parser to which to add these flags.
"""
_AddDNSSettingsFlagsToParser(parser, mutation_op=MutationOp.REGISTER)
_AddContactSettingsFlagsToParser(parser, mutation_op=MutationOp.REGISTER)
AddPriceFlagsToParser(parser, MutationOp.REGISTER)
messages = apis.GetMessagesModule('domains', API_VERSION_FOR_FLAGS)
notice_choices = ContactNoticeEnumMapper(messages).choices.copy()
notice_choices.update({
'hsts-preloaded':
('By sending this notice you acknowledge that the domain is '
'preloaded on the HTTP Strict Transport Security list in browsers. '
'Serving a website on such domain will require an SSL certificate. '
'See https://support.google.com/domains/answer/7638036 for details.')
})
base.Argument( # This is not a go/gcloud-style#commonly-used-flags.
'--notices',
help='Notices about special properties of certain domains or contacts.',
metavar='NOTICE',
type=arg_parsers.ArgList(element_type=str,
choices=notice_choices)).AddToParser(parser)
def _AddDNSSettingsFlagsToParser(parser, mutation_op):
"""Get flags for providing DNS settings.
Args:
parser: argparse parser to which to add these flags.
mutation_op: operation for which we're adding flags.
"""
dnssec_help_text = ''
group_help_text = """\
Set the authoritative name servers for the given domain.
"""
if mutation_op == MutationOp.REGISTER:
dnssec_help_text = ('If the zone is signed, DNSSEC will be enabled by '
'default unless you pass --disable-dnssec.')
if mutation_op == MutationOp.UPDATE:
group_help_text = group_help_text + """
Warning: Do not change name servers if ds_records is non-empty.
Clear ds_records first by calling this command with the
--disable-dnssec flag, and wait 24 hours before changing
name servers. Otherwise your domain may stop serving.
"""
if mutation_op == MutationOp.TRANSFER:
dnssec_help_text = ('DNSSEC will be disabled and will need to be enabled '
'after the transfer completes, if desired.')
group_help_text = group_help_text + """
Warning: If your DNS is hosted by your old registrar, we do not
recommend keeping your current DNS settings, as these services
often terminate when you transfer out. Instead, you should
switch to another DNS provider such as Cloud DNS. To avoid
downtime during the transfer, copy your DNS records to your new
DNS provider before initiating transfer.
Warning: If you are changing your DNS settings and your domain
currently has DS records, make sure to remove the DS records at
your old registrar and wait a day before initiating transfer.
If you are keeping your current DNS settings, then no changes
to DS records are necessary.
"""
dns_group = base.ArgumentGroup(
mutex=True, help=group_help_text, category=base.COMMONLY_USED_FLAGS)
# Disable this flag for the transfer case.
if mutation_op != MutationOp.TRANSFER:
dns_group.AddArgument(
base.Argument(
'--name-servers',
help='List of DNS name servers for the domain.',
metavar='NAME_SERVER',
type=arg_parsers.ArgList(str, min_length=2)))
cloud_dns_transfer_help_text = ''
if mutation_op == MutationOp.TRANSFER:
cloud_dns_transfer_help_text = (
' To avoid downtime following transfer, make sure the zone is '
'configured correctly before proceeding.')
cloud_dns_help_text = (
'The name of the Cloud DNS managed-zone to set as the name '
'server for the domain.\n'
'If it\'s in the same project, you can use short name. If not, '
'use the full resource name, e.g.: --cloud-dns-zone='
'projects/example-project/managedZones/example-zone.{}\n'
'{}').format(cloud_dns_transfer_help_text, dnssec_help_text)
google_dns_transfer_help_text = ''
if mutation_op == MutationOp.TRANSFER:
google_dns_transfer_help_text = (
' This blank-slate option cannot be configured before transfer.')
google_dns_help_text = (
'Use free name servers provided by Google Domains.{}\n'
'{}').format(google_dns_transfer_help_text, dnssec_help_text)
dns_group.AddArgument(
base.Argument('--cloud-dns-zone', help=cloud_dns_help_text))
dns_group.AddArgument(
base.Argument(
'--use-google-domains-dns',
help=google_dns_help_text,
default=False,
action=actions.DeprecationAction(
'--use-google-domains-dns',
warn=(
'The {flag_name} option is deprecated; See'
' https://cloud.google.com/domains/docs/deprecations/feature-deprecations.'
),
# Removed for REGISTER, Deprecated for UPDATE.
removed=(mutation_op == MutationOp.REGISTER),
action='store_true',
),
)
)
if mutation_op == MutationOp.TRANSFER:
dns_group.AddArgument(
base.Argument(
'--keep-dns-settings',
help=(
'Keep the domain\'s current DNS configuration from its current '
'registrar. Use this option only if you are sure that the '
'domain\'s current DNS service will not cease upon transfer, as'
' is often the case for DNS services provided for free by the '
'registrar.'),
default=False,
action='store_true'))
if mutation_op == MutationOp.UPDATE:
help_text = """\
A YAML file containing the required DNS settings.
If specified, its content will replace the values currently used in the
registration resource. If the file is missing some of the dns_settings
fields, those fields will be cleared.
Examples of file contents:
```
googleDomainsDns:
dsState: DS_RECORDS_PUBLISHED
glueRecords:
- hostName: ns1.example.com
ipv4Addresses:
- 8.8.8.8
- hostName: ns2.example.com
ipv4Addresses:
- 8.8.8.8
```
```
customDns:
nameServers:
- new.ns1.com
- new.ns2.com
dsRecords:
- keyTag: 24
algorithm: RSASHA1
digestType: SHA256
digest: 2e1cfa82b035c26cbbbdae632cea070514eb8b773f616aaeaf668e2f0be8f10d
- keyTag: 42
algorithm: RSASHA1
digestType: SHA256
digest: 2e1cfa82bf35c26cbbbdae632cea070514eb8b773f616aaeaf668e2f0be8f10d
```
"""
dns_group.AddArgument(
base.Argument(
'--dns-settings-from-file',
help=help_text,
metavar='DNS_SETTINGS_FILE_NAME'))
dns_group.AddToParser(parser)
if mutation_op != MutationOp.TRANSFER:
base.Argument(
'--disable-dnssec',
help="""\
Use this flag to disable DNSSEC, or to skip enabling it when switching
to a Cloud DNS Zone or Google Domains nameservers.
""",
default=False,
action='store_true').AddToParser(parser)
def _AddContactSettingsFlagsToParser(parser, mutation_op):
"""Get flags for providing Contact settings.
Args:
parser: argparse parser to which to add these flags.
mutation_op: operation for which we're adding flags.
"""
help_text = """\
A YAML file containing the contact data for the domain's three contacts:
registrant, admin, and technical.
The file can either specify a single set of contact data with label
'allContacts', or three separate sets of contact data with labels
'adminContact' and 'technicalContact'.
{}
Each contact data must contain values for all required fields: email,
phoneNumber and postalAddress in google.type.PostalAddress format.
For more guidance on how to specify postalAddress, please see:
https://support.google.com/business/answer/6397478
Examples of file contents:
```
allContacts:
email: 'example@example.com'
phoneNumber: '+1.8005550123'
postalAddress:
regionCode: 'US'
postalCode: '94043'
administrativeArea: 'CA'
locality: 'Mountain View'
addressLines: ['1600 Amphitheatre Pkwy']
recipients: ['Jane Doe']
```
{}
```
registrantContact:
email: 'registrant@example.com'
phoneNumber: '+1.8005550123'
postalAddress:
regionCode: 'US'
postalCode: '94043'
administrativeArea: 'CA'
locality: 'Mountain View'
addressLines: ['1600 Amphitheatre Pkwy']
recipients: ['Registrant Jane Doe']
adminContact:
email: 'admin@example.com'
phoneNumber: '+1.8005550123'
postalAddress:
regionCode: 'US'
postalCode: '94043'
administrativeArea: 'CA'
locality: 'Mountain View'
addressLines: ['1600 Amphitheatre Pkwy']
recipients: ['Admin Jane Doe']
technicalContact:
email: 'technical@example.com'
phoneNumber: '+1.8005550123'
postalAddress:
regionCode: 'US'
postalCode: '94043'
administrativeArea: 'CA'
locality: 'Mountain View'
addressLines: ['1600 Amphitheatre Pkwy']
recipients: ['Technic Jane Doe']
```
"""
if mutation_op == MutationOp.UPDATE:
help_text = help_text.format(
"""
If 'registrantContact', 'adminContact' or 'technicalContact' labels are used
then only the specified contacts are updated.
""", """
```
adminContact:
email: 'admin@example.com'
phoneNumber: '+1.8005550123'
postalAddress:
regionCode: 'US'
postalCode: '94043'
administrativeArea: 'CA'
locality: 'Mountain View'
addressLines: ['1600 Amphitheatre Pkwy']
recipients: ['Admin Jane Doe']
```
""")
else:
help_text = help_text.format('', '')
base.Argument(
'--contact-data-from-file',
help=help_text,
metavar='CONTACT_DATA_FILE_NAME',
category=base.COMMONLY_USED_FLAGS).AddToParser(parser)
def _ChoiceValueType(value):
"""Copy of base._ChoiceValueType."""
return value.replace('_', '-').lower()
messages = apis.GetMessagesModule('domains', API_VERSION_FOR_FLAGS)
base.Argument(
'--contact-privacy',
choices=ContactPrivacyEnumMapper(messages).choices,
type=_ChoiceValueType,
help=(
'The contact privacy mode to use. Supported privacy modes depend on'
' the domain.'
),
required=False,
category=base.COMMONLY_USED_FLAGS,
action=actions.DeprecationAction(
'--contact-privacy=private-contact-data',
show_message=lambda choice: choice == 'private-contact-data',
show_add_help=False,
warn=(
'The {flag_name} option is deprecated; See'
' https://cloud.google.com/domains/docs/deprecations/feature-deprecations.'
),
removed=False,
),
).AddToParser(parser)
def AddPriceFlagsToParser(parser, mutation_op):
get_price_method = ''
if mutation_op == MutationOp.REGISTER:
get_price_method = 'using the get-register-parameters command'
elif mutation_op == MutationOp.TRANSFER:
get_price_method = 'using the get-transfer-parameters command'
elif mutation_op == MutationOp.RENEWAL:
get_price_method = ('by calling the renew-domain command without the '
'--yearly-price flag')
base.Argument( # This is not a go/gcloud-style#commonly-used-flags.
'--yearly-price',
help=('Accept the domain\'s yearly price in the interactive flow or by '
'using this flag. Use a number followed by a currency code, for '
'example, "12.00 USD". Get the price {}.'.format(get_price_method)),
).AddToParser(parser)
def AddValidateOnlyFlagToParser(parser, verb, noun='registration'):
"""Adds validate_only flag as go/gcloud-style#commonly-used-flags."""
base.Argument(
'--validate-only',
help='Don\'t actually {} {}. Only validate arguments.'.format(verb, noun),
default=False,
action='store_true',
category=base.COMMONLY_USED_FLAGS).AddToParser(parser)
def AddAsyncFlagToParser(parser):
"""Adds async flag. It's not marked as go/gcloud-style#commonly-used-flags."""
base.ASYNC_FLAG.AddToParser(parser)
def AddTagFlagToParser(parser):
base.Argument(
'--tag',
help=('The Tag of the new registrar. Can be found at '
'https://nominet.uk/registrar-list/'),
).AddToParser(parser)
def AddManagementSettingsFlagsToParser(parser):
"""Get flags for configure management command.
Args:
parser: argparse parser to which to add these flags.
"""
messages = apis.GetMessagesModule('domains', API_VERSION_FOR_FLAGS)
TransferLockEnumMapper(messages).choice_arg.AddToParser(parser)
RenewalMethodEnumMapper(messages).choice_arg.AddToParser(parser)
def _GetContactPrivacyEnum(domains_messages):
"""Get Contact Privacy Enum from api messages."""
return domains_messages.ContactSettings.PrivacyValueValuesEnum
def ContactPrivacyEnumMapper(domains_messages):
return arg_utils.ChoiceEnumMapper(
'--contact-privacy',
_GetContactPrivacyEnum(domains_messages),
custom_mappings={
'PRIVATE_CONTACT_DATA': (
'private-contact-data',
(
"(DEPRECATED) Your contact info won't be available to the"
' public. To help protect your info and prevent spam, a third'
' party provides alternate (proxy) contact info for your'
' domain in the public directory at no extra cost. They will'
' forward received messages to you.'
' The private-contact-data option is deprecated; See'
' https://cloud.google.com/domains/docs/deprecations/feature-deprecations.'
),
),
'REDACTED_CONTACT_DATA': (
'redacted-contact-data',
(
'Limited personal information will be available to the'
' public. The actual information redacted depends on the'
' domain. For more information see'
' https://support.google.com/domains/answer/3251242.'
),
),
'PUBLIC_CONTACT_DATA': (
'public-contact-data',
(
'All the data from contact config is publicly available. To'
' set this value, you must also pass the --notices flag with'
' value public-contact-data-acknowledgement or agree to the'
' notice interactively.'
),
),
},
required=False,
help_str=(
'The contact privacy mode to use. Supported privacy modes '
'depend on the domain.'
),
)
def PrivacyChoiceStrength(privacy):
"""Returns privacy strength (stronger privacy means higher returned value)."""
if privacy == 'public-contact-data':
return 0
if privacy == 'redacted-contact-data':
return 1
if privacy == 'private-contact-data':
return 2
def _GetContactNoticeEnum(domains_messages):
"""Get ContactNoticeEnum from api messages."""
return domains_messages.ConfigureContactSettingsRequest.ContactNoticesValueListEntryValuesEnum
def ContactNoticeEnumMapper(domains_messages):
return arg_utils.ChoiceEnumMapper(
'--notices',
_GetContactNoticeEnum(domains_messages),
custom_mappings={
'PUBLIC_CONTACT_DATA_ACKNOWLEDGEMENT':
('public-contact-data-acknowledgement',
('By sending this notice you acknowledge that using '
'public-contact-data contact privacy makes all the data '
'from contact config publicly available.')),
},
required=False,
help_str=('Notices about special properties of contacts.'))
def _GetTransferLockEnum(domains_messages):
"""Get TransferLockStateValueValuesEnum from api messages."""
return domains_messages.ManagementSettings.TransferLockStateValueValuesEnum
def TransferLockEnumMapper(domains_messages):
return arg_utils.ChoiceEnumMapper(
'--transfer-lock-state',
_GetTransferLockEnum(domains_messages),
custom_mappings={
'LOCKED': ('locked', ('The transfer lock is locked.')),
'UNLOCKED': ('unlocked', ('The transfer lock is unlocked.')),
},
required=False,
help_str=('Transfer Lock of a registration. It needs to be unlocked '
'in order to transfer the domain to another registrar.'))
def _GetRenewalMethodEnum(domains_messages):
"""Get PreferredRenewalMethodValueValuesEnum from api messages."""
return (
domains_messages.ManagementSettings.PreferredRenewalMethodValueValuesEnum
)
def RenewalMethodEnumMapper(domains_messages):
return arg_utils.ChoiceEnumMapper(
'--preferred-renewal-method',
_GetRenewalMethodEnum(domains_messages),
custom_mappings={
'AUTOMATIC_RENEWAL': (
'automatic-renewal',
'The domain is automatically renewed each year.',
),
'RENEWAL_DISABLED': (
'renewal-disabled',
(
"The domain won't be renewed and will expire at its "
'expiration time.'
),
),
},
required=False,
help_str=(
'Preferred Renewal Method of a registration. '
'It defines how the registration should be renewed. '
'The actual Renewal Method can be set to renewal-disabled in case of '
'e.g. problems with the Billing Account or reporeted domain abuse.'
),
)

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cloud Domains Registration-specific printer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from googlecloudsdk.core.resource import custom_printer_base as cp
from googlecloudsdk.core.resource import yaml_printer as yp
REGISTRATION_PRINTER_FORMAT = 'registration'
class RegistrationPrinter(cp.CustomPrinterBase):
"""Prints the Cloud Domains registration in YAML format with custom fields order."""
_KNOWN_FIELDS_BY_IMPORTANCE = [
'name', 'createTime', 'domainName', 'state', 'issues', 'expireTime',
'labels', 'managementSettings', 'dnsSettings', 'contactSettings',
'pendingContactSettings', 'supportedPrivacy'
]
_KNOWN_REPEATED_FIELDS = ['issues', 'supportedPrivacy']
def _ClearField(self, registration, field):
if field in self._KNOWN_REPEATED_FIELDS:
setattr(registration, field, [])
else:
setattr(registration, field, None)
def _TransformKnownFields(self, printer, registration):
for field in self._KNOWN_FIELDS_BY_IMPORTANCE:
record = getattr(registration, field, None)
if record:
printer.AddRecord({field: record}, delimit=False)
def _TransformRemainingFields(self, printer, registration):
for field in self._KNOWN_FIELDS_BY_IMPORTANCE:
if getattr(registration, field, None):
self._ClearField(registration, field)
# printer.AddRecord prints {} for empty resources in scenario tests.
# That's why we only call it when there is something to print.
finished = True
if registration.all_unrecognized_fields():
finished = False
for f in registration.all_fields():
if getattr(registration, f.name):
finished = False
if not finished:
printer.AddRecord(registration, delimit=False)
def Transform(self, registration):
"""Transform a registration into a YAML output."""
yaml = yp.YamlPrinter()
self._TransformKnownFields(yaml, registration)
self._TransformRemainingFields(yaml, registration)

View File

@@ -0,0 +1,135 @@
# -*- coding: utf-8 -*- #
# Copyright 2019 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Shared resource flags for Cloud Domains commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.command_lib.util.concepts import concept_parsers
def RegistrationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='registration',
help_text='The domain registration for the {resource}.')
def LocationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='location',
help_text='The Cloud location for the {resource}.',
fallthroughs=[
deps.Fallthrough(lambda: 'global', 'location is always global')
])
def OperationAttributeConfig():
return concepts.ResourceParameterAttributeConfig(
name='registration',
help_text='Cloud Domains operation for the {resource}.')
def GetRegistrationResourceSpec():
return concepts.ResourceSpec(
'domains.projects.locations.registrations',
resource_name='registration',
registrationsId=RegistrationAttributeConfig(),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def GetLocationResourceSpec():
return concepts.ResourceSpec(
'domains.projects.locations',
resource_name='location',
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG)
def GetOperationResourceSpec():
return concepts.ResourceSpec(
'domains.projects.locations.operations',
resource_name='operation',
operationsId=OperationAttributeConfig(),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False)
def AddRegistrationResourceArg(parser, verb, noun=None, positional=True):
"""Add a resource argument for a Cloud Domains registration.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
noun: str, the resource; default: 'The domain registration'.
positional: bool, if True, means that the registration ID is a positional
arg rather than a flag.
"""
noun = noun or 'The domain registration'
concept_parsers.ConceptParser.ForResource(
'registration' if positional else '--registration',
GetRegistrationResourceSpec(),
'{} {}.'.format(noun, verb),
required=True,
flag_name_overrides={
'location': '' # location is always global so don't create a flag.
}).AddToParser(parser)
def AddLocationResourceArg(parser, verb=''):
"""Add a resource argument for a cloud location.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
"""
concept_parsers.ConceptParser.ForResource(
'--location',
GetLocationResourceSpec(),
'The Cloud location {}.'.format(verb),
required=True,
flag_name_overrides={
'location': '' # location is always global so don't create a flag.
}).AddToParser(parser)
def AddOperationResourceArg(parser, verb, positional=True):
"""Add a resource argument for a Cloud Domains registration.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
positional: bool, if True, means that the registration ID is a positional
arg rather than a flag.
"""
concept_parsers.ConceptParser.ForResource(
'operation' if positional else '--operation',
GetOperationResourceSpec(),
'The operation {}.'.format(verb),
required=True,
flag_name_overrides={
'location': '' # location is always global so don't create a flag.
}).AddToParser(parser)

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""General utilties for Cloud Domains commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from apitools.base.py import encoding
from googlecloudsdk.api_lib.domains import operations
from googlecloudsdk.api_lib.domains import registrations
from googlecloudsdk.command_lib.domains import flags
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core import resources
from googlecloudsdk.core import yaml
from googlecloudsdk.core.console import console_io
from googlecloudsdk.core.util import files
LOCATIONS_COLLECTION = 'domains.projects.locations'
OPERATIONS_COLLECTION = 'domains.projects.locations.operations'
REGISTRATIONS_COLLECTION = 'domains.projects.locations.registrations'
_PROJECT = lambda: properties.VALUES.core.project.Get(required=True)
_MAX_LIST_BATCH_SIZE = 200
def RegistrationsUriFunc(api_version):
def UriFunc(resource):
return ParseRegistration(api_version, resource.name).SelfLink()
return UriFunc
def AssertRegistrationOperational(api_version, registration):
messages = registrations.GetMessagesModule(api_version)
if registration.state not in [
messages.Registration.StateValueValuesEnum.ACTIVE,
messages.Registration.StateValueValuesEnum.SUSPENDED
]:
raise exceptions.Error(
'The registration resource must be in state ACTIVE or SUSPENDED, '
'not \'{}\'.'.format(registration.state))
def ParseMessageFromYamlFile(path, message_type, error_message):
"""Parse a Yaml file.
Args:
path: Yaml file path. If path is None returns None.
message_type: Message type to parse YAML into.
error_message: Error message to print in case of parsing error.
Returns:
parsed message of type message_type.
"""
if path is None:
return None
raw_message = yaml.load_path(path)
try:
parsed_message = encoding.PyValueToMessage(message_type, raw_message)
except Exception as e:
# This error may be slightly different in Py2 and Py3.
raise exceptions.Error('{}: {}'.format(error_message, e))
unknown_fields = []
for message in encoding.UnrecognizedFieldIter(parsed_message):
outer_message = ''.join([edge.field + '.' for edge in message[0]])
unknown_fields += [outer_message + field for field in message[1]]
unknown_fields.sort()
if unknown_fields:
raise exceptions.Error(
('{}.\nProblematic fields: \'{}\'').format(error_message,
', '.join(unknown_fields)))
return parsed_message
def NormalizeResourceName(domain):
"""Normalizes domain name in resource name."""
parts = domain.split('/')
parts[-1] = NormalizeDomainName(parts[-1])
return '/'.join(parts)
def NormalizeDomainName(domain):
"""Normalizes domain name (including punycoding)."""
if not domain:
raise exceptions.Error('Empty domain name')
try:
normalized = domain.encode('idna').decode() # To Punycode
normalized = normalized.lower().rstrip('.')
except UnicodeError as e:
raise exceptions.Error('Invalid domain name \'{}\': {}.'.format(domain, e))
return normalized
def PunycodeToUnicode(domain):
return domain.encode('utf-8').decode('idna')
def ValidateDomainName(domain):
if not domain:
return False
# Replace with some library function for FQDN validation.
pattern = r'^[a-z0-9-]+(\.[a-z0-9-]+)+\.{0,1}$'
if not re.match(pattern, domain) or '..' in domain:
return False
return True
def ValidateNonEmpty(s):
return s is not None and bool(s.strip())
def ValidateRegionCode(rc):
return rc is not None and len(rc) == 2 and rc.isalpha() and rc.isupper()
def ValidateEmail(email):
if not email:
return False
# Replace with some library function for email validation.
pattern = r'^[^@\s]+@[^@\s]+\.[^@\s]+$'
return bool(re.match(pattern, email))
def Prompt(prompt_string, message=None):
"""Prompt for user input.
Args:
prompt_string: Message to print in the line with prompt.
message: Optional message to print before prompt.
Returns:
User provided value.
"""
if message:
log.status.Print(message)
return console_io.PromptResponse(prompt_string)
def PromptWithValidator(prompt_string,
validator,
error_message,
message=None,
default=None):
"""Prompt for user input and validate output.
Args:
prompt_string: Message to print in the line with prompt.
validator: Validation function (str) -> bool.
error_message: Message to print if provided value is not correct.
message: Optional message to print before prompt.
default: Optional default value.
Returns:
Valid user provided value or default if not None and user chose it.
"""
if message:
log.status.Print(message)
while True:
if default is not None:
answer = console_io.PromptWithDefault(
message=prompt_string, default=default)
if not answer:
return default
else:
answer = console_io.PromptResponse(prompt_string)
if validator(answer):
return answer
else:
log.status.Print(error_message)
def GetRegistry(api_version):
registry = resources.REGISTRY.Clone()
registry.RegisterApiByName('domains', api_version)
return registry
def ParseRegistration(api_version, registration):
return GetRegistry(api_version).Parse(
registration,
params={
'projectsId': _PROJECT,
'locationsId': 'global'
},
collection=REGISTRATIONS_COLLECTION)
def ParseOperation(api_version, operation):
return GetRegistry(api_version).Parse(
operation,
params={
'projectsId': _PROJECT,
'locationsId': 'global'
},
collection=OPERATIONS_COLLECTION)
def DomainNamespace(domain):
# Return everything after the first encountered dot.
# This is needed to accommodate two-level domains like .co.uk.
return domain[domain.find('.'):]
def ParseTransferLockState(api_version, transfer_lock_state):
messages = registrations.GetMessagesModule(api_version)
if transfer_lock_state is None:
return None
return flags.TransferLockEnumMapper(messages).GetEnumForChoice(
transfer_lock_state)
def PromptForEnum(enum_mapper, enum_type, current_value):
"""Prompts the user for the new enum_type value.
Args:
enum_mapper: Instance of the EnumMapper.
enum_type: A string with enum type name to print.
current_value: Current value of the enum.
Returns:
The new enum choice or None if the enum shouldn't be updated.
"""
options = list(enum_mapper.choices)
update = console_io.PromptContinue(
f'Your current {enum_type} is: {current_value}.',
'Do you want to change it',
default=False,
)
if not update:
return None
current_choice = 0
for i, enum in enumerate(options):
if enum == enum_mapper.GetChoiceForEnum(current_value):
current_choice = i
index = console_io.PromptChoice(
options=options,
default=current_choice,
message=f'Specify new {enum_type}',
)
return options[index]
def PromptForTransferLockState(api_version, transfer_lock):
"""Prompts the user for new transfer lock state."""
messages = registrations.GetMessagesModule(api_version)
enum_mapper = flags.TransferLockEnumMapper(messages)
result = PromptForEnum(enum_mapper, 'Transfer Lock state', transfer_lock)
if result is None:
return None
return ParseTransferLockState(api_version, result)
def ParseRenewalMethod(api_version, renewal_method):
messages = registrations.GetMessagesModule(api_version)
if renewal_method is None:
return None
return flags.RenewalMethodEnumMapper(messages).GetEnumForChoice(
renewal_method
)
def PromptForRenewalMethod(api_version, preferred_renewal_method):
"""Prompts the user for new renewal method."""
messages = registrations.GetMessagesModule(api_version)
enum_mapper = flags.RenewalMethodEnumMapper(messages)
result = PromptForEnum(
enum_mapper, 'preferred Renewal Method', preferred_renewal_method
)
if result is None:
return None
return ParseRenewalMethod(api_version, result)
def PromptForAuthCode():
"""Prompts the user to enter the auth code."""
message = ('Please provide the authorization code from the domain\'s current '
'registrar to transfer the domain.')
log.status.Print(message)
auth_code = console_io.PromptPassword(
prompt='Authorization code: ',
error_message=' Authorization code must not be empty.',
validation_callable=ValidateNonEmpty)
return auth_code
def TransformMoneyType(r):
if r is None:
return None
dr = r
if not isinstance(dr, dict):
dr = encoding.MessageToDict(r)
return '{}.{:02d} {}'.format(dr['units'], int(dr.get('nanos', 0) / (10**7)),
dr.get('currencyCode', ''))
def _ParseMoney(money):
"""Parses money string as tuple (units, cents, currency)."""
match = re.match(r'^(\d+|\d+\.\d{2})\s*([A-Z]{3})$', money)
if match:
number, s = match.groups()
else:
raise ValueError('Value could not be parsed as number + currency code')
if '.' in number:
index = number.find('.')
return int(number[:index]), int(number[index + 1:]), s
else:
return int(number), 0, s
def ParseYearlyPrice(api_version, price_string):
"""Parses money string as type Money."""
if not price_string:
return None
try:
units, cents, currency = _ParseMoney(price_string)
except ValueError:
raise exceptions.Error(
(
f"Yearly price '{price_string}' is invalid. Please specify the"
' amount followed by the currency code.'
)
)
if currency == '$':
currency = 'USD'
messages = registrations.GetMessagesModule(api_version)
return messages.Money(
units=int(units), nanos=cents * 10**7, currencyCode=currency)
def EqualPrice(x, y):
if x.nanos is None:
x.nanos = 0
if y.nanos is None:
y.nanos = 0
return x == y
def PromptForYearlyPriceAck(price):
"""Asks the user to accept the yearly price."""
ack = console_io.PromptContinue(
'Yearly price: {}\n'.format(TransformMoneyType(price)),
prompt_string='Do you agree to pay this yearly price for your domain',
throw_if_unattended=True,
cancel_on_no=True,
default=False)
if ack:
return price
else:
return None
def ParseRegisterNotices(notices):
"""Parses registration notices.
Args:
notices: list of notices (lowercase-strings).
Returns:
Pair (public privacy ack: bool, hsts ack: bool).
"""
if not notices:
return False, False
return 'public-contact-data-acknowledgement' in notices, 'hsts-preloaded' in notices
def PromptForHSTSAck(domain):
ack = console_io.PromptContinue(
('{} is a secure namespace. You may purchase {} now but it will '
'require an SSL certificate for website connection.').format(
DomainNamespace(domain), domain),
throw_if_unattended=True,
cancel_on_no=True,
default=False)
return ack
def WaitForOperation(api_version, response, asynchronous):
"""Handles waiting for the operation and printing information about it.
Args:
api_version: Cloud Domains API version to call.
response: Response from the API call
asynchronous: If true, do not wait for the operation
Returns:
The last information about the operation.
"""
operation_ref = ParseOperation(api_version, response.name)
if asynchronous:
log.status.Print('Started \'{}\''.format(operation_ref.Name()))
else:
message = 'Waiting for \'{}\' to complete'
operations_client = operations.Client.FromApiVersion(api_version)
response = operations_client.WaitForOperation(
operation_ref, message.format(operation_ref.Name()))
return response
def ReadFileContents(path):
"""Reads the text contents from the given path.
Args:
path: str, The file path to read.
Raises:
Error: If the file cannot be read.
Returns:
str, The text string read from the file.
"""
if not path:
return None
return files.ReadFileContents(path)
def GetListBatchSize(args):
"""Returns the batch size for listing resources."""
if args.page_size:
return args.page_size
elif args.limit:
return min(args.limit, _MAX_LIST_BATCH_SIZE)
else:
return None