Unverified Commit 7c3372e5 authored by Alan Rominger's avatar Alan Rominger Committed by GitHub

Merge pull request #479 from john-westcott-iv/master

Send/Receive
parents 34b11e5f 31389c65
......@@ -67,7 +67,25 @@ Some examples:
# Filter job list for currently running jobs
$ tower-cli job list --status=running
# Export all objects
$ tower-cli receive --all
# Export all credentials
$ tower-cli receive --credential all
# Export a credential named "My Credential"
$ tower-cli receive --credential "My Credential"
# Import from a JSON file named assets.json
$ tower-cli send assets.json
# Import anything except an organization defined in a JSON file named assets.json
$ tower-cli send --prevent organization assets.json
# Copy all assets from one instance to another
$ tower-cli receive --tower-host tower1.example.com --all | tower-cli send --tower-host tower2.example.com
When in doubt, help is available!
......@@ -94,6 +112,7 @@ In specific, ``tower-cli --help`` lists all available resources in the current v
config Read or write tower-cli configuration.
credential Manage credentials within Ansible Tower.
credential_type Manage credential types within Ansible Tower.
empty Empties assets from Tower.
group Manage groups belonging to an inventory.
host Manage hosts belonging to a group within an...
instance Check instances within Ansible Tower.
......@@ -108,8 +127,10 @@ In specific, ``tower-cli --help`` lists all available resources in the current v
notification_template Manage notification templates within Ansible...
organization Manage organizations within Ansible Tower.
project Manage projects within Ansible Tower.
receive Export assets from Tower.
role Add and remove users/teams from roles.
schedule Manage schedules within Ansible Tower.
send Import assets into Tower.
setting Manage settings within Ansible Tower.
team Manage teams within Ansible Tower.
user Manage users within Ansible Tower.
......
......@@ -225,6 +225,8 @@ class TowerAuthTokenTests(unittest.TestCase):
with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
with client.test_mode as t:
t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
t.register('/authtoken/', json.dumps({'token': 'foobar', 'expires': expires}), status_code=200,
method='POST')
self.auth(self.req)
self.assertEqual(self.req.headers['Authorization'], 'Token foobar')
......
from tower_cli.api import client
from tower_cli.cli.transfer import common
from tower_cli.utils.data_structures import OrderedDict
from tests.compat import unittest
class TransferCommonTests(unittest.TestCase):
"""A set of tests to establish that the Common class works
in the way we expect.
"""
def test_get_api_options(self):
# Assert that an entry without a POST section returns None
with client.test_mode as t:
t.register_json('/inventories/', {'actions': {'PUT': {'FIRST': {'type': 'integer'}}}}, method='OPTIONS')
inventory_options = common.get_api_options('inventory')
self.assertIsNone(inventory_options)
# Assert that an entry with a POST section returns the post section
with client.test_mode as t:
t.register_json('/job_templates/', {'actions': {'POST': {'test': 'string'}}}, method='OPTIONS')
job_template_options = common.get_api_options('job_template')
self.assertEqual(job_template_options, {'test': 'string'}, "Failed to extract POST options")
# Test a cached API options
job_template_options = common.get_api_options('job_template')
self.assertEqual(job_template_options, {'test': 'string'}, "Failed to extract POST options")
def test_map_node_to_post_options(self):
source_node = {
"name": "My Name",
"created_on": "now",
"some_value": "the default value",
"some_other_value": "Not the default",
}
target_node = {}
post_options = {
"name": {"required": True},
"some_value": {"required": False, "default": "the default value"},
# Note, this function does not care if a required value is missing
"some_missing_required_value": {"required": True},
"some_other_value": {"default": "The default"},
}
# First test that nothing happens if post_options is None
common.map_node_to_post_options(None, source_node, target_node)
self.assertEqual(target_node, {}, "Post options of None modified the target node")
common.map_node_to_post_options(post_options, source_node, target_node)
self.assertEqual(target_node, {"name": "My Name", "some_other_value": "Not the default"}, "Failed node mapping")
def test_get_identity(self):
identity = common.get_identity('schedules')
self.assertEqual(identity, 'name', 'Schedules did not get proper identity {}'.format(identity))
identity = common.get_identity('inventory')
self.assertEqual(identity, 'name', 'Inventory did not get proper identity {}'.format(identity))
identity = common.get_identity('user')
self.assertEqual(identity, 'username', 'User did not get proper identity {}'.format(identity))
def test_remove_encrypted_value(self):
test_hash = {
'first': 'ok',
'second': common.ENCRYPTED_VALUE,
'sub': OrderedDict({
'first': common.ENCRYPTED_VALUE,
'second': 'ok',
}),
}
result_hash = {
'first': 'ok',
'second': '',
'sub': {
'first': '',
'second': 'ok',
},
}
common.remove_encrypted_values(test_hash)
self.assertEqual(test_hash, result_hash, "Failed to remove encrypted values from hash")
def test_remove_local_path_from_scm_project(self):
asset = {
'scm_type': 'Manual',
'local_path': 'somewhere',
}
result_asset = {
'scm_type': 'Manual',
'local_path': 'somewhere',
}
# Test a no change for either Manual or '' scm_type
common.remove_local_path_from_scm_project(asset)
self.assertEqual(asset, result_asset, "Incorrectly removed the local path for manual project")
asset['scm_type'] = ''
result_asset['scm_type'] = ''
common.remove_local_path_from_scm_project(asset)
self.assertEqual(asset, result_asset, "Incorrectly removed the local path for blank project")
# Test a change for a git scm_type
asset['scm_type'] = "git"
result_asset['scm_type'] = 'git'
del result_asset['local_path']
common.remove_local_path_from_scm_project(asset)
self.assertEqual(asset, result_asset, "Failed to remove the local path for git project")
from tower_cli.cli.transfer.logging_command import LoggingCommand
from six.moves import StringIO
import sys
import click
from tests.compat import unittest, mock
class LoggingCommandTests(unittest.TestCase):
"""A set of tests to establish that the LoggingCommand metaclass works
in the way we expect.
"""
def setUp(self):
self.held, sys.stdout = sys.stdout, StringIO()
def clear_string_buffer(self):
sys.stdout.seek(0)
sys.stdout.truncate(0)
def test_print_recap(self):
with mock.patch.object(click, 'get_terminal_size') as mock_method:
mock_method.return_value = [80, 128]
my_logging_command = LoggingCommand()
# Test default play recap (no starting new line)
my_logging_command.print_recap()
recap_msg = "PLAY RECAP *********************************************************************\n" +\
" ok=0 changed=0 warnings=0 failed=0\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "First recap message failed")
# Test second play recap (has starting new line)
self.clear_string_buffer()
my_logging_command.print_recap()
recap_msg = "\nPLAY RECAP *********************************************************************\n" +\
" ok=0 changed=0 warnings=0 failed=0\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "Second recap message failed")
# Test play recap with 1 ok
self.clear_string_buffer()
my_logging_command.ok_messages = 1
my_logging_command.print_recap()
recap_msg = "\nPLAY RECAP *********************************************************************\n" +\
" ok=1 changed=0 warnings=0 failed=0\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "OK recap message failed")
# Test play recap with 1 changed
self.clear_string_buffer()
my_logging_command.changed_messages = 1
my_logging_command.print_recap()
recap_msg = "\nPLAY RECAP *********************************************************************\n" +\
" ok=1 changed=1 warnings=0 failed=0\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "Changed recap message failed")
# Test play recap with 1 warnings
self.clear_string_buffer()
my_logging_command.warn_messages = 1
my_logging_command.print_recap()
recap_msg = "\nPLAY RECAP *********************************************************************\n" +\
" ok=1 changed=1 warnings=1 failed=0\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "Warn recap message failed")
# Test play recap with 1 failed
self.clear_string_buffer()
my_logging_command.error_messages = 1
my_logging_command.print_recap()
recap_msg = "\nPLAY RECAP *********************************************************************\n" +\
" ok=1 changed=1 warnings=1 failed=1\n\n"
self.assertEquals(sys.stdout.getvalue(), recap_msg, "Error recap message failed")
def test_my_print_no_color(self):
# Validate that with no_color specified a message comes out straight
my_logging_command = LoggingCommand()
my_logging_command.no_color = True
my_logging_command.my_print("Test Message", fg="red", bold="True", nl="False")
self.assertEquals(sys.stdout.getvalue(), "Test Message\n", "Message with no color did not come out correctly")
def test_print_into(self):
# Validate that the intro just prints a blank line
my_logging_command = LoggingCommand()
my_logging_command.print_intro()
self.assertEquals(sys.stdout.getvalue(), "\n", "Print Intro does not print a new line")
def test_get_terminal_size(self):
with mock.patch.object(click, 'get_terminal_size') as mock_method:
mock_method.return_value = [80, 128]
my_logging_command = LoggingCommand()
my_logging_command.get_rows()
self.assertEquals(my_logging_command.columns, 80, "Did not correctly get back the terminal size")
def test_print_header_row(self):
with mock.patch.object(click, 'get_terminal_size') as mock_method:
mock_method.return_value = [80, 128]
my_logging_command = LoggingCommand()
my_logging_command.print_header_row("inventory", "test inventory")
first_inv_msg = "INVENTORY [test inventory] *****************************************************\n"
self.assertEquals(sys.stdout.getvalue(), first_inv_msg, "First header row incorrect")
self.clear_string_buffer()
my_logging_command.print_header_row("inventory", "test inventory")
second_inv_msg = "\nINVENTORY [test inventory] *****************************************************\n"
self.assertEquals(sys.stdout.getvalue(), second_inv_msg, "Second header row incorrect")
def test_log_error(self):
my_logging_command = LoggingCommand()
my_logging_command.log_error("TestError")
self.assertEquals(sys.stdout.getvalue(), "TestError\n", "Error did not come out correctly")
self.assertEquals(my_logging_command.error_messages, 1)
def test_log_warn(self):
my_logging_command = LoggingCommand()
my_logging_command.log_warn("TestWarn")
self.assertEquals(sys.stdout.getvalue(), " [WARNING]: TestWarn\n", "Warn did not come out correctly")
self.assertEquals(my_logging_command.warn_messages, 1)
def test_log_ok(self):
my_logging_command = LoggingCommand()
my_logging_command.log_ok("TestOK")
self.assertEquals(sys.stdout.getvalue(), "TestOK\n", "OK did not come out correctly")
self.assertEquals(my_logging_command.ok_messages, 1)
def test_log_change(self):
my_logging_command = LoggingCommand()
my_logging_command.log_change("TestChange")
self.assertEquals(sys.stdout.getvalue(), "TestChange\n", "Change did not come out correctly")
self.assertEquals(my_logging_command.changed_messages, 1)
......@@ -54,18 +54,27 @@ class BasicTowerAuth(AuthBase):
def _get_auth_token(self):
filename = os.path.expanduser('~/.tower_cli_token.json')
token_json = None
try:
with open(filename) as f:
token_json = json.load(f)
if not isinstance(token_json, dict) or 'token' not in token_json or 'expires' not in token_json or \
dt.utcnow() > dt.strptime(token_json['expires'], TOWER_DATETIME_FMT):
if not isinstance(token_json, dict) or self.cli_client.get_prefix() not in token_json or \
'token' not in token_json[self.cli_client.get_prefix()] or \
'expires' not in token_json[self.cli_client.get_prefix()] or \
dt.utcnow() > dt.strptime(token_json[self.cli_client.get_prefix()]['expires'], TOWER_DATETIME_FMT):
raise Exception("Current token expires.")
return 'Token ' + token_json['token']
return 'Token ' + token_json[self.cli_client.get_prefix()]['token']
except Exception as e:
debug.log('Acquiring and caching auth token due to:\n%s' % str(e), fg='blue', bold=True)
token_json = self._acquire_token()
if not isinstance(token_json, dict) or 'token' not in token_json or 'expires' not in token_json:
raise exc.AuthError('Invalid Tower auth token format: %s' % json.dumps(token_json))
if not isinstance(token_json, dict):
token_json = {}
token_json[self.cli_client.get_prefix()] = self._acquire_token()
if not isinstance(token_json[self.cli_client.get_prefix()], dict) or \
'token' not in token_json[self.cli_client.get_prefix()] or \
'expires' not in token_json[self.cli_client.get_prefix()]:
raise exc.AuthError('Invalid Tower auth token format: %s' % json.dumps(
token_json[self.cli_client.get_prefix()]
))
with open(filename, 'w') as f:
json.dump(token_json, f)
try:
......@@ -75,7 +84,7 @@ class BasicTowerAuth(AuthBase):
'Unable to set permissions on {0} - {1} '.format(filename, e),
UserWarning
)
return 'Token ' + token_json['token']
return 'Token ' + token_json[self.cli_client.get_prefix()]['token']
def __call__(self, r):
if 'Authorization' in r.headers:
......@@ -188,6 +197,13 @@ class Client(Session):
"""Make a request to the Ansible Tower API, and return the
response.
"""
# If the URL has the api/vX at the front strip it off
# This is common to have if you are extracting a URL from an existing object.
# For example, any of the 'related' fields of an object will have this
import re
url = re.sub("^/?api/v[0-9]+/", "", url)
# Piece together the full URL.
use_version = not url.startswith('/o/')
url = '%s%s' % (self.get_prefix(use_version), url.lstrip('/'))
......
......@@ -29,8 +29,9 @@ from tower_cli.api import client
from tower_cli.conf import with_global_options, Parser, settings, _apply_runtime_setting
from tower_cli.utils import secho, supports_oauth
from tower_cli.constants import CUR_API_VERSION
from tower_cli.cli.transfer.common import SEND_ORDER
__all__ = ['version', 'config', 'login', 'logout']
__all__ = ['version', 'config', 'login', 'logout', 'receive', 'send', 'empty']
@click.command()
......@@ -209,6 +210,9 @@ def config(key=None, value=None, scope='user', global_=False, unset=False):
click.echo('Configuration updated successfully.')
# TODO:
# Someday it would be nice to create these for us
# Thus the import reference to transfer.common.SEND_ORDER
@click.command()
@click.argument('username', required=True)
@click.option('--password', required=True, prompt=True, hide_input=True)
......@@ -291,3 +295,107 @@ def logout():
'This version of Tower does not support OAuth2.0'
)
config.main(['oauth_token', '--unset', '--scope=user'])
@click.command()
@with_global_options
@click.option('--organization', required=False, multiple=True)
@click.option('--user', required=False, multiple=True)
@click.option('--team', required=False, multiple=True)
@click.option('--credential_type', required=False, multiple=True)
@click.option('--credential', required=False, multiple=True)
@click.option('--notification_template', required=False, multiple=True)
@click.option('--inventory_script', required=False, multiple=True)
@click.option('--inventory', required=False, multiple=True)
@click.option('--project', required=False, multiple=True)
@click.option('--job_template', required=False, multiple=True)
@click.option('--workflow', required=False, multiple=True)
@click.option('--all', is_flag=True)
def receive(organization=None, user=None, team=None, credential_type=None, credential=None,
notification_template=None, inventory_script=None, inventory=None, project=None, job_template=None,
workflow=None, all=None):
"""Export assets from Tower.
'tower receive' exports one or more assets from a Tower instance
For all of the possible assets types the TEXT can either be the assets name
(or username for the case of a user) or the keyword all. Specifying all
will export all of the assets of that type.
"""
from tower_cli.cli.transfer.receive import Receiver
receiver = Receiver()
assets_to_export = {}
for asset_type in SEND_ORDER:
assets_to_export[asset_type] = locals()[asset_type]
receiver.receive(all=all, asset_input=assets_to_export)
@click.command()
@with_global_options
@click.argument('source', required=False, nargs=-1)
@click.option('--prevent', multiple=True, required=False,
help='Prevents import of a specific asset type.\n'
'Multiple prevent options can be passed.')
@click.option('--secret_management', multiple=False, required=False, default='default',
type=click.Choice(['default', 'prompt', 'random']),
help='What to do with secrets for new items.\n'
'default - use "password", "token" or "secret" depending on the field'
'prompt - prompt for the secret to use'
'random - generate a random string for the secret'
)
@click.option('--no-color', is_flag=True,
help="Disable color output"
)
def send(source=None, prevent=None, secret_management='default', no_color=False):
"""Import assets into Tower.
'tower send' imports one or more assets into a Tower instance
The import can take either JSON or YAML.
Data can be sent on stdin (i.e. from tower-cli receive pipe) and/or from files
or directories passed as parameters.
If a directory is specified only files that end in .json, .yaml or .yml will be
imported. Other files will be ignored.
"""
from tower_cli.cli.transfer.send import Sender
sender = Sender(no_color)
sender.send(source, prevent, secret_management)
@click.command()
@with_global_options
@click.option('--organization', required=False, multiple=True)
@click.option('--user', required=False, multiple=True)
@click.option('--team', required=False, multiple=True)
@click.option('--credential_type', required=False, multiple=True)
@click.option('--credential', required=False, multiple=True)
@click.option('--notification_template', required=False, multiple=True)
@click.option('--inventory_script', required=False, multiple=True)
@click.option('--inventory', required=False, multiple=True)
@click.option('--project', required=False, multiple=True)
@click.option('--job_template', required=False, multiple=True)
@click.option('--workflow', required=False, multiple=True)
@click.option('--all', is_flag=True)
@click.option('--no-color', is_flag=True,
help="Disable color output"
)
def empty(organization=None, user=None, team=None, credential_type=None, credential=None, notification_template=None,
inventory_script=None, inventory=None, project=None, job_template=None, workflow=None,
all=None, no_color=False):
"""Empties assets from Tower.
'tower empty' removes all assets from Tower
"""
# Create an import/export object
from tower_cli.cli.transfer.cleaner import Cleaner
destroyer = Cleaner(no_color)
assets_to_export = {}
for asset_type in SEND_ORDER:
assets_to_export[asset_type] = locals()[asset_type]
destroyer.go_ham(all=all, asset_input=assets_to_export)
import click
from tower_cli.cli.transfer import common
from tower_cli.cli.transfer.logging_command import LoggingCommand
from tower_cli.exceptions import TowerCLIError
import tower_cli
class Cleaner(LoggingCommand):
def __init__(self, no_color):
self.no_color = no_color
def go_ham(self, all=False, asset_input=None):
stdout = click.get_text_stream('stdout')
stdin = click.get_text_stream('stdin')
assets_from_input = common.get_assets_from_input(all, asset_input)
stdout.write("Please confirm that you want to clean the Tower instance by typing 'YES': ")
response = stdin.readline()
if response.strip() != 'YES':
stdout.write("\nAborting request to empty the instance\n")
return
self.print_intro()
for asset_type in common.SEND_ORDER[::-1]:
if asset_type not in assets_from_input:
continue
identifier = common.get_identity(asset_type)
assets_to_remove = []
if assets_from_input[asset_type]['all']:
resources = tower_cli.get_resource(asset_type).list(all_pages=True)
if 'results' not in resources:
continue
assets_to_remove = assets_to_remove + resources['results']
else:
for name in assets_from_input[asset_type]['names']:
try:
resource = tower_cli.get_resource(asset_type).get(**{identifier: name})
assets_to_remove.append(resource)
except TowerCLIError:
self.print_header_row(asset_type, name)
self.log_ok("Asset does not exist")
for asset in assets_to_remove:
self.print_header_row(asset_type, asset[identifier])
if 'managed_by_tower' in asset and asset['managed_by_tower']:
self.log_warn("{} is managed by tower and can not be deleted".format(asset[identifier]))
continue
try:
tower_cli.get_resource(asset_type).delete(asset['id'])
self.log_change("Asset removed")
except Exception as e:
self.log_error("Unable to delete : {}".format(e))
self.print_recap()
This diff is collapsed.
from tower_cli.utils import secho
import click
class LoggingCommand:
ok_messages = 0
error_messages = 0
changed_messages = 0
warn_messages = 0
columns = None
no_color = False
def print_intro(self):
self.my_print("")
def get_rows(self):
self.columns = click.get_terminal_size()[0]
def print_header_row(self, asset_type, asset_name):
if self.columns is None:
self.get_rows()
else:
self.my_print('')
# The 4 is from 2 spaces and 2 brackets
stars = '*' * (int(self.columns) - (len(asset_type + asset_name) + 4))
self.my_print("{} [{}] {}".format(asset_type.replace("_", " ").upper(), asset_name, stars))
def print_recap(self):
if self.columns is None:
self.get_rows()
else:
self.my_print('')
# Print the recap message
recap_message = "PLAY RECAP"
stars = '*' * (int(self.columns) - (len(recap_message) + 1))
self.my_print("{} {}".format(recap_message, stars))
spaces_separating_messages = " "
# Print OK count
self.my_print(spaces_separating_messages, nl=False)
ok = "ok={}".format(self.ok_messages)
color = 'black'
if self.ok_messages > 0:
color = 'green'
self.my_print(ok, fg=color, nl=False)
# Print CHANGED count
self.my_print(spaces_separating_messages, nl=False)
changed = "changed={}".format(self.changed_messages)
color = 'black'
if self.changed_messages > 0:
color = 'yellow'
self.my_print(changed, fg=color, nl=False)
# Print WARNING count
self.my_print(spaces_separating_messages, nl=False)
warnings = "warnings={}".format(self.warn_messages)
color = 'black'
if self.warn_messages > 0:
color = 'magenta'
self.my_print(warnings, fg=color, nl=False)
# Print ERROR count
self.my_print(spaces_separating_messages, nl=False)
error = "failed={}".format(self.error_messages)
color = 'black'
if self.error_messages > 0:
color = 'red'
self.my_print(error, fg=color)
self.my_print("")
def log_warn(self, warn_message):
self.my_print(" [WARNING]: {}".format(warn_message), fg='magenta', bold=True)
self.warn_messages = self.warn_messages + 1
def log_ok(self, ok_message):
self.my_print("{}".format(ok_message), fg='green')
self.ok_messages = self.ok_messages + 1
def log_change(self, change_message):
self.my_print("{}".format(change_message), fg='yellow')
self.changed_messages = self.changed_messages + 1
def log_error(self, error_message):
self.my_print("{}".format(error_message), fg='red', bold=True)
self.error_messages = self.error_messages + 1
def my_print(self, message=None, fg='black', bold=False, nl=True):
if self.no_color:
secho(message, fg='black', bold=False, nl=nl)
else:
secho(message, fg=fg, bold=bold, nl=nl)
import tower_cli
from tower_cli.exceptions import TowerCLIError
from tower_cli.cli.transfer import common
from tower_cli.conf import settings
from tower_cli.utils import parser
import click
class Receiver:
def receive(self, all=False, asset_input=None):
exported_objects = self.export_assets(all, asset_input)
stdout = click.get_text_stream('stdout')
if settings.format == 'human' or settings.format == 'json':
import json
stdout.write(json.dumps(exported_objects, indent=2))
elif settings.format == 'yaml':
import yaml
stdout.write(parser.ordered_dump(exported_objects, Dumper=yaml.SafeDumper, default_flow_style=False))
else:
raise TowerCLIError("Format {} is unsupported".format(settings.format))
stdout.write("\n")
def export_assets(self, all, asset_input):
# Extract and consolidate all of the items we got on the command line
assets_to_export = common.get_assets_from_input(all, asset_input)
# These will be returned from this method
exported_objects = []
for asset_type in assets_to_export:
# Load the API options for this asset_type of asset
types_api_options = common.get_api_options(asset_type)
# Now we are going to extract the objects from Tower and put them into an array for processing
acquired_assets_to_export = []
identifier = common.get_identity(asset_type)
# Now we are either going to get everything or just one item and append that to the assets_to_export
if assets_to_export[asset_type]['all']:
resources = tower_cli.get_resource(asset_type).list(all_pages=True)
if 'results' not in resources:
continue
acquired_assets_to_export = acquired_assets_to_export + resources['results']
else:
for name in assets_to_export[asset_type]['names']:
try:
resource = tower_cli.get_resource(asset_type).get(**{identifier: name})
except TowerCLIError as e:
raise TowerCLIError("Unable to get {} named {} : {}".format(asset_type, name, e))
acquired_assets_to_export.append(resource)
# Next we are going to loop over the objects we got from Tower
for asset in acquired_assets_to_export:
# If this object is managed_by_tower then move on
if 'managed_by_tower' in asset and asset['managed_by_tower']:
continue
# Resolve the dependencies
common.resolve_asset_dependencies(asset, asset_type)
# Create a new object with the ASSET_TYPE_KEY and merge the options in from the object we got
exported_asset = {common.ASSET_TYPE_KEY: asset_type}
common.map_node_to_post_options(types_api_options, asset, exported_asset)