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,51 @@
# -*- 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 Transfer Service commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
DETAILED_HELP = {
'DESCRIPTION':
"""\
The gcloud transfer command group lets you create and manage
Transfer Service jobs, operations, and agents.
To get started, run:
`gcloud transfer jobs create --help`
More info on prerequisite IAM permissions:
https://cloud.google.com/storage-transfer/docs/on-prem-set-up
""",
}
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.ALPHA, base.ReleaseTrack.GA)
class Transfer(base.Group):
"""Manage Transfer Service jobs, operations, and agents."""
category = base.TRANSFER_CATEGORY
detailed_help = DETAILED_HELP
def Filter(self, context, args):
# TODO(b/190541554): Determine if command group works with project number
base.RequireProjectID(args)
del context, args

View File

@@ -0,0 +1,26 @@
# -*- 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.
"""Transfer agent pool commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.UniverseCompatible
class AgentPools(base.Group):
"""Manage on-premise transfer agent pools."""

View File

@@ -0,0 +1,90 @@
# -*- 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.
"""Command to create a Transfer Service agent pool."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import agent_pools_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import agent_pools_flag_util
from googlecloudsdk.command_lib.transfer import name_util
@base.UniverseCompatible
class Create(base.Command):
"""Create a Transfer Service agent pool."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Create an agent pool -- a group of agents used to connect to a source or
destination filesystem.
""",
'EXAMPLES':
"""\
To create an agent pool with name 'my-pool', display name 'daily backups',
and no bandwidth limit, run:
$ {command} my-pool --display-name='daily backups'
To create an agent pool with name 'my-pool', display name 'daily backups',
and a bandwidth limit of 50 MB/s, run:
$ {command} my-pool --display-name="daily backups" --bandwidth-limit=50
"""
}
# pylint:enable=line-too-long
@staticmethod
def Args(parser):
agent_pools_flag_util.setup_parser(parser)
parser.add_argument(
'--no-async',
action='store_true',
help='Block other tasks in your terminal until the pool has been'
' created. If not included, pool creation will run asynchronously.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_agent_pool_name = name_util.add_agent_pool_prefix(args.name)
agent_pool_id = name_util.remove_agent_pool_prefix(args.name)
agent_pool_project = name_util.get_agent_pool_project_from_string(
formatted_agent_pool_name)
agent_pool_object = messages.AgentPool(
displayName=args.display_name, name=formatted_agent_pool_name)
if args.bandwidth_limit:
agent_pool_object.bandwidthLimit = messages.BandwidthLimit(
limitMbps=args.bandwidth_limit)
initial_result = client.projects_agentPools.Create(
messages.StoragetransferProjectsAgentPoolsCreateRequest(
agentPool=agent_pool_object,
agentPoolId=agent_pool_id,
projectId=agent_pool_project))
if args.no_async:
final_result = agent_pools_util.block_until_created(
formatted_agent_pool_name)
else:
final_result = initial_result
return final_result

View File

@@ -0,0 +1,64 @@
# -*- 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.
"""Command to delete transfer agent pools."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
@base.UniverseCompatible
class Delete(base.Command):
"""Delete a Transfer Service agent pool."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Delete an agent pool. Note that before you can delete a pool, all
the pool's agents must be stopped, its associated jobs must be disabled,
and there must be no associated in-progress transfer operations.
""",
'EXAMPLES':
"""\
To delete agent pool 'foo', run:
$ {command} foo
To check if there are active operations associated with a pool before
deleting it, scroll through the results of:
$ {grandparent_command} operations list --format=yaml --operation-statuses=in_progress
"""
}
# pylint:enable=line-too-long
@staticmethod
def Args(parser):
parser.add_argument('name', help='The name of the job you want to delete.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_agent_pool_name = name_util.add_agent_pool_prefix(args.name)
client.projects_agentPools.Delete(
messages.StoragetransferProjectsAgentPoolsDeleteRequest(
name=formatted_agent_pool_name))

View File

@@ -0,0 +1,53 @@
# -*- 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.
"""Command to get details about a specific agent pool."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import agent_pools_util
from googlecloudsdk.calliope import base
from googlecloudsdk.core.resource import resource_printer
@base.UniverseCompatible
class Describe(base.Command):
"""Get details about a specific agent pool."""
detailed_help = {
'DESCRIPTION':
"""\
Get details about a specific agent pool.
""",
'EXAMPLES':
"""\
To monitor an agent pool, run:
$ {command} NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name', help='The name of the agent pool you want to describe.')
def Display(self, args, resources):
del args # Unsued.
resource_printer.Print(resources, 'json')
def Run(self, args):
return agent_pools_util.api_get(args.name)

View File

@@ -0,0 +1,102 @@
# -*- 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.
"""Command to list transfer agent pools."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import list_util
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import properties
from googlecloudsdk.core.resource import resource_printer
@base.UniverseCompatible
class List(base.Command):
"""List Transfer Service transfer agent pools."""
detailed_help = {
'DESCRIPTION':
"""\
List Transfer Service transfer pools in a given project to show their
configurations.
""",
'EXAMPLES':
"""\
To list all agent pools in your current project, run:
$ {command}
To list agent pools named "foo" and "bar" in your project, run:
$ {command} --names=foo,bar
To list all information about jobs 'foo' and 'bar' formatted as JSON, run:
$ {command} --names=foo,bar --format=json
""",
}
@staticmethod
def Args(parser):
list_util.add_common_list_flags(parser)
parser.add_argument(
'--names',
type=arg_parsers.ArgList(),
metavar='NAMES',
help='The names of the agent pools you want to list. Separate multiple'
' names with commas (e.g., --name=foo,bar). If not specified, all'
' agent pools in your current project will be listed.')
def Display(self, args, resources):
"""API response display logic."""
resource_printer.Print(resources, args.format or 'yaml')
def Run(self, args):
"""Command execution logic."""
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
if args.names:
formatted_agent_pool_names = name_util.add_agent_pool_prefix(args.names)
else:
formatted_agent_pool_names = None
filter_dictionary = {
'agentPoolNames': formatted_agent_pool_names,
}
filter_string = json.dumps(filter_dictionary)
resources_iterator = list_pager.YieldFromList(
client.projects_agentPools,
messages.StoragetransferProjectsAgentPoolsListRequest(
filter=filter_string,
projectId=properties.VALUES.core.project.Get()),
batch_size=args.page_size,
batch_size_attribute='pageSize',
field='agentPools',
limit=args.limit,
)
list_util.print_transfer_resources_iterator(resources_iterator,
self.Display, args)

View File

@@ -0,0 +1,96 @@
# -*- 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.
"""Command to update a Transfer Service agent pool."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import agent_pools_flag_util
from googlecloudsdk.command_lib.transfer import name_util
@base.UniverseCompatible
class Update(base.Command):
"""Update a Transfer Service agent pool."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Update an agent pool.
""",
'EXAMPLES':
"""\
To remove the bandwidth limit for agent pool 'foo', run:
$ {command} foo --clear-bandwidth-limit
To remove the display name for agent pool 'foo', run:
$ {command} foo --clear-display-name
To update the bandwidth limit for agent pool 'foo' to 100 MB/s, run:
$ {command} foo --bandwidth-limit=100
To update the display name for agent pool 'foo' to 'example name', run:
$ {command} foo --display-name="example name"
"""
}
# pylint:enable=line-too-long
@staticmethod
def Args(parser):
agent_pools_flag_util.setup_parser(parser)
parser.add_argument(
'--clear-display-name',
action='store_true',
help='Remove the display name from the agent pool.')
parser.add_argument(
'--clear-bandwidth-limit',
action='store_true',
help="Remove the agent pool's bandwidth limit, which enables the pool's"
' agents to use all bandwidth available to them.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
agent_pool_object = messages.AgentPool()
update_mask_list = []
if args.bandwidth_limit or args.clear_bandwidth_limit:
update_mask_list.append('bandwidth_limit')
if args.bandwidth_limit:
agent_pool_object.bandwidthLimit = messages.BandwidthLimit(
limitMbps=args.bandwidth_limit)
if args.display_name or args.clear_display_name:
update_mask_list.append('display_name')
agent_pool_object.displayName = args.display_name
if update_mask_list:
update_mask = ','.join(update_mask_list)
else:
update_mask = None
formatted_agent_pool_name = name_util.add_agent_pool_prefix(args.name)
return client.projects_agentPools.Patch(
messages.StoragetransferProjectsAgentPoolsPatchRequest(
agentPool=agent_pool_object,
name=formatted_agent_pool_name,
updateMask=update_mask))

View File

@@ -0,0 +1,32 @@
# -*- 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.
"""Transfer agents commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.UniverseCompatible
class Agents(base.Group):
"""Manage Transfer Service agents.
Manage agents. Agents are lightweight applications that enable Transfer
Service users to transfer data to or from POSIX filesystems, such as
on-premises filesystems. Agents are installed locally on your machine and run
within Docker containers.
"""

View File

@@ -0,0 +1,173 @@
# -*- 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.
"""Command to delete transfer agents."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import agents_util
from googlecloudsdk.core.resource import resource_printer
_DELETE_SPECIFIC_AGENTS_MESSAGE = """\
To delete specific agents on your machine, run the following command:
{container_manager} stop {container_ids}
Note: If you encounter a permission error or cannot find the agent, you may need
to add "sudo" before "{container_manager}".
"""
_DELETE_ALL_AGENTS_MESSAGE = """\
To delete all agents on your machine, run the following command:
{container_manager} stop $({container_manager} container list --quiet --all --filter ancestor=gcr.io/cloud-ingest/tsop-agent)
Note: If you encounter a permission error, you may need to add "sudo" before both instances of "{container_manager}".
"""
_UNINSTALL_MESSAGE = """\
To delete all agents on your machine and uninstall the machine's agent container image, run the following commands:
{container_manager} stop $({container_manager} container list --quiet --all --filter ancestor=gcr.io/cloud-ingest/tsop-agent)
# May take a moment for containers to shutdown before you can run:
{container_manager} image rm gcr.io/cloud-ingest/tsop-agent
Note: If you encounter a permission error, you may need to add "sudo" before all three instances of "{container_manager}".
"""
_LIST_AGENTS_MESSAGE = """\
Pick which agents to delete. You can include --all to delete all agents on your machine or --ids to specify agent IDs. You can find agent IDs by running:
{container_manager} container list --all --filter ancestor=gcr.io/cloud-ingest/tsop-agent
"""
_DELETE_COMMAND_DESCRIPTION_TEXT = """\
Delete Transfer Service agents from your machine.
"""
_DELETE_COMMAND_EXAMPLES_TEXT = """\
If you plan to delete specific agents, you can list which agents are running on your machine by running:
$ {container_managers} container list --all --filter ancestor=gcr.io/cloud-ingest/tsop-agent
Then run:
$ {{command}} --ids=id1,id2,...
"""
def _get_detailed_help_text(release_track):
"""Returns the detailed help text for the delete command.
Args:
release_track (base.ReleaseTrack): The release track.
Returns:
A dict containing keys DESCRIPTION, EXAMPLES that provides detailed help.
"""
is_alpha = release_track == base.ReleaseTrack.ALPHA
container_managers = 'docker (or podman)' if is_alpha else 'docker'
return {
'DESCRIPTION': _DELETE_COMMAND_DESCRIPTION_TEXT,
'EXAMPLES': _DELETE_COMMAND_EXAMPLES_TEXT.format(
container_managers=container_managers
),
}
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Delete(base.Command):
"""Delete Transfer Service transfer agents."""
detailed_help = _get_detailed_help_text(base.ReleaseTrack.GA)
@staticmethod
def Args(parser):
mutually_exclusive_flags_group = parser.add_group(
mutex=True, sort_args=False
)
mutually_exclusive_flags_group.add_argument(
'--ids',
type=arg_parsers.ArgList(),
metavar='IDS',
help=(
'The IDs of the agents you want to delete. Separate multiple agent'
' IDs with commas, with no spaces following the commas.'
),
)
mutually_exclusive_flags_group.add_argument(
'--all',
action='store_true',
help='Delete all agents running on your machine.',
)
mutually_exclusive_flags_group.add_argument(
'--uninstall',
action='store_true',
help=(
'Fully uninstall the agent container image in addition to deleting'
' the agents. Uninstalling the container image will free up space,'
" but you'll need to reinstall it to run agents on this machine in"
' the future.'
),
)
def Display(self, args, resources):
del args # Unused.
resource_printer.Print(resources, 'object')
def Run(self, args):
container_manager = agents_util.ContainerManager.from_args(args)
if args.ids:
return _DELETE_SPECIFIC_AGENTS_MESSAGE.format(
container_manager=container_manager.value,
container_ids=' '.join(args.ids),
)
if args.all:
return _DELETE_ALL_AGENTS_MESSAGE.format(
container_manager=container_manager.value,
)
if args.uninstall:
return _UNINSTALL_MESSAGE.format(
container_manager=container_manager.value,
)
return _LIST_AGENTS_MESSAGE.format(
container_manager=container_manager.value,
)
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class DeleteAlpha(Delete):
"""Delete Transfer Service transfer agents."""
detailed_help = _get_detailed_help_text(base.ReleaseTrack.ALPHA)
@staticmethod
def Args(parser):
Delete.Args(parser)
# TODO(b/377355485) - Once Podman support is GA, move this flag to GA track.
parser.add_argument(
'--container-manager',
choices=sorted(
[option.value for option in agents_util.ContainerManager]
),
default=agents_util.ContainerManager.DOCKER.value,
help='The container manager to use for running agents.',
)

View File

@@ -0,0 +1,838 @@
# -*- 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.
"""Command to install on-premise Transfer agent."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import collections
import copy
import os
import shutil
import socket
import subprocess
import sys
from googlecloudsdk.api_lib.transfer import agent_pools_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import agents_util
from googlecloudsdk.command_lib.transfer import creds_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.credentials import gce_cache
from googlecloudsdk.core.universe_descriptor import universe_descriptor
from googlecloudsdk.core.util import platforms
from oauth2client import client as oauth2_client
COUNT_FLAG_HELP_TEXT = """
Specify the number of agents to install on your current machine.
System requirements: 8 GB of memory and 4 CPUs per agent.
Note: If the 'id-prefix' flag is specified, Transfer Service increments a number
value after each prefix. Example: prefix1, prefix2, etc.
"""
CREDS_FILE_FLAG_HELP_TEXT = """
Specify the path to the service account's credentials file.
No input required if authenticating with your user account credentials,
which Transfer Service will look for in your system.
Note that the credentials location will be mounted to the agent container.
"""
MOUNT_DIRECTORIES_HELP_TEXT = """
If you want to grant agents access to specific parts of your filesystem
instead of the entire filesystem, specify which directory paths to
mount to the agent container. Multiple paths must be separated by
commas with no spaces (e.g.,
--mount-directories=/system/path/to/dir1,/path/to/dir2). When mounting
specific directories, gcloud transfer will also mount a directory for
logs (either /tmp or what you've specified for --logs-directory) and
your Google credentials file for agent authentication.
It is strongly recommended that you use this flag. If this flag isn't specified,
gcloud transfer will mount your entire filesystem to the agent container and
give the agent root access.
"""
NETWORK_HELP_TEXT = """
Specify the network to connect the container to. This flag maps directly
to the `--network` flag in the underlying '{container_managers} run' command.
If binding directly to the host's network is an option, then setting this value
to 'host' can dramatically improve transfer performance.
"""
MISSING_PROJECT_ERROR_TEXT = """
Could not find project ID. Try adding the project flag: --project=[project-id]
"""
PROXY_FLAG_HELP_TEXT = """
Specify the HTTP URL and port of a proxy server if you want to use a forward
proxy. For example, to use the URL 'example.com' and port '8080' specify
'http://www.example.com:8080/'
Ensure that you specify the HTTP URL and not an HTTPS URL to avoid
double-wrapping requests in TLS encryption. Double-wrapped requests prevent the
proxy server from sending valid outbound requests.
"""
MISSING_CREDENTIALS_ERROR_TEXT = """
Credentials file not found at {creds_file_path}.
{fix_suggestion}.
Afterwards, re-run {executed_command}.
"""
CHECK_AGENT_CONNECTED_HELP_TEXT_FORMAT = """
To confirm your agents are connected, go to the following link in your browser,
and check that agent status is 'Connected' (it can take a moment for the status
to update and may require a page refresh):
https://console.cloud.google.com/transfer/on-premises/agent-pools/pool/\
{pool}/agents?project={project}
If your agent does not appear in the pool, check its local logs by running
"{logs_command}". The container ID is the string of random
characters printed by step [2/3]. The container ID can also be found by running
"{list_command}".
"""
S3_COMPATIBLE_HELP_TEXT = """
Allow the agent to work with S3-compatible sources. This flag blocks the
agent's ability to work with other source types (e.g., file systems).
When using this flag, you must provide source credentials either as
environment variables `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` or
as default credentials in your system's configuration files.
To provide credentials as environment variables, run:
```
AWS_ACCESS_KEY_ID="id" AWS_SECRET_ACCESS_KEY="secret" gcloud transfer agents install --s3-compatible-mode
```
"""
# Container manager installation guide URLs for the different container managers
# that are supported (currently Docker and Podman).
CONTAINER_MANAGER_INSTALLATION_GUIDE_URL_MAP = {
agents_util.ContainerManager.DOCKER: collections.defaultdict(
# Default guide URL when an OS-specific guide URL is not found.
lambda: 'https://docs.docker.com/engine/install/',
# OS-specific guide URLs.
{
platforms.OperatingSystem.LINUX: (
'https://docs.docker.com/engine/install/'
),
platforms.OperatingSystem.WINDOWS: (
'https://docs.docker.com/engine/install/binaries/#install-server-and-client-binaries-on-windows'
),
platforms.OperatingSystem.MACOSX: (
'https://docs.docker.com/engine/install/binaries/#install-client-binaries-on-macos'
),
},
),
agents_util.ContainerManager.PODMAN: collections.defaultdict(
# Default guide URL when an OS-specific guide URL is not found.
lambda: 'https://podman.io/docs/installation/',
# OS-specific guide URLs.
{
platforms.OperatingSystem.LINUX: (
'https://podman.io/docs/installation/#installing-on-linux'
),
platforms.OperatingSystem.WINDOWS: (
'https://podman.io/docs/installation/#windows'
),
platforms.OperatingSystem.MACOSX: (
'https://podman.io/docs/installation/#macos'
),
},
),
}
# Help text for when the container manager is not found.
CONTAINER_MANAGER_NOT_FOUND_HELP_TEXT = """
The agent runs inside a {container_manager} container, so you'll need
to install {container_manager} before finishing agent installation.
See the installation instructions at
{installation_guide_url} and re-run
'{executed_command}' after {container_manager} installation.
"""
def _get_container_subcommand(use_sudo, container_manager, subcommand):
"""Returns the container command for the given subcommand and container manager.
Args:
use_sudo (bool): Whether to use sudo in the command.
container_manager (agents_util.ContainerManager): The container manager.
subcommand (str): The subcommand to run.
Returns:
str: The container command for the given subcommand and container manager.
"""
sudo_prefix = 'sudo ' if use_sudo else ''
return (
f'{sudo_prefix}{container_manager.value} container'
f' {subcommand} [container ID]'
)
def _expand_path(path):
"""Converts relative and symbolic paths to absolute paths.
Args:
path (str|None): The path to expand. If None, returns None.
Returns:
str|None: The absolute path or None if path is None.
"""
if path is None:
return None
return os.path.abspath(os.path.expanduser(path))
def _get_executed_command():
"""Returns the run command. Does not include environment variables.
Returns:
str: The command that was executed by the user.
"""
return ' '.join(sys.argv)
def _log_created_agent(command):
"""Logs the command used to create the agent.
Args:
command (list[str]): The command used to create the agent.
"""
log.info('Created agent with command:\n{}'.format(' '.join(command)))
def _authenticate_and_get_creds_file_path(creds_file_supplied_by_user=None):
"""Ensures agent will be able to authenticate and returns creds.
Args:
creds_file_supplied_by_user (str): The path to the credentials file.
Returns:
str: The path to the credentials file.
Raises:
OSError: If the credentials file is not found.
"""
# Can't disable near "else" (https://github.com/PyCQA/pylint/issues/872).
# pylint:disable=protected-access
if creds_file_supplied_by_user:
creds_file_path = _expand_path(creds_file_supplied_by_user)
if not os.path.exists(creds_file_path):
fix_suggestion = (
'Check for typos and ensure a creds file exists at the path')
raise OSError(
MISSING_CREDENTIALS_ERROR_TEXT.format(
creds_file_path=creds_file_path,
fix_suggestion=fix_suggestion,
executed_command=_get_executed_command()))
return creds_file_path
creds_file_path = oauth2_client._get_well_known_file()
# pylint:enable=protected-access
if os.path.exists(creds_file_path):
return creds_file_path
if gce_cache.GetOnGCE(check_age=False):
# GCE VMs allow user to authenticate via GCE metadata server.
return None
fix_suggestion = ('To generate a credentials file, please run'
' `gcloud auth application-default login`')
raise OSError(
MISSING_CREDENTIALS_ERROR_TEXT.format(
creds_file_path=creds_file_path,
fix_suggestion=fix_suggestion,
executed_command=_get_executed_command()))
def _check_if_container_manager_is_installed(
container_manager=agents_util.ContainerManager.DOCKER,
):
"""Checks for binary identified by container_manager is in system PATH.
Args:
container_manager (agents_util.ContainerManager): The container manager.
Raises:
OSError: If the binary is not found.
"""
command = container_manager.value
if shutil.which(command):
return
# Raise a message that includes the installation guide URL.
log.error('[2/3] {} not found'.format(command.title()))
help_str = _get_help_text_for_container_manager_not_found(
container_manager=container_manager,
current_os=platforms.OperatingSystem(),
executed_command=_get_executed_command(),
)
raise OSError(help_str)
# Pairs of user arg and container manager flag.
# Coincidence that it's just a case change.
_ADD_IF_PRESENT_PAIRS = [
('enable_multipart', '--enable-multipart'),
('hdfs_data_transfer_protection', '--hdfs-data-transfer-protection'),
('hdfs_namenode_uri', '--hdfs-namenode-uri'),
('hdfs_username', '--hdfs-username'),
('kerberos_config_file', '--kerberos-config-file'),
('kerberos_keytab_file', '--kerberos-keytab-file'),
('kerberos_service_principal', '--kerberos-service-principal'),
('kerberos_user_principal', '--kerberos-user-principal'),
('max_concurrent_small_file_uploads', '--entirefile-fr-parallelism'),
]
def _add_container_flag_if_user_arg_present(user_args, container_args):
"""Adds user flags values directly to Docker/Podman command.
Args:
user_args (argparse.Namespace): The user arguments.
container_args (list[str]): The container arguments.
"""
for user_arg, container_flag in _ADD_IF_PRESENT_PAIRS:
user_value = getattr(user_args, user_arg, None)
if user_value is not None:
container_args.append('{}={}'.format(container_flag, user_value))
def _get_container_run_command(
args, project, creds_file_path, elevate_privileges=False
):
"""Returns container run command from user arguments and generated values.
When `elevate_privileges` is True, the command will be run with sudo and
SELinux will be disabled by passing appropriate security-opt flags. This is
needed for running the agent in a container that is not owned by the user.
Args:
args (argparse.Namespace): The user arguments.
project (str): The project to use for the agent.
creds_file_path (str): The path to the credentials file.
elevate_privileges (bool): Whether to use sudo and disable SELinux.
Returns:
list[str]: The container run command.
"""
base_container_command = []
if elevate_privileges:
base_container_command.append('sudo')
container_manager = agents_util.ContainerManager.from_args(args)
base_container_command.extend([
container_manager.value,
'run',
'--ulimit',
'memlock={}'.format(args.memlock_limit),
'--rm',
'-d',
])
aws_access_key, aws_secret_key = creds_util.get_default_aws_creds()
if aws_access_key:
base_container_command.append('--env')
base_container_command.append('AWS_ACCESS_KEY_ID={}'.format(aws_access_key))
if aws_secret_key:
base_container_command.append('--env')
base_container_command.append(
'AWS_SECRET_ACCESS_KEY={}'.format(aws_secret_key)
)
if args.network:
base_container_command.append('--network={}'.format(args.network))
expanded_creds_file_path = _expand_path(creds_file_path)
expanded_logs_directory_path = _expand_path(args.logs_directory)
root_with_drive = os.path.abspath(os.sep)
root_without_drive = os.sep
mount_entire_filesystem = (
not args.mount_directories
or root_with_drive in args.mount_directories
or root_without_drive in args.mount_directories
)
if mount_entire_filesystem:
base_container_command.append('-v=/:/transfer_root')
else:
# Mount mandatory directories.
mount_flags = [
'-v={}:/tmp'.format(expanded_logs_directory_path),
]
if expanded_creds_file_path is not None:
mount_flags.append(
'-v={creds_file_path}:{creds_file_path}'.format(
creds_file_path=expanded_creds_file_path),
)
for path in args.mount_directories:
# Mount custom directory.
mount_flags.append('-v={path}:{path}'.format(path=path))
base_container_command.extend(mount_flags)
if args.proxy:
base_container_command.append('--env')
base_container_command.append('HTTPS_PROXY={}'.format(args.proxy))
# default docker_uri_prefix is gcr.io/
docker_uri_prefix = 'gcr.io/'
if not properties.IsDefaultUniverse():
universe_descriptor_obj = universe_descriptor.GetUniverseDomainDescriptor()
docker_uri_prefix = (
f'docker.{universe_descriptor_obj.artifact_registry_domain}'
f'/{universe_descriptor_obj.project_prefix}/cloud-ingest/'
)
agent_args = [
f'{docker_uri_prefix}cloud-ingest/tsop-agent:latest',
'--agent-pool={}'.format(args.pool),
'--hostname={}'.format(socket.gethostname()),
'--log-dir={}'.format(expanded_logs_directory_path),
'--project-id={}'.format(project),
]
if expanded_creds_file_path is not None:
agent_args.append('--creds-file={}'.format(expanded_creds_file_path))
if mount_entire_filesystem:
agent_args.append('--enable-mount-directory')
if args.id_prefix:
if args.count is not None:
agent_id_prefix = args.id_prefix + '0'
else:
agent_id_prefix = args.id_prefix
agent_args.append('--agent-id-prefix={}'.format(agent_id_prefix))
gcs_api_endpoint = getattr(args, 'gcs_api_endpoint', None)
if gcs_api_endpoint:
agent_args.append('--gcs-api-endpoint={}'.format(gcs_api_endpoint))
_add_container_flag_if_user_arg_present(args, agent_args)
if args.s3_compatible_mode:
# TODO(b/238213039): Remove when this flag becomes optional.
agent_args.append('--enable-s3')
# Propagate universe domain to the agent if available.
if not properties.IsDefaultUniverse():
universe_domain = properties.VALUES.core.universe_domain.Get()
agent_args.append('--universe-domain={}'.format(universe_domain))
return base_container_command + agent_args
def _execute_container_command(args, project, creds_file_path):
"""Generates, executes, and returns agent install and run command.
Args:
args (argparse.Namespace): The user arguments.
project (str): The project to use for the agent.
creds_file_path (str): The path to the credentials file.
Returns:
list[str]: The container run command.
Raises:
OSError: If the command fails to execute.
"""
container_run_command = _get_container_run_command(
args, project, creds_file_path
)
completed_process = subprocess.run(container_run_command, check=False)
if completed_process.returncode == 0:
_log_created_agent(container_run_command)
return container_run_command
container_manager = agents_util.ContainerManager.from_args(args)
log.status.Print(
'\nCould not execute {} command. Trying with "sudo".'.format(
container_manager.value.title()
)
)
elevated_privileges_container_run_command = _get_container_run_command(
args, project, creds_file_path, elevate_privileges=True
)
elevated_prev_completed_process = subprocess.run(
elevated_privileges_container_run_command, check=False
)
if elevated_prev_completed_process.returncode == 0:
_log_created_agent(elevated_privileges_container_run_command)
return elevated_privileges_container_run_command
command_str = ' '.join(container_run_command)
raise OSError(f'Error executing command:\n{command_str}')
def _create_additional_agents(agent_count, agent_id_prefix, container_command):
"""Creates multiple identical agents.
Args:
agent_count (int): The number of agents to create.
agent_id_prefix (str): The prefix to add to the agent ID.
container_command (list[str]): The container command to execute.
"""
# Find the index of the --agent-id-prefix flag in the docker command.
idx_agent_prefix = -1
for idx, token in enumerate(container_command):
if token.startswith('--agent-id-prefix='):
idx_agent_prefix = idx
break
for count in range(1, agent_count):
# container_command is a list, so copy to avoid mutating the original.
container_command_copy = copy.deepcopy(container_command)
if agent_id_prefix:
# Since agent_id_prefix is not None, we know that idx_agent_prefix is not
# -1.
container_command_copy[idx_agent_prefix] = (
'--agent-id-prefix={}{}'.format(agent_id_prefix, str(count))
)
# Less error handling than before. Just propogate any process errors.
subprocess.run(container_command_copy, check=True)
_log_created_agent(container_command_copy)
def _get_help_text_for_container_manager_not_found(
container_manager, current_os, executed_command
):
"""Returns the help text for when the container manager is not found.
Args:
container_manager (agents_util.ContainerManager): The container manager.
current_os (platforms.OperatingSystem): The current operating system.
executed_command (str): The command that was executed.
Returns:
str: The help text for when the container manager is not found.
Raises:
ValueError: If the container manager is not supported.
"""
if container_manager not in CONTAINER_MANAGER_INSTALLATION_GUIDE_URL_MAP:
raise ValueError(f'Container manager not supported: {container_manager}')
# Get OS-specific installation guide URL for container manager.
installation_guide_url = CONTAINER_MANAGER_INSTALLATION_GUIDE_URL_MAP[
container_manager
][current_os]
return CONTAINER_MANAGER_NOT_FOUND_HELP_TEXT.format(
container_manager=container_manager.value.title(),
installation_guide_url=installation_guide_url,
executed_command=executed_command,
)
INSTALL_CMD_DESCRIPTION_TEXT = """\
Install Transfer Service agents to enable you to transfer data to or from
POSIX filesystems, such as on-premises filesystems. Agents are installed
locally on your machine and run inside {container_managers} containers.
"""
INSTALL_CMD_EXAMPLES_TEXT = """\
To create an agent pool for your agent, see the
`gcloud transfer agent-pools create` command.
To install an agent that authenticates with your user account credentials
and has default agent parameters, run:
$ {command} --pool=AGENT_POOL
You will be prompted to run a command to generate a credentials file if
one does not already exist.
To install an agent that authenticates with a service account with
credentials stored at '/example/path.json', run:
$ {command} --creds-file=/example/path.json --pool=AGENT_POOL
To install an agent using service account impersonation, run:
$ {command} --creds-file=/example/path.json --pool=CUSTOM_AGENT_POOL --impersonate-service-account=impersonated-account@project-id.iam.gserviceaccount.com
Note : The `--impersonate-service-account` flag only applies to the API
calls made by gcloud during the agent installation and authorization process.
The impersonated credentials are not passed to the transfer agent's runtime
environment. The agent itself does not support impersonation and will use
the credentials provided via the `--creds-file` flag or the default gcloud
authenticated account for all of its operations. To grant the agent permissions,
you must provide a service account key with the required direct roles
(e.g., Storage Transfer Agent, Storage Object User)
"""
def _get_detailed_help_text(release_track):
"""Returns the detailed help dictionary for the install command based on the release track.
Args:
release_track (base.ReleaseTrack): The release track.
Returns:
dict[str, str]: The detailed help dictionary for the install command.
"""
is_alpha = release_track == base.ReleaseTrack.ALPHA
container_managers = 'Docker or Podman' if is_alpha else 'Docker'
description_text = INSTALL_CMD_DESCRIPTION_TEXT.format(
container_managers=container_managers
)
return {
'DESCRIPTION': description_text,
'EXAMPLES': INSTALL_CMD_EXAMPLES_TEXT,
}
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA)
class Install(base.Command):
"""Install Transfer Service agents."""
detailed_help = _get_detailed_help_text(base.ReleaseTrack.GA)
@staticmethod
def Args(parser, release_track=base.ReleaseTrack.GA):
"""Add arguments for the install command.
Args:
parser (argparse.ArgumentParser): The argument parser for the command.
release_track (base.ReleaseTrack): The release track.
"""
parser.add_argument(
'--pool',
required=True,
help='The agent pool to associate with the newly installed agent.'
' When creating transfer jobs, the agent pool parameter will determine'
' which agents are activated.')
parser.add_argument('--count', type=int, help=COUNT_FLAG_HELP_TEXT)
parser.add_argument('--creds-file', help=CREDS_FILE_FLAG_HELP_TEXT)
# The flag --docker-network is only supported in GA and will eventually
# be replaced by the --network flag.
if release_track == base.ReleaseTrack.GA:
parser.add_argument(
'--docker-network',
dest='network',
help=NETWORK_HELP_TEXT.format(container_managers='docker'),
)
parser.add_argument(
'--enable-multipart',
action=arg_parsers.StoreTrueFalseAction,
help='Split up files and transfer the resulting chunks in parallel'
' before merging them at the destination. Can be used make transfers of'
' large files faster as long as the network and disk speed are not'
' limiting factors. If unset, agent decides when to use the feature.')
parser.add_argument(
'--id-prefix',
help='An optional prefix to add to the agent ID to help identify the'
' agent.')
parser.add_argument(
'--logs-directory',
default='/tmp',
help='Specify the absolute path to the directory you want to store'
' transfer logs in. If not specified, gcloud transfer will mount your'
' /tmp directory for logs.')
parser.add_argument(
'--memlock-limit',
default=64000000,
type=int,
help="Set the agent container's memlock limit. A value of 64000000"
' (default) or higher is required to ensure that agent versions'
' 1.14 or later have enough locked memory to be able to start.')
parser.add_argument(
'--mount-directories',
type=arg_parsers.ArgList(),
metavar='MOUNT-DIRECTORIES',
help=MOUNT_DIRECTORIES_HELP_TEXT,
)
parser.add_argument('--proxy', help=PROXY_FLAG_HELP_TEXT)
parser.add_argument(
'--s3-compatible-mode',
action='store_true',
help=S3_COMPATIBLE_HELP_TEXT)
hdfs_group = parser.add_group(
category='HDFS',
sort_args=False,
)
hdfs_group.add_argument(
'--hdfs-namenode-uri',
help=(
'A URI representing an HDFS cluster including a schema, namenode,'
' and port. Examples: "rpc://my-namenode:8020",'
' "http://my-namenode:9870".\n\nUse "http" or "https" for WebHDFS.'
' If no schema is'
' provided, the CLI assumes native "rpc". If no port is provided,'
' the default is 8020 for RPC, 9870 for HTTP, and 9871 for HTTPS.'
' For example, the input "my-namenode" becomes'
' "rpc://my-namenode:8020".'
),
)
hdfs_group.add_argument(
'--hdfs-username',
help='Username for connecting to an HDFS cluster with simple auth.',
)
hdfs_group.add_argument(
'--hdfs-data-transfer-protection',
choices=['authentication', 'integrity', 'privacy'],
help=(
'Client-side quality of protection setting for Kerberized clusters.'
' Client-side QOP value cannot be more restrictive than the'
' server-side QOP value.'
),
)
kerberos_group = parser.add_group(
category='Kerberos',
sort_args=False,
)
kerberos_group.add_argument(
'--kerberos-config-file', help='Path to Kerberos config file.'
)
kerberos_group.add_argument(
'--kerberos-keytab-file',
help=(
'Path to a Keytab file containing the user principal specified'
' with the --kerberos-user-principal flag.'
),
)
kerberos_group.add_argument(
'--kerberos-user-principal',
help=(
'Kerberos user principal to use when connecting to an HDFS cluster'
' via Kerberos auth.'
),
)
kerberos_group.add_argument(
'--kerberos-service-principal',
help=(
'Kerberos service principal to use, of the form'
' "<primary>/<instance>". Realm is mapped from your Kerberos'
' config. Any supplied realm is ignored. If not passed in, it will'
' default to "hdfs/<namenode_fqdn>" (fqdn = fully qualified domain'
' name).'
),
)
def Run(self, args):
"""Installs the agent.
Args:
args (argparse.Namespace): The arguments to the command.
"""
if args.count is not None and args.count < 1:
raise ValueError('Agent count must be greater than zero.')
project = properties.VALUES.core.project.Get()
if not project:
raise ValueError(MISSING_PROJECT_ERROR_TEXT)
messages = apis.GetMessagesModule('transfer', 'v1')
if (agent_pools_util.api_get(args.pool).state !=
messages.AgentPool.StateValueValuesEnum.CREATED):
raise ValueError('Agent pool not found: ' + args.pool)
creds_file_path = _authenticate_and_get_creds_file_path(args.creds_file)
log.status.Print('[1/3] Credentials found ✓')
# Get the container_manager attribute on args, or default to Docker
# because we are in the GA surface, we need to be resilient to the
# container_manager flag not being present.
container_manager = agents_util.ContainerManager.from_args(args)
_check_if_container_manager_is_installed(container_manager)
log.status.Print('[2/3] {} found ✓'.format(container_manager.value.title()))
container_command = _execute_container_command(
args, project, creds_file_path
)
if args.count is not None:
_create_additional_agents(args.count, args.id_prefix, container_command)
log.status.Print('[3/3] Agent installation complete! ✓')
# If the user ran the command with sudo, we need to use sudo for the
# subsequent commands (logs and list).
use_sudo = container_command[0] == 'sudo'
log.status.Print(
CHECK_AGENT_CONNECTED_HELP_TEXT_FORMAT.format(
pool=args.pool,
project=project,
logs_command=_get_container_subcommand(
use_sudo,
container_manager,
'logs',
),
list_command=_get_container_subcommand(
use_sudo,
container_manager,
'list',
),
)
)
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class InstallAlpha(Install):
"""Install Transfer Service agents."""
detailed_help = _get_detailed_help_text(base.ReleaseTrack.ALPHA)
@staticmethod
def Args(parser):
"""Add arguments for the install command.
Args:
parser (argparse.ArgumentParser): The argument parser for the command.
"""
Install.Args(parser, release_track=base.ReleaseTrack.ALPHA)
parser.add_argument(
'--max-concurrent-small-file-uploads',
type=int,
help='Adjust the maximum number of files less than or equal to 32 MiB'
' large that the agent can upload in parallel. Not recommended for'
" users unfamiliar with Google Cloud's rate limiting.")
# podman is available in alpha only but the wiring to make it work in GA
# is already in place.
parser.add_argument(
'--container-manager',
choices=sorted(
[option.value for option in agents_util.ContainerManager]
),
default=agents_util.ContainerManager.DOCKER.value,
help='The container manager to use for running agents.',
)
# --network is the new name for the --docker-network flag, once we are ready
# to deprecate and eventually remove the --docker-network flag.
parser.add_argument(
'--network',
dest='network',
help=NETWORK_HELP_TEXT.format(container_managers='(docker or podman)'),
)
parser.add_argument(
'--gcs-api-endpoint',
help=(
'The API endpoint for Google Cloud Storage. Override to use a'
' regional endpoint, ensuring data remains within designated'
' geographic boundaries.'
),
)

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Transfer appliances commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Appliances(base.Group):
"""Manage Transfer Appliances.
Transfer Appliances are high-capacity storage devices that enable
the transfer and secure shipment of data to a Google upload facility, where
data is uploaded to Cloud Storage.
"""

View File

@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to delete Transfer Appliances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import uuid
from googlecloudsdk.api_lib.transfer.appliances import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import exceptions as gcloud_exception
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import resource_args
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Delete(base.DeleteCommand):
"""Delete a transfer appliance."""
detailed_help = {
'DESCRIPTION':
"""\
Delete a specific transfer appliance.
""",
'EXAMPLES':
"""\
To delete an appliance, run:
$ {command} APPLIANCE
""",
}
@staticmethod
def Args(parser):
resource_args.add_appliance_resource_arg(
parser, verb=resource_args.ResourceVerb.DELETE)
@gcloud_exception.CatchHTTPErrorRaiseHTTPException(
'Status code: {status_code}. {status_message}.'
)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
name = args.CONCEPTS.appliance.Parse().RelativeName()
operation = client.projects_locations_appliances.Delete(
messages.TransferapplianceProjectsLocationsAppliancesDeleteRequest(
name=name, requestId=uuid.uuid4().hex
)
)
return operations_util.wait_then_yield_nothing(
operation, 'delete appliance')

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to describe Transfer Appliance Orders."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import offline_import_printer
from googlecloudsdk.command_lib.transfer.appliances import resource_args
from googlecloudsdk.core.resource import resource_printer
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Describe(base.DescribeCommand):
"""Get configuration details about a Transfer Appliance."""
detailed_help = {
'DESCRIPTION':
"""\
Get configuration details about a specific transfer appliance.
""",
'EXAMPLES':
"""\
To describe an appliance, run:
$ {command} APPLIANCE
To view details of the order associated with an appliance, first obtain
the ORDER identifier, then use it to look up the order:
$ {command} APPLIANCE --format="value(order)"
$ {command} orders describe ORDER
""",
}
@staticmethod
def Args(parser):
resource_args.add_appliance_resource_arg(
parser, resource_args.ResourceVerb.DESCRIBE)
resource_printer.RegisterFormatter(
offline_import_printer.OFFLINE_IMPORT_PRINTER_FORMAT,
offline_import_printer.OfflineImportPrinter,
)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
appliance_ref = args.CONCEPTS.appliance.Parse()
request = messages.TransferapplianceProjectsLocationsAppliancesGetRequest(
name=appliance_ref.RelativeName())
return client.projects_locations_appliances.Get(request=request)

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to list Transfer Appliances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import resource_args
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class List(base.ListCommand):
"""List Transfer Appliances."""
detailed_help = {
'DESCRIPTION':
"""\
List Transfer Appliances in a given project to show their state and
corresponding orders.
""",
'EXAMPLES':
"""\
To list all appliances in your current project, run:
$ {command}
To list all information about all jobs formatted as JSON, run:
$ {command} --format=json
""",
}
@staticmethod
def Args(parser):
resource_args.add_list_resource_args(parser, listing_orders=False)
parser.display_info.AddFormat(
"""
yaml(displayName,model,name,sessionId,order,state)
""")
def Run(self, args):
"""Command execution logic."""
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
return list_pager.YieldFromList(
client.projects_locations_appliances,
messages.TransferapplianceProjectsLocationsAppliancesListRequest(
filter=resource_args.parse_list_resource_args_as_filter_string(
args, listing_orders=False),
orderBy='name asc',
parent=resource_args.get_parent_string(args.region)),
batch_size_attribute='pageSize',
field='appliances')

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Transfer Appliance Order commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Orders(base.Group):
"""Manage Transfer Appliance Orders.
Transfer Appliances are high-capacity storage devices that enable
the transfer and secure shipment of data to a Google upload facility, where
data is uploaded to Cloud Storage.
"""

View File

@@ -0,0 +1,175 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to create transfer appliance orders."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import uuid
from googlecloudsdk.api_lib.transfer.appliances import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import exceptions as gcloud_exception
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import flags
from googlecloudsdk.command_lib.transfer.appliances import mapping_util
from googlecloudsdk.command_lib.transfer.appliances import regions
from googlecloudsdk.command_lib.transfer.appliances import resource_args
DETAILED_HELP = {
'DESCRIPTION':
"""
*{command}* facilitates the creation of Transfer Appliance orders.
When an order is created, an appliance record is created as well.
""",
'EXAMPLES':
"""
To order a rackable appliance with 40 TB of storage, named
`my-appliance`, a Cloud Storage destination of `my-bucket` and the
minimum amount of contact information.
$ {command} my-appliance \
--model=TA40_RACKABLE \
--shipping-contact="name=Jane Doe,emails=[jane@example.com],phone=12345678910" \
--offline-import=gs://my-bucket \
--order-contact="name=John Doe,phone=123456578910,emails=[john@example.com]" --country=US \
--address="lines=['1600 Amphitheatre Parkway'],locality=Mountain View,administrative-area=CA,postal-code=94043"
To clone an appliance order with the ID `my-appliance` and location
`us-central1`, only changing the name and Cloud Storage destination:
$ {command} \
my-other-appliance --country=US --clone=my-appliance \
--clone-region=us-central1 --offline-import=my-other-bucket
To use a flags file to create an appliance rather than provide a
long list of flags:
$ {command} my-appliance \
--flags-file=FLAGS_FILE
Example file with all possible flags set:
--address:
lines:
- 1600 Amphitheatre Parkway
locality: Mountain View
administrative-area: California
postal-code: 94043
--cmek: projects/p/locations/global/keyRings/kr/cryptoKeys/ck
--country: US
--delivery-notes: None
--display-name: test
--internet-enabled:
--model: TA40_RACKABLE
--offline-export:
source: gs://my-bucket/path
manifest: gs://my-other-bucket/manifest
--offline-import: gs://my-bucket/path
--online-import: gs://my-bucket/path
--order-contact:
business: Google
name: Jane Doe
phone: 1234567890
emails:
- janedoe@example.com
--shipping-contact:
business: Google
name: John Doe
phone: 1234567890
emails:
- johndoe@example.com
""",
}
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Create(base.Command):
"""Create an order for a transfer appliance."""
detailed_help = DETAILED_HELP
@staticmethod
def Args(parser):
parser.add_argument(
'name',
help='Immutable ID that will uniquely identify the appliance.')
parser.add_argument(
'--submit',
action='store_true',
help=(
'When specified the order will be submitted immediately. By '
'default, orders are created in a draft state. Use '
'`{parent_command} update --submit` to submit the order later.'
)
)
resource_args.add_clone_resource_arg(parser)
flags.add_appliance_settings(parser)
flags.add_delivery_information(parser)
@gcloud_exception.CatchHTTPErrorRaiseHTTPException(
'Status code: {status_code}. {status_message}.'
)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
appliance = messages.Appliance()
order = messages.Order()
results = []
region = regions.COUNTRY_TO_LOCATION_MAP[args.country]
parent = resource_args.get_parent_string(region)
if args.IsSpecified('clone'):
# Clone-specific logic.
clone_ref = args.CONCEPTS.clone.Parse()
order = client.projects_locations_orders.Get(
request=messages.TransferapplianceProjectsLocationsOrdersGetRequest(
name=clone_ref.RelativeName()))
if order.appliances:
# We only use the first appliance in a clone operation, as the
# workflow expects a 1:1 relationship of orders to appliances.
appliance = client.projects_locations_appliances.Get(
messages.TransferapplianceProjectsLocationsAppliancesGetRequest(
name=order.appliances[0]))
# Map args to the appliance resource and make the API call, append result.
mapping_util.apply_args_to_appliance(appliance, args)
operation = client.projects_locations_appliances.Create(
messages.TransferapplianceProjectsLocationsAppliancesCreateRequest(
appliance=appliance,
applianceId=args.name,
parent=parent,
requestId=uuid.uuid4().hex))
results.append(operations_util.wait_then_yield_appliance(
operation, 'create'))
# Map args to the order resource, make the API call, append result.
appliance_name = resource_args.get_appliance_name(region, args.name)
mapping_util.apply_args_to_order(order, args, appliance_name)
order.skipDraft = args.submit
operation = client.projects_locations_orders.Create(
messages.TransferapplianceProjectsLocationsOrdersCreateRequest(
order=order,
orderId=args.name,
parent=parent,
requestId=uuid.uuid4().hex))
results.append(operations_util.wait_then_yield_order(
operation, 'create'))
return results

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to delete Transfer Appliance Orders."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import uuid
from googlecloudsdk.api_lib.transfer.appliances import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import exceptions as gcloud_exception
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import resource_args
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Delete(base.DeleteCommand):
"""Delete transfer appliance orders."""
detailed_help = {
'DESCRIPTION':
"""\
Delete transfer appliance orders.
""",
'EXAMPLES':
"""\
To delete an order, run:
$ {command} ORDER
To delete an order but keep the associated appliance records:
$ {command} ORDER --keep-appliances
""",
}
@staticmethod
def Args(parser):
resource_args.add_order_resource_arg(
parser, verb=resource_args.ResourceVerb.DELETE)
parser.add_argument(
'--keep-appliances',
action='store_true',
help=(
'Keep appliances associated with the order rather than deleting'
' them.'
))
@gcloud_exception.CatchHTTPErrorRaiseHTTPException(
'Status code: {status_code}. {status_message}.'
)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
order_ref = args.CONCEPTS.order.Parse()
# Get the order first to get to the appliance names to delete.
if not args.keep_appliances:
request = messages.TransferapplianceProjectsLocationsOrdersGetRequest(
name=order_ref.RelativeName())
order = client.projects_locations_orders.Get(request=request)
for appliance_name in order.appliances:
operation = client.projects_locations_appliances.Delete(
messages.TransferapplianceProjectsLocationsAppliancesDeleteRequest(
name=appliance_name, requestId=uuid.uuid4().hex))
operations_util.wait_then_yield_nothing(operation, 'delete appliance')
operation = client.projects_locations_orders.Delete(
messages.TransferapplianceProjectsLocationsOrdersDeleteRequest(
name=order_ref.RelativeName(), requestId=uuid.uuid4().hex))
return operations_util.wait_then_yield_nothing(operation, 'delete order')

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to describe Transfer Appliance Orders."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import resource_args
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Describe(base.DescribeCommand):
"""Get information about Transfer Appliance orders."""
detailed_help = {
'DESCRIPTION':
"""\
Get information about transfer appliance orders.
""",
'EXAMPLES':
"""\
To describe an order by name, including its prefix, run:
$ {command} ORDER --region=REGION
""",
}
@staticmethod
def Args(parser):
resource_args.add_order_resource_arg(
parser, resource_args.ResourceVerb.DESCRIBE)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
order_ref = args.CONCEPTS.order.Parse()
request = messages.TransferapplianceProjectsLocationsOrdersGetRequest(
name=order_ref.RelativeName())
return client.projects_locations_orders.Get(request=request)

View File

@@ -0,0 +1,72 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to list Transfer Appliances."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import resource_args
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class List(base.ListCommand):
"""List Transfer Appliance orders."""
detailed_help = {
'DESCRIPTION':
"""\
List Transfer Appliances in a given project to show their state and
corresponding orders.
""",
'EXAMPLES':
"""\
To list all appliances in your current project, run:
$ {command}
To list all information about all jobs formatted as JSON, run:
$ {command} --format=json
""",
}
@staticmethod
def Args(parser):
resource_args.add_list_resource_args(parser, listing_orders=True)
parser.display_info.AddFormat(
"""
yaml(name,appliances,state,submit_time.date(),update_time.date())
""")
def Run(self, args):
"""Command execution logic."""
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
return list_pager.YieldFromList(
client.projects_locations_orders,
messages.TransferapplianceProjectsLocationsOrdersListRequest(
filter=resource_args.parse_list_resource_args_as_filter_string(
args, listing_orders=True),
orderBy='name asc',
parent=resource_args.get_parent_string(args.region)),
batch_size_attribute='pageSize',
field='orders')

View File

@@ -0,0 +1,124 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Command to update transfer appliance orders."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import uuid
from googlecloudsdk.api_lib.transfer.appliances import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.api_lib.util import exceptions as gcloud_exception
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer.appliances import flags
from googlecloudsdk.command_lib.transfer.appliances import mapping_util
from googlecloudsdk.command_lib.transfer.appliances import resource_args
from googlecloudsdk.core import log
DETAILED_HELP = {
'DESCRIPTION':
"""
*{command}* facilitates the update of Transfer Appliance Orders.
""",
'EXAMPLES':
"""
To update the shipping contact of an appliance called `my-appliance`:
$ {command} my-appliance --shipping-contact="name=Jane Doe,emails=[jane@example.com],phone=12345678910"
""",
}
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Update(base.Command):
"""Update an order for a transfer appliance."""
detailed_help = DETAILED_HELP
@staticmethod
def Args(parser):
resource_args.add_order_resource_arg(
parser, resource_args.ResourceVerb.UPDATE)
parser.add_argument(
'--submit',
action='store_true',
help='Submits an order that is in draft.')
flags.add_appliance_settings(parser, for_create_command=False)
flags.add_delivery_information(parser, for_create_command=False)
@gcloud_exception.CatchHTTPErrorRaiseHTTPException(
'Status code: {status_code}. {status_message}.'
)
def Run(self, args):
client = apis.GetClientInstance('transferappliance', 'v1alpha1')
messages = apis.GetMessagesModule('transferappliance', 'v1alpha1')
name = args.CONCEPTS.order.Parse().RelativeName()
results = []
# Get the order first to get to the appliance ID.
order = client.projects_locations_orders.Get(
messages.TransferapplianceProjectsLocationsOrdersGetRequest(name=name))
if order.appliances:
# We only use the first appliance in update operations, as the workflow
# expects a 1:1 relationship of orders to appliances.
appliance_name = order.appliances[0]
if len(order.appliances) > 1:
log.warning(
'Only 1 appliance per order is supported. {} will be updated and'
' all others will be ignored.'.format(appliance_name))
appliance = messages.Appliance()
update_mask = mapping_util.apply_args_to_appliance(appliance, args)
if update_mask:
operation = client.projects_locations_appliances.Patch(
messages.TransferapplianceProjectsLocationsAppliancesPatchRequest(
name=appliance_name,
appliance=appliance,
requestId=uuid.uuid4().hex,
updateMask=update_mask,
)
)
results.append(operations_util.wait_then_yield_appliance(
operation, 'update'))
# Map args to the order resource, make the API call if needed.
update_mask = mapping_util.apply_args_to_order(order, args)
if update_mask:
operation = client.projects_locations_orders.Patch(
messages.TransferapplianceProjectsLocationsOrdersPatchRequest(
name=name,
order=order,
requestId=uuid.uuid4().hex,
updateMask=update_mask,
)
)
results.append(operations_util.wait_then_yield_order(operation, 'update'))
if args.submit:
operation = client.projects_locations_orders.Submit(
messages.TransferapplianceProjectsLocationsOrdersSubmitRequest(
name=name))
if update_mask:
# We don't want to dump out the order twice, so when an order update
# already occurred we just wait for the submit operation to complete.
operations_util.wait_then_yield_nothing(operation, 'submit')
else:
# Since there's no update operation on the order we can yield an order
# and add it to the result output.
results.append(operations_util.wait_then_yield_order(
operation, 'submit'))
if not results:
log.warning('No updates were performed.')
return results

View File

@@ -0,0 +1,231 @@
# -*- coding: utf-8 -*- #
# Copyright 2022 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.
"""Command to authorize accounts for transfer."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
import os
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.projects import util as projects_util
from googlecloudsdk.core import log
from googlecloudsdk.core import properties
from googlecloudsdk.core.credentials import creds
from googlecloudsdk.core.credentials import store as creds_store
from googlecloudsdk.core.universe_descriptor import universe_descriptor
from googlecloudsdk.core.util import files
EXPECTED_USER_ROLES = frozenset([
'roles/owner',
'roles/storagetransfer.admin',
'roles/storagetransfer.transferAgent',
'roles/storage.objectAdmin',
'roles/pubsub.editor',
])
EXPECTED_P4SA_ROLES = frozenset([
'roles/storage.admin',
'roles/storagetransfer.serviceAgent',
])
EXPECTED_GCS_SA_ROLES = frozenset(['roles/pubsub.publisher'])
SERVICE_ACCOUNT_URL_FORMAT = (
'serviceAccount:service-{project_number}@{service_account_url_suffix}'
)
def _get_iam_prefixed_email(email_string, is_service_account):
"""Returns an email format useful for interacting with IAM APIs."""
iam_prefix = 'serviceAccount' if is_service_account else 'user'
return '{}:{}'.format(iam_prefix, email_string)
def _get_iam_prefiexed_gcs_sa_email(project_number):
"""Returns a GCS SA email."""
project_prefix = (
universe_descriptor.UniverseDescriptor()
.Get(properties.VALUES.core.universe_domain.Get())
.project_prefix
)
if project_prefix:
service_account_url_suffix = (
f'gs-project-accounts.{project_prefix}.iam.gserviceaccount.com'
)
else:
service_account_url_suffix = 'gs-project-accounts.iam.gserviceaccount.com'
return SERVICE_ACCOUNT_URL_FORMAT.format(
project_number=project_number,
service_account_url_suffix=service_account_url_suffix,
)
def _get_existing_transfer_roles_for_account(
project_iam_policy, prefixed_account_email, roles_set
):
"""Returns roles in IAM policy from roles_set assigned to account email."""
roles = set()
# iam_policy.bindings structure:
# list[<Binding
# members=['serviceAccount:member@thing.iam.gserviceaccount.com', ...],
# role='roles/somerole'>...]
for binding in project_iam_policy.bindings:
if (any([m == prefixed_account_email for m in binding.members]) and
binding.role in roles_set):
roles.add(binding.role)
return roles
@base.UniverseCompatible
class Authorize(base.Command):
"""Authorize an account for all Transfer Service features."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Authorize a Google account for all Transfer Service features.
This command provides admin and owner rights for simplicity. If that's
too much authority for your use case, see custom setups here:
https://cloud.google.com/storage-transfer/docs/on-prem-set-up
""",
'EXAMPLES':
"""\
To see what Transfer Service IAM roles the account logged into gcloud may
be missing, run:
$ {command}
To add the missing IAM roles, run:
$ {command} --add-missing
To check a custom service account for missing roles, run:
$ {command} --creds-file=path/to/service-account-key.json
"""
}
@staticmethod
def Args(parser):
parser.add_argument(
'--creds-file',
help='The path to the creds file for an account to authorize.'
' The file should be in JSON format and contain a "type" and'
' "client_email", which are automatically generated for most'
' creds files downloaded from Google (e.g. service account tokens).'
' If this flag is not present, the command authorizes the user'
' currently logged into gcloud.')
parser.add_argument(
'--add-missing',
action='store_true',
help='Add IAM roles necessary to use all Transfer Service'
' features to the specified account. By default, this command just'
' prints missing roles.')
def Run(self, args):
client = apis.GetClientInstance('storagetransfer', 'v1')
messages = apis.GetMessagesModule('storagetransfer', 'v1')
if args.creds_file:
expanded_file_path = os.path.abspath(os.path.expanduser(args.creds_file))
with files.FileReader(expanded_file_path) as file_reader:
try:
parsed_creds_file = json.load(file_reader)
account_email = parsed_creds_file['client_email']
is_service_account = parsed_creds_file['type'] == 'service_account'
except (ValueError, KeyError) as e:
log.error(e)
raise ValueError('Invalid creds file format.'
' Run command with "--help" flag for more details.')
prefixed_account_email = _get_iam_prefixed_email(
account_email, is_service_account)
else:
account_email = properties.VALUES.core.account.Get()
is_service_account = creds.IsServiceAccountCredentials(creds_store.Load())
prefixed_account_email = _get_iam_prefixed_email(account_email,
is_service_account)
project_id = properties.VALUES.core.project.Get()
parsed_project_id = projects_util.ParseProject(project_id)
project_iam_policy = projects_api.GetIamPolicy(parsed_project_id)
existing_user_roles = _get_existing_transfer_roles_for_account(
project_iam_policy, prefixed_account_email, EXPECTED_USER_ROLES)
log.status.Print('User {} has roles:\n{}'.format(account_email,
list(existing_user_roles)))
missing_user_roles = EXPECTED_USER_ROLES - existing_user_roles
log.status.Print('Missing roles:\n{}'.format(list(missing_user_roles)))
all_missing_role_tuples = [
(prefixed_account_email, role) for role in missing_user_roles
]
log.status.Print('***')
transfer_p4sa_email = client.googleServiceAccounts.Get(
messages.StoragetransferGoogleServiceAccountsGetRequest(
projectId=project_id)).accountEmail
prefixed_transfer_p4sa_email = _get_iam_prefixed_email(
transfer_p4sa_email, is_service_account=True)
existing_p4sa_roles = _get_existing_transfer_roles_for_account(
project_iam_policy, prefixed_transfer_p4sa_email, EXPECTED_P4SA_ROLES)
log.status.Print('Google-managed transfer account {} has roles:\n{}'.format(
transfer_p4sa_email, list(existing_p4sa_roles)))
missing_p4sa_roles = EXPECTED_P4SA_ROLES - existing_p4sa_roles
log.status.Print('Missing roles:\n{}'.format(list(missing_p4sa_roles)))
all_missing_role_tuples += [
(prefixed_transfer_p4sa_email, role) for role in missing_p4sa_roles
]
if self.ReleaseTrack() is base.ReleaseTrack.ALPHA:
project_number = projects_util.GetProjectNumber(project_id)
prefixed_gcs_sa_email = _get_iam_prefiexed_gcs_sa_email(project_number)
existing_gcs_sa_roles = _get_existing_transfer_roles_for_account(
project_iam_policy, prefixed_gcs_sa_email, EXPECTED_GCS_SA_ROLES)
log.status.Print('***')
log.status.Print(
'Google-managed service account {} has roles:\n{}'.format(
prefixed_gcs_sa_email, list(existing_gcs_sa_roles)
)
)
missing_gcs_sa_roles = EXPECTED_GCS_SA_ROLES - existing_gcs_sa_roles
log.status.Print('Missing roles:\n{}'.format(list(missing_gcs_sa_roles)))
all_missing_role_tuples += [
(prefixed_gcs_sa_email, role) for role in missing_gcs_sa_roles
]
if args.add_missing or all_missing_role_tuples:
log.status.Print('***')
if args.add_missing:
if all_missing_role_tuples:
log.status.Print('Adding roles:\n{}'.format(all_missing_role_tuples))
projects_api.AddIamPolicyBindings(parsed_project_id,
all_missing_role_tuples)
log.status.Print('***')
# Source:
# https://cloud.google.com/iam/docs/granting-changing-revoking-access
log.status.Print(
'Done. Permissions typically take seconds to propagate, but,'
' in some cases, it can take up to seven minutes.')
else:
log.status.Print('No missing roles to add.')
else:
log.status.Print('Rerun with --add-missing to add missing roles.')

View File

@@ -0,0 +1,26 @@
# -*- 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.
"""Transfer jobs commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.UniverseCompatible
class Jobs(base.Group):
"""Manage transfer jobs."""

View File

@@ -0,0 +1,26 @@
# -*- 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.
"""Command group for managing Storagetransfer transfer job configurations."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class Config(base.Group):
"""Manage Storagetransfer transfer job configurations."""

View File

@@ -0,0 +1,38 @@
release_tracks: [ALPHA]
command_type: CONFIG_EXPORT
help_text:
brief: Export the configuration for a Storagetransfer transfer job.
description: |
*{command}* exports the configuration for a Storagetransfer transfer job.
Transfer job configurations can be exported in
Kubernetes Resource Model (krm) or Terraform HCL formats. The
default format is `krm`.
Specifying `--all` allows you to export the configurations for all
transfer jobs within the project.
Specifying `--path` allows you to export the configuration(s) to
a local directory.
examples: |
To export the configuration for a transfer job, run:
$ {command} my-transfer-job
To export the configuration for a transfer job to a file, run:
$ {command} my-transfer-job --path=/path/to/dir/
To export the configuration for a transfer job in Terraform
HCL format, run:
$ {command} my-transfer-job --resource-format=terraform
To export the configurations for all transfer jobs within a
project, run:
$ {command} --all
arguments:
resource:
help_text: Transfer job to export the configuration for.
spec: !REF googlecloudsdk.command_lib.transfer.resources:transfer_job

View File

@@ -0,0 +1,107 @@
# -*- 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.
"""Command to create transfer jobs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.storage import storage_url
from googlecloudsdk.command_lib.transfer import jobs_apitools_util
from googlecloudsdk.command_lib.transfer import jobs_flag_util
from googlecloudsdk.core import log
@base.UniverseCompatible
class Create(base.Command):
"""Create a Transfer Service transfer job."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Create a Transfer Service transfer job, allowing you to transfer data to
Google Cloud Storage on a one-time or recurring basis.
""",
'EXAMPLES':
"""\
To create a one-time, immediate transfer job to move data from Google
Cloud Storage bucket "foo" into the "baz" folder in Cloud Storage bucket
"bar", run:
$ {command} gs://foo gs://bar/baz/
To create a transfer job to move data from an Amazon S3 bucket called
"foo" to a Google Cloud Storage bucket named "bar" that runs every day
with custom name "my-test-job", run:
$ {command} s3://foo gs://bar --name=my-test-job --source-creds-file=/examplefolder/creds.txt --schedule-repeats-every=1d
To create a one-time, immediate transfer job to move data between Google
Cloud Storage buckets "foo" and "bar" with filters to include objects that
start with prefixes "baz" and "qux"; and objects modified in the 24 hours
before the transfer started, run:
$ {command} gs://foo gs://bar/ --include-prefixes=baz,qux --include-modified-after-relative=1d
To create a one-time, immediate transfer job to move data from a directory
with absolute path "/foo/bar/" in the filesystem associated with
agent pool "my-pool" into Google Cloud Storage bucket "example-bucket",
run:
$ {command} posix:///foo/bar/ gs://example-bucket --source-agent-pool=my-pool
"""
}
# pylint:enable=line-too-long
@classmethod
def Args(cls, parser):
jobs_flag_util.setup_parser(parser, release_track=cls.ReleaseTrack())
def Run(self, args):
is_hdfs_source = args.source.startswith(
storage_url.ProviderPrefix.HDFS.value
)
is_posix_source = args.source.startswith(
storage_url.ProviderPrefix.POSIX.value
)
is_posix_destination = args.destination.startswith(
storage_url.ProviderPrefix.POSIX.value
)
if (is_hdfs_source or is_posix_source) and not args.source_agent_pool:
raise ValueError(
'Missing agent pool. Please add --source-agent-pool flag.')
if is_posix_destination and not args.destination_agent_pool:
raise ValueError(
'Missing agent pool. Please add --destination-agent-pool flag.')
if (is_posix_source and is_posix_destination and
not args.intermediate_storage_path):
raise ValueError('Missing intermediate storage path.'
' Please add --intermediate-storage-path flag.')
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
result = client.transferJobs.Create(
jobs_apitools_util.generate_transfer_job_message(args, messages))
if args.no_async:
log.status.Print('Created job: {}'.format(result.name))
operations_util.block_until_done(job_name=result.name)
return result

View File

@@ -0,0 +1,59 @@
# -*- 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.
"""Command to delete transfer jobs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import jobs_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import properties
@base.UniverseCompatible
class Delete(base.Command):
"""Delete a Transfer Service transfer job."""
detailed_help = {
'DESCRIPTION':
"""\
Delete a Transfer Service transfer job.
""",
'EXAMPLES':
"""\
To delete job 'foo', run:
$ {command} foo
"""
}
@staticmethod
def Args(parser):
parser.add_argument('name', help='The name of the job you want to delete.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_job_name = name_util.add_job_prefix(args.name)
client.transferJobs.Delete(
messages.StoragetransferTransferJobsDeleteRequest(
jobName=formatted_job_name,
projectId=properties.VALUES.core.project.Get()))
# Display metadata of job with status updated to `DELETED`.
return jobs_util.api_get(args.name)

View File

@@ -0,0 +1,61 @@
# -*- 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.
"""Command to get details on a transfer job."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import jobs_util
from googlecloudsdk.calliope import base
from googlecloudsdk.core.resource import resource_printer
@base.UniverseCompatible
class Describe(base.Command):
"""Get configuration and latest operation details about transfer job."""
detailed_help = {
'DESCRIPTION':
"""\
Get configuration and latest operation details about a specific transfer
job.
""",
'EXAMPLES':
"""\
To describe a job, run:
$ {command} JOB-NAME
If you're looking for recent error details, use the "latestOperationName"
returned by this command as input to the "operations describe" command:
$ {command} JOB-NAME --format="json(latestOperationName)"
$ {grandparent_command} operations describe OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name', help='The name of the job you want to describe.')
def Display(self, args, resources):
del args # Unsued.
resource_printer.Print(resources, 'json')
def Run(self, args):
return jobs_util.api_get(args.name)

View File

@@ -0,0 +1,175 @@
# -*- 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.
"""Command to list Transfer jobs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import enum
import json
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import list_util
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import properties
from googlecloudsdk.core.resource import resource_printer
class JobType(enum.Enum):
"""The type of the job."""
TRANSFER = 'transfer'
REPLICATION = 'replication'
@base.UniverseCompatible
@base.ReleaseTracks(base.ReleaseTrack.GA)
class List(base.Command):
"""List Transfer Service transfer jobs."""
detailed_help = {
'DESCRIPTION':
"""\
List Transfer Service transfer jobs in a given project to show their
configurations and latest operations.
""",
'EXAMPLES':
"""\
To list all jobs in your current project, run:
$ {command}
To list all disabled jobs in your project, run:
$ {command} --job-statuses=disabled
To list jobs 'foo' and 'bar', run:
$ {command} --job-names=foo,bar
To list all information about all jobs formatted as JSON, run:
$ {command} --format=json
To list all information about jobs 'foo' and 'bar' formatted as YAML, run:
$ {command} --job-names=foo,bar --format=YAML
""",
}
@staticmethod
def Args(parser):
parser.SetSortArgs(False)
list_util.add_common_list_flags(parser)
parser.add_argument(
'--job-names',
type=arg_parsers.ArgList(),
metavar='JOB_NAMES',
help='The names of the jobs you want to list. Separate multiple job'
' names with commas (e.g., --job-names=foo,bar). If not specified,'
' all jobs will be listed.')
parser.add_argument(
'--job-statuses',
type=arg_parsers.ArgList(),
metavar='JOB_STATUSES',
help='List only jobs with the statuses you specify.'
" Options include 'enabled', 'disabled', 'deleted' (case"
' insensitive). Separate multiple statuses with commas (e.g.,'
' --job-statuses=enabled,deleted). If not specified, all jobs will'
' be listed.')
parser.add_argument(
'--expand-table',
action='store_true',
help='Include additional table columns (job name, source, destination,'
' frequency, lastest operation name, job status) in command output.'
' Tip: increase the size of your terminal before running the command.')
parser.add_argument(
'--job-type',
choices=[JobType.TRANSFER.value, JobType.REPLICATION.value],
default=JobType.TRANSFER.value,
help='The type of the job you want to list.',
)
def Display(self, args, resources):
"""API response display logic."""
if args.expand_table:
# Removes unwanted "transferJobs/" and "transferOperations/" prefixes.
format_string = """table(
name.slice(13:).join(sep=''),
firstof(transferSpec, replicationSpec).firstof(
gcsDataSource, awsS3DataSource, httpDataSource,
azureBlobStorageDataSource, posixDataSource, hdfsDataSource
).firstof(
bucketName, listUrl, container, rootDirectory, path
).trailoff(45):label=SOURCE,
firstof(transferSpec, replicationSpec).firstof(
gcsDataSink, posixDataSink
).firstof(
bucketName, rootDirectory
).trailoff(45):label=DESTINATION,
latestOperationName.slice(19:).join(sep=''),
status)
"""
else:
format_string = """table(
name.slice(13:).join(sep=''),
latestOperationName.slice(19:).join(sep=''))
"""
resource_printer.Print(resources, args.format or format_string)
def Run(self, args):
"""Command execution logic."""
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
if args.job_names:
formatted_job_names = name_util.add_job_prefix(args.job_names)
else:
formatted_job_names = None
job_statuses = args.job_statuses or None
filter_dictionary = {
'jobNames': formatted_job_names,
'jobStatuses': job_statuses,
'projectId': properties.VALUES.core.project.Get(),
}
if args.job_type == JobType.REPLICATION.value:
# Filter to list replication jobs.
filter_dictionary['dataBackend'] = 'QUERY_REPLICATION_CONFIGS'
filter_string = json.dumps(filter_dictionary)
resources_iterator = list_pager.YieldFromList(
client.transferJobs,
messages.StoragetransferTransferJobsListRequest(filter=filter_string),
batch_size=args.page_size,
batch_size_attribute='pageSize',
field='transferJobs',
limit=args.limit,
)
list_util.print_transfer_resources_iterator(resources_iterator,
self.Display, args)
@base.DefaultUniverseOnly
@base.ReleaseTracks(base.ReleaseTrack.ALPHA)
class ListAlpha(List):
"""List Transfer Service transfer jobs."""
@staticmethod
def Args(parser):
List.Args(parser)

View File

@@ -0,0 +1,59 @@
# -*- 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.
"""Command to monitor the last operation of a transfer job."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import jobs_util
from googlecloudsdk.api_lib.transfer import operations_util
from googlecloudsdk.calliope import base
@base.UniverseCompatible
class Monitor(base.Command):
"""Track progress in real time for a transfer job's latest operation."""
detailed_help = {
'DESCRIPTION':
"""\
Track progress in real time for a transfer job's latest operation.
""",
'EXAMPLES':
"""\
To monitor a job, run:
$ {command} JOB-NAME
If you're looking for recent error details, use the "Operation name"
returned by this command as input to the "operations describe" command:
$ {command} JOB-NAME
$ {grandparent_command} operations describe OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name',
help='The name of the job you want to monitor'
" (you'll see details for the job's latest operation).")
def Run(self, args):
operation_name = jobs_util.block_until_operation_created(args.name)
operations_util.display_monitoring_view(operation_name)

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.
"""Command to run transfer jobs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import operations_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import properties
@base.UniverseCompatible
class Run(base.Command):
"""Run a Transfer Service transfer job."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Run a Transfer Service transfer job.
""",
'EXAMPLES':
"""\
To run job 'foo', run:
$ {command} foo
"""
}
@staticmethod
def Args(parser):
parser.add_argument('name', help='The name of the job you want to run.')
parser.add_argument(
'--no-async',
action='store_true',
help=(
'Blocks other tasks in your terminal until the transfer operation'
' has completed. If not included, tasks will run asynchronously.'))
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_name = name_util.add_job_prefix(args.name)
result = client.transferJobs.Run(
messages.StoragetransferTransferJobsRunRequest(
jobName=formatted_name,
runTransferJobRequest=messages.RunTransferJobRequest(
projectId=properties.VALUES.core.project.Get())))
if args.no_async:
operations_util.block_until_done(operation_name=result.name)
return result

View File

@@ -0,0 +1,194 @@
# -*- 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.
"""Command to update transfer jobs."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import jobs_util
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import jobs_apitools_util
from googlecloudsdk.command_lib.transfer import jobs_flag_util
def _clear_fields(args, messages, job):
"""Removes fields from TransferJob based on clear flags."""
if args.clear_description:
job.description = None
if args.clear_source_creds_file:
if getattr(job.transferSpec, 'awsS3DataSource', None):
job.transferSpec.awsS3DataSource.awsAccessKey = None
job.transferSpec.awsS3DataSource.roleArn = None
if getattr(job.transferSpec, 'azureBlobStorageDataSource', None):
job.transferSpec.azureBlobStorageDataSource.azureCredentials = None
if args.clear_event_stream:
if job.replicationSpec:
# Do not clear the event stream for replication job.
raise ValueError('Cannot clear event stream for replication job.')
job.eventStream = None
if args.clear_schedule:
job.schedule = None
if args.clear_source_agent_pool:
job.transferSpec.sourceAgentPoolName = None
if args.clear_destination_agent_pool:
job.transferSpec.sinkAgentPoolName = None
if args.clear_intermediate_storage_path:
job.transferSpec.gcsIntermediateDataLocation = None
if args.clear_manifest_file:
job.transferSpec.transferManifest = None
if getattr(job.transferSpec, 'awsS3DataSource', None):
if getattr(args, 'clear_s3_cloudfront_domain', None):
job.transferSpec.awsS3DataSource.cloudfrontDomain = None
object_conditions, transfer_options = None, None
if job.transferSpec:
# It is a transfer job.
object_conditions = getattr(job.transferSpec, 'objectConditions', None)
transfer_options = getattr(job.transferSpec, 'transferOptions', None)
elif job.replicationSpec:
# Otherwise, it's a replication job.
object_conditions = getattr(job.replicationSpec, 'objectConditions', None)
transfer_options = getattr(job.replicationSpec, 'transferOptions', None)
if object_conditions:
if args.clear_include_prefixes:
object_conditions.includePrefixes = []
if args.clear_exclude_prefixes:
object_conditions.excludePrefixes = []
if args.clear_include_modified_before_absolute:
object_conditions.lastModifiedBefore = None
if args.clear_include_modified_after_absolute:
object_conditions.lastModifiedSince = None
if args.clear_include_modified_before_relative:
object_conditions.minTimeElapsedSinceLastModification = None
if args.clear_include_modified_after_relative:
object_conditions.maxTimeElapsedSinceLastModification = None
if object_conditions == messages.ObjectConditions():
if job.transferSpec:
job.transferSpec.objectConditions = None
else:
job.replicationSpec.objectConditions = None
if transfer_options:
if args.clear_delete_from:
transfer_options.deleteObjectsFromSourceAfterTransfer = None
transfer_options.deleteObjectsUniqueInSink = None
if args.clear_delete_from:
transfer_options.deleteObjectsFromSourceAfterTransfer = None
transfer_options.deleteObjectsUniqueInSink = None
if transfer_options.metadataOptions:
existing_metadata_options = transfer_options.metadataOptions
new_metadata_options = existing_metadata_options
if args.clear_preserve_metadata:
new_metadata_options = messages.MetadataOptions()
if (existing_metadata_options.storageClass != messages.MetadataOptions
.StorageClassValueValuesEnum.STORAGE_CLASS_PRESERVE):
# Maintain custom values that aren't the preserve flag.
new_metadata_options.storageClass = (
existing_metadata_options.storageClass)
if args.clear_custom_storage_class:
new_metadata_options.storageClass = None
if new_metadata_options == messages.MetadataOptions():
transfer_options.metadataOptions = None
else:
transfer_options.metadataOptions = new_metadata_options
if transfer_options == messages.TransferOptions():
if job.transferSpec:
job.transferSpec.transferOptions = None
else:
job.replicationSpec.transferOptions = None
if args.clear_notification_config:
job.notificationConfig = None
if args.clear_notification_event_types:
job.notificationConfig.eventTypes = []
if args.clear_log_config:
job.loggingConfig = None
if getattr(job.transferSpec, 'awsS3CompatibleDataSource', None):
if args.clear_source_endpoint:
job.transferSpec.awsS3CompatibleDataSource.endpoint = None
if args.clear_source_signing_region:
job.transferSpec.awsS3CompatibleDataSource.region = None
s3_compatible_metadata = getattr(job.transferSpec.awsS3CompatibleDataSource,
's3Metadata', None)
if s3_compatible_metadata:
if args.clear_source_auth_method:
s3_compatible_metadata.authMethod = None
if args.clear_source_list_api:
s3_compatible_metadata.listApi = None
if args.clear_source_network_protocol:
s3_compatible_metadata.protocol = None
if args.clear_source_request_model:
s3_compatible_metadata.requestModel = None
if s3_compatible_metadata == messages.S3CompatibleMetadata():
job.transferSpec.awsS3CompatibleDataSource.s3Metadata = None
@base.UniverseCompatible
class Update(base.Command):
"""Update a Transfer Service transfer job."""
# pylint:disable=line-too-long
detailed_help = {
'DESCRIPTION':
"""\
Update a Transfer Service transfer job.
""",
'EXAMPLES':
"""\
To disable transfer job 'foo', run:
$ {command} foo --status=disabled
To remove the schedule for transfer job 'foo' so that it will only run
when you manually start it, run:
$ {command} foo --clear-schedule
To clear the values from the `include=prefixes` object condition in
transfer job 'foo', run:
$ {command} foo --clear-include-prefixes
"""
}
@classmethod
def Args(cls, parser):
jobs_flag_util.setup_parser(
parser, is_update=True, release_track=cls.ReleaseTrack()
)
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
existing_job = jobs_util.api_get(args.name)
_clear_fields(args, messages, existing_job)
return client.transferJobs.Patch(
jobs_apitools_util.generate_transfer_job_message(
args, messages, existing_job=existing_job))

View File

@@ -0,0 +1,25 @@
# -*- 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.
"""Transfer operations commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.calliope import base
class Operations(base.Group):
"""Manage transfer operations."""

View File

@@ -0,0 +1,57 @@
# -*- 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.
"""Command to cancel a transfer operation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import log
@base.UniverseCompatible
class Cancel(base.Command):
"""Cancel a transfer operation."""
detailed_help = {
'DESCRIPTION':
"""\
Cancel a transfer operation.
""",
'EXAMPLES':
"""\
To cancel an operation, run:
$ {command} OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name', help='The name of the transfer operation you want to cancel.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_name = name_util.add_operation_prefix(args.name)
client.transferOperations.Cancel(
messages.StoragetransferTransferOperationsCancelRequest(
name=formatted_name))
log.status.Print('Sent cancel request for {}'.format(formatted_name))

View File

@@ -0,0 +1,53 @@
# -*- 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.
"""Command to get details on a transfer operation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import operations_util
from googlecloudsdk.calliope import base
from googlecloudsdk.core.resource import resource_printer
@base.UniverseCompatible
class Describe(base.Command):
"""Get configuration and latest transfer operation details."""
detailed_help = {
'DESCRIPTION':
"""\
Get details about a specific transfer operation.
""",
'EXAMPLES':
"""\
To describe an operation, run:
$ {command} OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name', help='The name of the operation you want to describe.')
def Display(self, args, resources):
del args # Unsued.
resource_printer.Print(resources, 'json')
def Run(self, args):
return operations_util.api_get(args.name)

View File

@@ -0,0 +1,156 @@
# -*- 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.
"""Command to list transfer operations."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import json
from apitools.base.py import list_pager
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import list_util
from googlecloudsdk.command_lib.transfer import name_util
from googlecloudsdk.core import properties
from googlecloudsdk.core.resource import resource_printer
@base.UniverseCompatible
class List(base.Command):
"""List Transfer Service transfer operations."""
detailed_help = {
'DESCRIPTION':
"""\
List Transfer Service transfer operations to view their progress details
at a glance.
""",
'EXAMPLES':
"""\
To list all transfer operations in your current project, run:
$ {command}
To list all failed operations in your project, run:
$ {command} --operation-statuses=failed
To list operations 'foo' and 'bar', run:
$ {command} --operation-names=foo,bar
To list all operations in your current project as JSON, which provides
all fields and formatting available in the API, run:
$ {command} --format=json
""",
}
@staticmethod
def Args(parser):
parser.SetSortArgs(False)
list_util.add_common_list_flags(parser)
parser.add_argument(
'--job-names',
type=arg_parsers.ArgList(),
metavar='JOB_NAMES',
help='The names of the jobs whose operations you want to list. Separate'
' multiple job names with commas (e.g., --job-names=foo,bar). If not'
' specified, operations for all jobs are listed.')
parser.add_argument(
'--operation-names',
type=arg_parsers.ArgList(),
metavar='OPERATION_NAMES',
help='The names of operations you want to list. Separate multiple'
' operation names with commas (e.g., --operation-names-name=foo,bar).'
' If not specified, all operations are listed.')
parser.add_argument(
'--operation-statuses',
type=arg_parsers.ArgList(),
metavar='OPERATION_STATUSES',
help='List only transfer operations with the statuses you'
" specify. Options include 'in_progress', 'paused', 'success',"
"'failed', 'aborted'. Separate multiple statuses with commas (e.g.,"
' --operation-statuses=failed,aborted).')
parser.add_argument(
'--expand-table',
action='store_true',
help='Include additional table columns (operation name, start time,'
' status, data copied, status, has errors, job name) in command'
' output. Tip: increase the size of your terminal before running the'
' command.')
def Display(self, args, resources):
"""API response display logic."""
if args.expand_table:
# Removes unwanted "transferJobs/" and "transferOperations/" prefixes.
# Extract start date from start time string.
# Remove "s" from repeatInterval seconds and make ISO duration string.
format_string = """table(
name.slice(19:).join(sep=''),
metadata.startTime.date('%Y-%m-%d'):label='START DATE',
metadata.counters.bytesCopiedToSink.size():label='DATA COPIED',
metadata.status,
metadata.errorBreakdowns.yesno(yes='Yes'):label='HAS ERRORS',
metadata.transferJobName.slice(13:).join(
sep=''):label='TRANSFER JOB NAME')
"""
else:
format_string = """table(
name.slice(19:).join(sep=''),
metadata.startTime.date('%Y-%m-%d'):label='START DATE',
metadata.status)
"""
resource_printer.Print(resources, args.format or format_string)
def Run(self, args):
"""Command execution logic."""
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
if args.job_names:
formatted_job_names = name_util.add_job_prefix(args.job_names)
else:
formatted_job_names = None
if args.operation_names:
formatted_operation_names = name_util.add_operation_prefix(
args.operation_names)
else:
formatted_operation_names = None
operation_statuses = args.operation_statuses or None
filter_dictionary = {
'jobNames': formatted_job_names,
'operationNames': formatted_operation_names,
'transferStatuses': operation_statuses,
'projectId': properties.VALUES.core.project.Get(),
}
filter_string = json.dumps(filter_dictionary)
resources_iterator = list_pager.YieldFromList(
client.transferOperations,
messages.StoragetransferTransferOperationsListRequest(
filter=filter_string, name='transferOperations'),
batch_size=args.page_size,
batch_size_attribute='pageSize',
field='operations',
limit=args.limit,
)
list_util.print_transfer_resources_iterator(resources_iterator,
self.Display, args)

View File

@@ -0,0 +1,53 @@
# -*- 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.
"""Command to monitor a currently running transfer operation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.transfer import operations_util
from googlecloudsdk.calliope import base
@base.UniverseCompatible
class Monitor(base.Command):
"""Track progress in real time for a transfer operation."""
detailed_help = {
'DESCRIPTION':
"""\
Track progress in real time for a transfer operation.
""",
'EXAMPLES':
"""\
To monitor an operation, run:
$ {command} OPERATION-NAME
If you're looking for specific error details, use the
"operations describe" command:
$ {parent_command} describe OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name', help='The name of the operation you want to monitor.')
def Run(self, args):
operations_util.display_monitoring_view(args.name)

View File

@@ -0,0 +1,56 @@
# -*- 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.
"""Command to pause a currently running transfer operation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
@base.UniverseCompatible
class Pause(base.Command):
"""Pause a currently running transfer operation."""
detailed_help = {
'DESCRIPTION':
"""\
Pause a currently running transfer operation.
""",
'EXAMPLES':
"""\
To pause an operation, run:
$ {command} OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name',
help='The name of the paused transfer operation you want to cancel.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_name = name_util.add_operation_prefix(args.name)
client.transferOperations.Pause(
messages.StoragetransferTransferOperationsPauseRequest(
name=formatted_name))

View File

@@ -0,0 +1,56 @@
# -*- 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.
"""Command to resume a currently running transfer operation."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.util import apis
from googlecloudsdk.calliope import base
from googlecloudsdk.command_lib.transfer import name_util
@base.UniverseCompatible
class Resume(base.Command):
"""Resume a currently paused transfer operation."""
detailed_help = {
'DESCRIPTION':
"""\
Resume a currently paused transfer operation.
""",
'EXAMPLES':
"""\
To resume an operation, run:
$ {command} OPERATION-NAME
""",
}
@staticmethod
def Args(parser):
parser.add_argument(
'name',
help='The name of the paused transfer operation you want to resume.')
def Run(self, args):
client = apis.GetClientInstance('transfer', 'v1')
messages = apis.GetMessagesModule('transfer', 'v1')
formatted_name = name_util.add_operation_prefix(args.name)
client.transferOperations.Resume(
messages.StoragetransferTransferOperationsResumeRequest(
name=formatted_name))