Skip to content

Commit 2a1cbff

Browse files
authored
Add version command and decouple evidence requests (google#1369)
1 parent b4cdbb0 commit 2a1cbff

File tree

6 files changed

+150
-113
lines changed

6 files changed

+150
-113
lines changed

turbinia/api/cli/turbinia_client/core/commands.py

+9-91
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
import os
1818
import logging
1919
import click
20-
import base64
21-
import mimetypes
2220
import tarfile
2321

22+
from importlib.metadata import version as importlib_version
23+
2424
from turbinia_api_lib import exceptions
2525
from turbinia_api_lib import api_client
2626
from turbinia_api_lib.api import turbinia_requests_api
@@ -274,95 +274,6 @@ def get_task(
274274
f'when calling get_task_status: {exception.body}')
275275

276276

277-
@click.pass_context
278-
def create_request(ctx: click.Context, *args: int, **kwargs: int) -> None:
279-
"""Creates and submits a new Turbinia request."""
280-
client: api_client.ApiClient = ctx.obj.api_client
281-
api_instance = turbinia_requests_api.TurbiniaRequestsApi(client)
282-
evidence_name = ctx.command.name
283-
284-
# Normalize the evidence class name from lowercase to the original name.
285-
evidence_name = ctx.obj.normalize_evidence_name(evidence_name)
286-
# Build request and request_options objects to send to the API server.
287-
request_options = list(ctx.obj.request_options.keys())
288-
request = {'evidence': {'type': evidence_name}, 'request_options': {}}
289-
290-
if 'googlecloud' in evidence_name:
291-
api_instance_config = turbinia_configuration_api.TurbiniaConfigurationApi(
292-
client)
293-
cloud_provider = api_instance_config.read_config()['CLOUD_PROVIDER']
294-
if cloud_provider != 'GCP':
295-
log.error(
296-
f'The evidence type {evidence_name} is Google Cloud only and '
297-
f'the configured provider for this Turbinia instance is '
298-
f'{cloud_provider}.')
299-
return
300-
301-
for key, value in kwargs.items():
302-
# If the value is not empty, add it to the request.
303-
if kwargs.get(key):
304-
# Check if the key is for evidence or request_options
305-
if not key in request_options:
306-
request['evidence'][key] = value
307-
elif key in ('jobs_allowlist', 'jobs_denylist'):
308-
jobs_list = value.split(',')
309-
request['request_options'][key] = jobs_list
310-
else:
311-
request['request_options'][key] = value
312-
313-
if all(key in request['request_options']
314-
for key in ('recipe_name', 'recipe_data')):
315-
log.error('You can only provide one of recipe_data or recipe_name')
316-
return
317-
318-
recipe_name = request['request_options'].get('recipe_name')
319-
if recipe_name:
320-
if not recipe_name.endswith('.yaml'):
321-
recipe_name = f'{recipe_name}.yaml'
322-
# Fallback path for the recipe would be TURBINIA_CLI_CONFIG_PATH/recipe_name
323-
# This is the same path where the client configuration is loaded from.
324-
recipe_path_fallback = os.path.expanduser(ctx.obj.config_path)
325-
recipe_path_fallback = os.path.join(recipe_path_fallback, recipe_name)
326-
327-
if os.path.isfile(recipe_name):
328-
recipe_path = recipe_name
329-
elif os.path.isfile(recipe_path_fallback):
330-
recipe_path = recipe_path_fallback
331-
else:
332-
log.error(f'Unable to load recipe {recipe_name}.')
333-
return
334-
335-
try:
336-
with open(recipe_path, 'r', encoding='utf-8') as recipe_file:
337-
# Read the file and convert to base64 encoded bytes.
338-
recipe_bytes = recipe_file.read().encode('utf-8')
339-
recipe_data = base64.b64encode(recipe_bytes)
340-
except OSError as exception:
341-
log.error(f'Error opening recipe file {recipe_path}: {exception}')
342-
return
343-
except TypeError as exception:
344-
log.error(f'Error converting recipe data to Base64: {exception}')
345-
return
346-
# We found the recipe file, so we will send it to the API server
347-
# via the recipe_data parameter. To do so, we need to pop recipe_name
348-
# from the request so that we only have recipe_data.
349-
request['request_options'].pop('recipe_name')
350-
# recipe_data should be a UTF-8 encoded string.
351-
request['request_options']['recipe_data'] = recipe_data.decode('utf-8')
352-
353-
# Send the request to the API server.
354-
try:
355-
log.info(f'Sending request: {request}')
356-
api_response = api_instance.create_request(request)
357-
log.info(f'Received response: {api_response}')
358-
except exceptions.ApiException as exception:
359-
log.error(
360-
f'Received status code {exception.status} '
361-
f'when calling create_request: {exception.body}')
362-
except (TypeError, exceptions.ApiTypeError) as exception:
363-
log.error(f'The request object is invalid. {exception}')
364-
365-
366277
@groups.evidence_group.command('summary')
367278
@click.pass_context
368279
@click.option(
@@ -514,3 +425,10 @@ def upload_evidence(
514425
formatter.EvidenceMarkdownReport({}).dict_to_markdown(
515426
report, 0, format_keys=False))
516427
click.echo(report)
428+
429+
430+
@click.command('version')
431+
def version():
432+
"""Returns the turbinia-client package distribution version."""
433+
cli_version = importlib_version('turbinia-client')
434+
click.echo(f'turbinia-client version {cli_version}')

turbinia/api/cli/turbinia_client/core/groups.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
"""Turbinia API client command-line tool."""
1616

1717
import click
18+
import logging
19+
import sys
20+
21+
from turbinia_api_lib import exceptions
22+
from turbinia_client.factory import factory
23+
24+
log = logging.getLogger('turbinia')
1825

1926

2027
@click.group('config')
@@ -42,6 +49,32 @@ def jobs_group():
4249
"""Get a list of enabled Turbinia jobs."""
4350

4451

45-
@click.group('submit')
46-
def submit_group():
47-
"""Submit new requests to the Turbinia API server."""
52+
@click.pass_context
53+
def setup_submit(ctx: click.Context):
54+
try:
55+
ctx.obj.evidence_mapping = ctx.obj.get_evidence_arguments()
56+
ctx.obj.request_options = ctx.obj.get_request_options()
57+
58+
# Build all the commands based on responses from the API server.
59+
request_commands = factory.CommandFactory.create_dynamic_objects(
60+
evidence_mapping=ctx.obj.evidence_mapping,
61+
request_options=ctx.obj.request_options)
62+
for command in request_commands:
63+
submit_group.add_command(command)
64+
except exceptions.ApiException as exception:
65+
log.error(
66+
'Error while attempting to contact the API server during setup: %s',
67+
exception)
68+
sys.exit(-1)
69+
70+
71+
@click.group('submit', chain=True, invoke_without_command=True)
72+
@click.pass_context
73+
def submit_group(ctx: click.Context):
74+
"""Submit new requests to the Turbinia API server.
75+
76+
Please run this command without any arguments to view a list
77+
of available evidence types.
78+
"""
79+
ctx.invoke(setup_submit)
80+
click.echo(submit_group.get_help(ctx))

turbinia/api/cli/turbinia_client/factory/factory.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import click
2222

2323
from turbinia_client.helpers import click_helper
24-
from turbinia_client.core.commands import create_request
2524

2625
T = TypeVar('T', bound='FactoryInterface')
2726

@@ -147,7 +146,8 @@ def create_dynamic_objects(
147146
request_options=request_options)
148147
OptionFactory.append_request_option_objects(params, request_options)
149148
cmd = CommandFactory.create_object(
150-
name=evidence_name_lower, params=params, callback=create_request)
149+
name=evidence_name_lower, params=params,
150+
callback=click_helper.create_request)
151151
command_objects.append(cmd)
152152
return command_objects
153153

turbinia/api/cli/turbinia_client/helpers/click_helper.py

+101
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,19 @@
1414
# limitations under the License.
1515
"""Turbinia API client command-line tool."""
1616

17+
import os
18+
import logging
19+
import click
20+
import base64
21+
1722
from typing import Tuple, Sequence
23+
from turbinia_api_lib.api import turbinia_configuration_api
24+
from turbinia_api_lib.api import turbinia_requests_api
25+
26+
from turbinia_api_lib import exceptions
27+
from turbinia_api_lib import api_client
28+
29+
log = logging.getLogger('turbinia')
1830

1931

2032
def generate_option_parameters(
@@ -23,3 +35,92 @@ def generate_option_parameters(
2335
name.
2436
"""
2537
return ((['--' + option_name], option_name), {'required': False, 'type': str})
38+
39+
40+
@click.pass_context
41+
def create_request(ctx: click.Context, *args: int, **kwargs: int) -> None:
42+
"""Creates and submits a new Turbinia request."""
43+
client: api_client.ApiClient = ctx.obj.api_client
44+
api_instance = turbinia_requests_api.TurbiniaRequestsApi(client)
45+
evidence_name = ctx.command.name
46+
47+
# Normalize the evidence class name from lowercase to the original name.
48+
evidence_name = ctx.obj.normalize_evidence_name(evidence_name)
49+
# Build request and request_options objects to send to the API server.
50+
request_options = list(ctx.obj.request_options.keys())
51+
request = {'evidence': {'type': evidence_name}, 'request_options': {}}
52+
53+
if 'googlecloud' in evidence_name:
54+
api_instance_config = turbinia_configuration_api.TurbiniaConfigurationApi(
55+
client)
56+
cloud_provider = api_instance_config.read_config()['CLOUD_PROVIDER']
57+
if cloud_provider != 'GCP':
58+
log.error(
59+
f'The evidence type {evidence_name} is Google Cloud only and '
60+
f'the configured provider for this Turbinia instance is '
61+
f'{cloud_provider}.')
62+
return
63+
64+
for key, value in kwargs.items():
65+
# If the value is not empty, add it to the request.
66+
if kwargs.get(key):
67+
# Check if the key is for evidence or request_options
68+
if not key in request_options:
69+
request['evidence'][key] = value
70+
elif key in ('jobs_allowlist', 'jobs_denylist'):
71+
jobs_list = value.split(',')
72+
request['request_options'][key] = jobs_list
73+
else:
74+
request['request_options'][key] = value
75+
76+
if all(key in request['request_options']
77+
for key in ('recipe_name', 'recipe_data')):
78+
log.error('You can only provide one of recipe_data or recipe_name')
79+
return
80+
81+
recipe_name = request['request_options'].get('recipe_name')
82+
if recipe_name:
83+
if not recipe_name.endswith('.yaml'):
84+
recipe_name = f'{recipe_name}.yaml'
85+
# Fallback path for the recipe would be TURBINIA_CLI_CONFIG_PATH/recipe_name
86+
# This is the same path where the client configuration is loaded from.
87+
recipe_path_fallback = os.path.expanduser(ctx.obj.config_path)
88+
recipe_path_fallback = os.path.join(recipe_path_fallback, recipe_name)
89+
90+
if os.path.isfile(recipe_name):
91+
recipe_path = recipe_name
92+
elif os.path.isfile(recipe_path_fallback):
93+
recipe_path = recipe_path_fallback
94+
else:
95+
log.error(f'Unable to load recipe {recipe_name}.')
96+
return
97+
98+
try:
99+
with open(recipe_path, 'r', encoding='utf-8') as recipe_file:
100+
# Read the file and convert to base64 encoded bytes.
101+
recipe_bytes = recipe_file.read().encode('utf-8')
102+
recipe_data = base64.b64encode(recipe_bytes)
103+
except OSError as exception:
104+
log.error(f'Error opening recipe file {recipe_path}: {exception}')
105+
return
106+
except TypeError as exception:
107+
log.error(f'Error converting recipe data to Base64: {exception}')
108+
return
109+
# We found the recipe file, so we will send it to the API server
110+
# via the recipe_data parameter. To do so, we need to pop recipe_name
111+
# from the request so that we only have recipe_data.
112+
request['request_options'].pop('recipe_name')
113+
# recipe_data should be a UTF-8 encoded string.
114+
request['request_options']['recipe_data'] = recipe_data.decode('utf-8')
115+
116+
# Send the request to the API server.
117+
try:
118+
log.info(f'Sending request: {request}')
119+
api_response = api_instance.create_request(request)
120+
log.info(f'Received response: {api_response}')
121+
except exceptions.ApiException as exception:
122+
log.error(
123+
f'Received status code {exception.status} '
124+
f'when calling create_request: {exception.body}')
125+
except (TypeError, exceptions.ApiTypeError) as exception:
126+
log.error(f'The request object is invalid. {exception}')

turbinia/api/cli/turbinia_client/turbiniacli.py

-8
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,6 @@ def setup(self) -> None:
9898
log.info(
9999
f'Using configuration instance name -> {self.config_instance:s}'
100100
f' with host {self.api_server_address:s}:{self.api_server_port:d}')
101-
try:
102-
self.evidence_mapping = self.get_evidence_arguments()
103-
self.request_options = self.get_request_options()
104-
except turbinia_api_lib.ApiException as exception:
105-
log.error(
106-
'Error while attempting to contact the API server during setup: %s',
107-
exception)
108-
sys.exit(-1)
109101

110102
@property
111103
def api_client(self):

turbinia/api/cli/turbinia_client/turbiniacli_tool.py

+2-9
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323

2424
from turbinia_client.turbiniacli import TurbiniaCli
2525
from turbinia_client.core import groups
26-
from turbinia_client.factory import factory
26+
from turbinia_client.core.commands import version
2727

2828
_LOGGER_FORMAT = '%(asctime)s %(levelname)s %(name)s - %(message)s'
2929
logging.basicConfig(format=_LOGGER_FORMAT, level=logging.INFO)
@@ -79,13 +79,6 @@ def cli(ctx: click.Context, config_instance: str, config_path: str) -> None:
7979
# Set up the tool based on the configuration file parameters.
8080
ctx.obj.setup()
8181

82-
# Build all the commands based on responses from the API server.
83-
request_commands = factory.CommandFactory.create_dynamic_objects(
84-
evidence_mapping=ctx.obj.evidence_mapping,
85-
request_options=ctx.obj.request_options)
86-
for command in request_commands:
87-
groups.submit_group.add_command(command)
88-
8982

9083
def main():
9184
"""Initialize the cli application."""
@@ -97,7 +90,7 @@ def main():
9790
cli.add_command(groups.jobs_group)
9891
cli.add_command(groups.result_group)
9992
cli.add_command(groups.status_group)
100-
93+
cli.add_command(version)
10194
try:
10295
cli.main()
10396
except (ConnectionRefusedError, urllib3_exceptions.MaxRetryError,

0 commit comments

Comments
 (0)