move code linting to a stricter pep8-esque auto-formatting tool, black

This commit is contained in:
Ryan Petrello
2021-03-19 12:44:51 -04:00
parent 9b702e46fe
commit c2ef0a6500
671 changed files with 20538 additions and 21924 deletions

View File

@@ -1,4 +1,4 @@
from awxkit.api import pages, client, resources # NOQA
from awxkit.config import config # NOQA
from awxkit import awx # NOQA
from awxkit.ws import WSClient # NOQA
from awxkit.api import pages, client, resources # NOQA
from awxkit.config import config # NOQA
from awxkit import awx # NOQA
from awxkit.ws import WSClient # NOQA

View File

@@ -1,2 +1,2 @@
from .pages import * # NOQA
from .client import * # NOQA
from .pages import * # NOQA
from .client import * # NOQA

View File

@@ -49,8 +49,7 @@ class Connection(object):
_next = kwargs.get('next')
if _next:
headers = self.session.headers.copy()
self.post('/api/login/', headers=headers,
data=dict(username=username, password=password, next=_next))
self.post('/api/login/', headers=headers, data=dict(username=username, password=password, next=_next))
self.session_id = self.session.cookies.get('sessionid')
self.uses_session_cookie = True
else:
@@ -79,8 +78,7 @@ class Connection(object):
use_endpoint = use_endpoint[1:]
url = '/'.join([self.server, use_endpoint])
kwargs = dict(verify=self.verify, params=query_parameters, json=json, data=data,
hooks=dict(response=log_elapsed))
kwargs = dict(verify=self.verify, params=query_parameters, json=json, data=data, hooks=dict(response=log_elapsed))
if headers is not None:
kwargs['headers'] = headers

View File

@@ -3,7 +3,6 @@ from awxkit.utils import random_title
class HasCopy(object):
def can_copy(self):
return self.get_related('copy').can_copy

View File

@@ -24,7 +24,7 @@ def dependency_graph(page, *provided_dependencies):
return graph
def optional_dependency_graph(page, *provided_dependencies):
def optional_dependency_graph(page, *provided_dependencies):
"""Creates a dependency graph for a page including all dependencies and optional_dependencies
Any optional provided_dependencies will be included as if they were dependencies,
without affecting the value of each keyed page.
@@ -104,8 +104,7 @@ def all_instantiated_dependencies(*potential_parents):
"""
scope_provided_dependencies = []
instantiated = set([x for x in potential_parents
if not isinstance(x, type) and not isinstance(x, tuple)])
instantiated = set([x for x in potential_parents if not isinstance(x, type) and not isinstance(x, tuple)])
for potential_parent in [x for x in instantiated if hasattr(x, '_dependency_store')]:
for dependency in potential_parent._dependency_store.values():
@@ -178,7 +177,6 @@ class DSAdapter(object):
# Hijack json.dumps and simplejson.dumps (used by requests)
# to allow HasCreate.create_payload() serialization without impacting payload.ds access
def filter_ds_from_payload(dumps):
def _filter_ds_from_payload(obj, *a, **kw):
if hasattr(obj, 'get') and isinstance(obj.get('ds'), DSAdapter):
filtered = obj.copy()
@@ -191,10 +189,12 @@ def filter_ds_from_payload(dumps):
import json # noqa
json.dumps = filter_ds_from_payload(json.dumps)
try:
import simplejson # noqa
simplejson.dumps = filter_ds_from_payload(simplejson.dumps)
except ImportError:
pass
@@ -299,8 +299,7 @@ class HasCreate(object):
# remove falsy values
provided_and_desired_dependencies = [x for x in provided_and_desired_dependencies if x]
# (HasCreate(), True) tells HasCreate._update_dependencies to link
provided_dependencies = [(x, True) for x in provided_and_desired_dependencies
if not isinstance(x, type) and not isinstance(x, tuple)]
provided_dependencies = [(x, True) for x in provided_and_desired_dependencies if not isinstance(x, type) and not isinstance(x, tuple)]
# Since dependencies are often declared at runtime, we need to use some introspection
# to determine previously created ones for proper dependency store linking.
@@ -374,12 +373,7 @@ class HasCreate(object):
to_teardown = all_instantiated_dependencies(self)
to_teardown_types = set(map(get_class_if_instance, to_teardown))
order = [
set(
[
potential for potential in (
get_class_if_instance(x) for x in group) if potential in to_teardown_types
]
)
set([potential for potential in (get_class_if_instance(x) for x in group) if potential in to_teardown_types])
for group in page_creation_order(self, *to_teardown)
]
order.reverse()

View File

@@ -3,7 +3,6 @@ import awxkit.exceptions as exc
class HasInstanceGroups(object):
def add_instance_group(self, instance_group):
with suppress(exc.NoContent):
self.related['instance_groups'].post(dict(id=instance_group.id))

View File

@@ -2,29 +2,25 @@ from awxkit.utils import suppress
import awxkit.exceptions as exc
notification_endpoints = ("notification_templates", "notification_templates_started", "notification_templates_error",
"notification_templates_success")
notification_endpoints = ("notification_templates", "notification_templates_started", "notification_templates_error", "notification_templates_success")
wfjt_notification_endpoints = notification_endpoints + ('notification_templates_approvals',)
class HasNotifications(object):
def add_notification_template(self, notification_template, endpoint="notification_templates_success"):
from awxkit.api.pages.workflow_job_templates import WorkflowJobTemplate
supported_endpoints = wfjt_notification_endpoints if isinstance(self, WorkflowJobTemplate) \
else notification_endpoints
supported_endpoints = wfjt_notification_endpoints if isinstance(self, WorkflowJobTemplate) else notification_endpoints
if endpoint not in supported_endpoints:
raise ValueError('Unsupported notification endpoint "{0}". Please use one of {1}.'
.format(endpoint, notification_endpoints))
raise ValueError('Unsupported notification endpoint "{0}". Please use one of {1}.'.format(endpoint, notification_endpoints))
with suppress(exc.NoContent):
self.related[endpoint].post(dict(id=notification_template.id))
def remove_notification_template(self, notification_template, endpoint="notification_templates_success"):
from awxkit.api.pages.workflow_job_templates import WorkflowJobTemplate
supported_endpoints = wfjt_notification_endpoints if isinstance(self, WorkflowJobTemplate) \
else notification_endpoints
supported_endpoints = wfjt_notification_endpoints if isinstance(self, WorkflowJobTemplate) else notification_endpoints
if endpoint not in supported_endpoints:
raise ValueError('Unsupported notification endpoint "{0}". Please use one of {1}.'
.format(endpoint, notification_endpoints))
raise ValueError('Unsupported notification endpoint "{0}". Please use one of {1}.'.format(endpoint, notification_endpoints))
with suppress(exc.NoContent):
self.related[endpoint].post(dict(id=notification_template.id, disassociate=notification_template.id))

View File

@@ -40,8 +40,7 @@ class HasStatus(object):
if not getattr(self, 'event_processing_finished', True):
elapsed = datetime.utcnow() - start_time
time_left = timeout - elapsed.total_seconds()
poll_until(lambda: getattr(self.get(), 'event_processing_finished', True),
interval=interval, timeout=time_left, **kwargs)
poll_until(lambda: getattr(self.get(), 'event_processing_finished', True), interval=interval, timeout=time_left, **kwargs)
return self
def wait_until_started(self, interval=1, timeout=60):
@@ -65,9 +64,7 @@ class HasStatus(object):
msg = ''
else:
msg += '\n'
msg += '{0}-{1} has status of {2}, which is not in {3}.'.format(
self.type.title(), self.id, self.status, status_list
)
msg += '{0}-{1} has status of {2}, which is not in {3}.'.format(self.type.title(), self.id, self.status, status_list)
if getattr(self, 'job_explanation', ''):
msg += '\njob_explanation: {}'.format(bytes_to_str(self.job_explanation))
if getattr(self, 'result_traceback', ''):
@@ -79,10 +76,8 @@ class HasStatus(object):
try:
data = json.loads(self.job_explanation.replace('Previous Task Failed: ', ''))
dep_output = self.connection.get(
'{0}/api/v2/{1}s/{2}/stdout/'.format(
self.endpoint.split('/api')[0], data['job_type'], data['job_id']
),
query_parameters=dict(format='txt_download')
'{0}/api/v2/{1}s/{2}/stdout/'.format(self.endpoint.split('/api')[0], data['job_type'], data['job_id']),
query_parameters=dict(format='txt_download'),
).content
msg += '\nDependency output:\n{}'.format(bytes_to_str(dep_output))
except Exception as e:

View File

@@ -3,13 +3,11 @@ from awxkit.utils import random_title
class HasSurvey(object):
def add_survey(self, spec=None, name=None, description=None, required=False, enabled=True):
payload = dict(name=name or 'Survey - {}'.format(random_title()),
description=description or random_title(10),
spec=spec or [dict(required=required,
question_name="What's the password?",
variable="secret",
type="password",
default="foo")])
payload = dict(
name=name or 'Survey - {}'.format(random_title()),
description=description or random_title(10),
spec=spec or [dict(required=required, question_name="What's the password?", variable="secret", type="password", default="foo")],
)
if enabled != self.survey_enabled:
self.patch(survey_enabled=enabled)
return self.related.survey_spec.post(payload).get()

View File

@@ -4,7 +4,6 @@ from awxkit.utils import PseudoNamespace
class HasVariables(object):
@property
def variables(self):
return PseudoNamespace(yaml.safe_load(self.json.variables))

View File

@@ -33,7 +33,7 @@ from .workflow_job_templates import * # NOQA
from .workflow_job_template_nodes import * # NOQA
from .workflow_jobs import * # NOQA
from .workflow_job_nodes import * # NOQA
from .workflow_approvals import * # NOQA
from .workflow_approvals import * # NOQA
from .settings import * # NOQA
from .instances import * # NOQA
from .instance_groups import * # NOQA

View File

@@ -8,11 +8,16 @@ class AccessList(page.PageList, users.User):
pass
page.register_page([resources.organization_access_list,
resources.user_access_list,
resources.inventory_access_list,
resources.group_access_list,
resources.credential_access_list,
resources.project_access_list,
resources.job_template_access_list,
resources.team_access_list], AccessList)
page.register_page(
[
resources.organization_access_list,
resources.user_access_list,
resources.inventory_access_list,
resources.group_access_list,
resources.credential_access_list,
resources.project_access_list,
resources.job_template_access_list,
resources.team_access_list,
],
AccessList,
)

View File

@@ -16,5 +16,4 @@ class ActivityStreams(page.PageList, ActivityStream):
pass
page.register_page([resources.activity_stream,
resources.object_activity_stream], ActivityStreams)
page.register_page([resources.activity_stream, resources.object_activity_stream], ActivityStreams)

View File

@@ -24,31 +24,40 @@ class AdHocCommand(HasCreate, UnifiedJob):
return self.walk(result.url)
def payload(self, inventory, credential, module_name='ping', **kwargs):
payload = PseudoNamespace(inventory=inventory.id,
credential=credential.id,
module_name=module_name)
payload = PseudoNamespace(inventory=inventory.id, credential=credential.id, module_name=module_name)
optional_fields = ('diff_mode', 'extra_vars', 'module_args', 'job_type', 'limit', 'forks',
'verbosity')
optional_fields = ('diff_mode', 'extra_vars', 'module_args', 'job_type', 'limit', 'forks', 'verbosity')
return update_payload(payload, optional_fields, kwargs)
def create_payload(self, module_name='ping', module_args=np, job_type=np, limit=np, verbosity=np,
inventory=Inventory, credential=Credential, **kwargs):
def create_payload(self, module_name='ping', module_args=np, job_type=np, limit=np, verbosity=np, inventory=Inventory, credential=Credential, **kwargs):
self.create_and_update_dependencies(inventory, credential)
payload = self.payload(module_name=module_name, module_args=module_args, job_type=job_type, limit=limit,
verbosity=verbosity, inventory=self.ds.inventory, credential=self.ds.credential,
**kwargs)
payload = self.payload(
module_name=module_name,
module_args=module_args,
job_type=job_type,
limit=limit,
verbosity=verbosity,
inventory=self.ds.inventory,
credential=self.ds.credential,
**kwargs
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(self, module_name='ping', module_args=np, job_type=np, limit=np, verbosity=np,
inventory=Inventory, credential=Credential, **kwargs):
def create(self, module_name='ping', module_args=np, job_type=np, limit=np, verbosity=np, inventory=Inventory, credential=Credential, **kwargs):
payload = self.create_payload(module_name=module_name, module_args=module_args,
job_type=job_type, limit=limit, verbosity=verbosity,
inventory=inventory, credential=credential, **kwargs)
payload = self.create_payload(
module_name=module_name,
module_args=module_args,
job_type=job_type,
limit=limit,
verbosity=verbosity,
inventory=inventory,
credential=credential,
**kwargs
)
return self.update_identity(AdHocCommands(self.connection).post(payload))
@@ -60,7 +69,7 @@ class AdHocCommands(page.PageList, AdHocCommand):
pass
page.register_page([resources.ad_hoc_commands,
resources.inventory_related_ad_hoc_commands,
resources.group_related_ad_hoc_commands,
resources.host_related_ad_hoc_commands], AdHocCommands)
page.register_page(
[resources.ad_hoc_commands, resources.inventory_related_ad_hoc_commands, resources.group_related_ad_hoc_commands, resources.host_related_ad_hoc_commands],
AdHocCommands,
)

View File

@@ -90,18 +90,14 @@ class ApiV2(base.Base):
return None
# Note: doing _page[key] automatically parses json blob strings, which can be a problem.
fields = {
key: _page.json[key] for key in post_fields
if key in _page.json and key not in _page.related and key != 'id'
}
fields = {key: _page.json[key] for key in post_fields if key in _page.json and key not in _page.related and key != 'id'}
for key in post_fields:
if key in _page.related:
related = _page.related[key]
else:
if post_fields[key]['type'] == 'id' and _page.json.get(key) is not None:
log.warning("Related link %r missing from %s, attempting to reconstruct endpoint.",
key, _page.endpoint)
log.warning("Related link %r missing from %s, attempting to reconstruct endpoint.", key, _page.endpoint)
resource = getattr(self, key, None)
if resource is None:
log.error("Unable to infer endpoint for %r on %s.", key, _page.endpoint)
@@ -119,8 +115,7 @@ class ApiV2(base.Base):
continue
rel_natural_key = rel_endpoint.get_natural_key(self._cache)
if rel_natural_key is None:
log.error("Unable to construct a natural key for foreign key %r of object %s.",
key, _page.endpoint)
log.error("Unable to construct a natural key for foreign key %r of object %s.", key, _page.endpoint)
return None # This foreign key has unresolvable dependencies
fields[key] = rel_natural_key
@@ -154,10 +149,7 @@ class ApiV2(base.Base):
continue
if 'results' in rel_page:
results = (
x.get_natural_key(self._cache) if by_natural_key else self._export(x, rel_post_fields)
for x in rel_page.results
)
results = (x.get_natural_key(self._cache) if by_natural_key else self._export(x, rel_post_fields) for x in rel_page.results)
related[key] = [x for x in results if x is not None]
else:
related[key] = rel_page.json
@@ -190,8 +182,7 @@ class ApiV2(base.Base):
if isinstance(value, int) or value.isdecimal():
return endpoint.get(id=int(value))
options = self._cache.get_options(endpoint)
identifier = next(field for field in options['search_fields']
if field in ('name', 'username', 'hostname'))
identifier = next(field for field in options['search_fields'] if field in ('name', 'username', 'hostname'))
return endpoint.get(**{identifier: value})
def export_assets(self, **kwargs):
@@ -214,8 +205,7 @@ class ApiV2(base.Base):
# Import methods
def _dependent_resources(self, data):
page_resource = {getattr(self, resource)._create().__item_class__: resource
for resource in self.json}
page_resource = {getattr(self, resource)._create().__item_class__: resource for resource in self.json}
data_pages = [getattr(self, resource)._create().__item_class__ for resource in EXPORTABLE_RESOURCES]
for page_cls in itertools.chain(*has_create.page_creation_order(*data_pages)):

View File

@@ -12,10 +12,12 @@ class OAuth2Application(HasCreate, base.Base):
dependencies = [Organization]
def payload(self, **kwargs):
payload = PseudoNamespace(name=kwargs.get('name') or 'OAuth2Application - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
client_type=kwargs.get('client_type', 'public'),
authorization_grant_type=kwargs.get('authorization_grant_type', 'password'))
payload = PseudoNamespace(
name=kwargs.get('name') or 'OAuth2Application - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
client_type=kwargs.get('client_type', 'public'),
authorization_grant_type=kwargs.get('authorization_grant_type', 'password'),
)
if kwargs.get('organization'):
payload.organization = kwargs['organization'].id
@@ -35,8 +37,7 @@ class OAuth2Application(HasCreate, base.Base):
return self.update_identity(OAuth2Applications(self.connection).post(payload))
page.register_page((resources.application,
(resources.applications, 'post')), OAuth2Application)
page.register_page((resources.application, (resources.applications, 'post')), OAuth2Application)
class OAuth2Applications(page.PageList, OAuth2Application):
@@ -51,8 +52,7 @@ class OAuth2AccessToken(HasCreate, base.Base):
optional_dependencies = [OAuth2Application]
def payload(self, **kwargs):
payload = PseudoNamespace(description=kwargs.get('description') or random_title(10),
scope=kwargs.get('scope', 'write'))
payload = PseudoNamespace(description=kwargs.get('description') or random_title(10), scope=kwargs.get('scope', 'write'))
if kwargs.get('oauth_2_application'):
payload.application = kwargs['oauth_2_application'].id
@@ -73,8 +73,7 @@ class OAuth2AccessToken(HasCreate, base.Base):
return self.update_identity(OAuth2AccessTokens(self.connection).post(payload))
page.register_page((resources.token,
(resources.tokens, 'post')), OAuth2AccessToken)
page.register_page((resources.token, (resources.tokens, 'post')), OAuth2AccessToken)
class OAuth2AccessTokens(page.PageList, OAuth2AccessToken):

View File

@@ -3,11 +3,7 @@ import logging
from requests.auth import HTTPBasicAuth
from awxkit.api.pages import (
Page,
get_registered_page,
exception_from_status_code
)
from awxkit.api.pages import Page, get_registered_page, exception_from_status_code
from awxkit.config import config
from awxkit.api.resources import resources
import awxkit.exceptions as exc
@@ -17,7 +13,6 @@ log = logging.getLogger(__name__)
class Base(Page):
def silent_delete(self):
"""Delete the object. If it's already deleted, ignore the error"""
try:
@@ -129,14 +124,14 @@ class Base(Page):
@property
def object_roles(self):
from awxkit.api.pages import Roles, Role
url = self.get().json.related.object_roles
for obj_role in Roles(self.connection, endpoint=url).get().json.results:
yield Role(self.connection, endpoint=obj_role.url).get()
def get_authtoken(self, username='', password=''):
default_cred = config.credentials.default
payload = dict(username=username or default_cred.username,
password=password or default_cred.password)
payload = dict(username=username or default_cred.username, password=password or default_cred.password)
auth_url = resources.authtoken
return get_registered_page(auth_url)(self.connection, endpoint=auth_url).post(payload).token
@@ -146,9 +141,7 @@ class Base(Page):
load_default_authtoken = load_authtoken
def get_oauth2_token(self, username='', password='', client_id=None,
description='AWX CLI',
client_secret=None, scope='write'):
def get_oauth2_token(self, username='', password='', client_id=None, description='AWX CLI', client_secret=None, scope='write'):
default_cred = config.credentials.default
username = username or default_cred.username
password = password or default_cred.password
@@ -157,38 +150,21 @@ class Base(Page):
HTTPBasicAuth(client_id, client_secret)(req)
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
resp = self.connection.post(
'/api/o/token/',
data={
"grant_type": "password",
"username": username,
"password": password,
"scope": scope
},
headers=req.headers
'/api/o/token/', data={"grant_type": "password", "username": username, "password": password, "scope": scope}, headers=req.headers
)
elif client_id:
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
resp = self.connection.post(
'/api/o/token/',
data={
"grant_type": "password",
"username": username,
"password": password,
"client_id": client_id,
"scope": scope
},
headers=req.headers
data={"grant_type": "password", "username": username, "password": password, "client_id": client_id, "scope": scope},
headers=req.headers,
)
else:
HTTPBasicAuth(username, password)(req)
resp = self.connection.post(
'/api/v2/users/{}/personal_tokens/'.format(username),
json={
"description": description,
"application": None,
"scope": scope
},
headers=req.headers
json={"description": description, "application": None, "scope": scope},
headers=req.headers,
)
if resp.ok:
result = resp.json()
@@ -201,9 +177,9 @@ class Base(Page):
def load_session(self, username='', password=''):
default_cred = config.credentials.default
self.connection.login(username=username or default_cred.username,
password=password or default_cred.password,
**self.connection.get_session_requirements())
self.connection.login(
username=username or default_cred.username, password=password or default_cred.password, **self.connection.get_session_requirements()
)
return self
def cleanup(self):

View File

@@ -4,22 +4,17 @@ from . import page
class Config(base.Base):
@property
def is_aws_license(self):
return self.license_info.get('is_aws', False) or \
'ami-id' in self.license_info or \
'instance-id' in self.license_info
return self.license_info.get('is_aws', False) or 'ami-id' in self.license_info or 'instance-id' in self.license_info
@property
def is_valid_license(self):
return self.license_info.get('valid_key', False) and \
'instance_count' in self.license_info
return self.license_info.get('valid_key', False) and 'instance_count' in self.license_info
@property
def is_trial_license(self):
return self.is_valid_license and \
self.license_info.get('trial', False)
return self.is_valid_license and self.license_info.get('trial', False)
@property
def is_awx_license(self):
@@ -27,8 +22,7 @@ class Config(base.Base):
@property
def is_enterprise_license(self):
return self.is_valid_license and \
self.license_info.get('license_type', None) == 'enterprise'
return self.is_valid_license and self.license_info.get('license_type', None) == 'enterprise'
@property
def features(self):
@@ -37,7 +31,6 @@ class Config(base.Base):
class ConfigAttach(page.Page):
def attach(self, **kwargs):
return self.post(json=kwargs).json

View File

@@ -16,5 +16,4 @@ class CredentialInputSources(page.PageList, CredentialInputSource):
pass
page.register_page([resources.credential_input_sources,
resources.related_input_sources], CredentialInputSources)
page.register_page([resources.credential_input_sources, resources.related_input_sources], CredentialInputSources)

View File

@@ -44,7 +44,8 @@ credential_input_fields = (
'tenant',
'username',
'vault_password',
'vault_id')
'vault_id',
)
def generate_private_key():
@@ -52,15 +53,9 @@ def generate_private_key():
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
backend=default_backend()
)
key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=default_backend())
return key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()
encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()
).decode('utf-8')
@@ -98,11 +93,10 @@ credential_type_name_to_config_kind_map = {
'source control': 'scm',
'machine': 'ssh',
'vault': 'vault',
'vmware vcenter': 'vmware'}
'vmware vcenter': 'vmware',
}
config_kind_to_credential_type_name_map = {
kind: name
for name, kind in credential_type_name_to_config_kind_map.items()}
config_kind_to_credential_type_name_map = {kind: name for name, kind in credential_type_name_to_config_kind_map.items()}
def kind_and_config_cred_from_credential_type(credential_type):
@@ -115,8 +109,7 @@ def kind_and_config_cred_from_credential_type(credential_type):
config_cred = config.credentials.network
kind = 'net'
elif credential_type.kind == 'cloud':
kind = credential_type_name_to_config_kind_map[credential_type.name.lower(
)]
kind = credential_type_name_to_config_kind_map[credential_type.name.lower()]
config_kind = kind if kind != 'azure_rm' else 'azure'
config_cred = config.credentials.cloud[config_kind]
else:
@@ -127,11 +120,8 @@ def kind_and_config_cred_from_credential_type(credential_type):
return kind, PseudoNamespace()
def get_payload_field_and_value_from_kwargs_or_config_cred(
field, kind, kwargs, config_cred):
if field in (
'project_id',
'project_name'): # Needed to prevent Project kwarg collision
def get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, kwargs, config_cred):
if field in ('project_id', 'project_name'): # Needed to prevent Project kwarg collision
config_field = 'project'
elif field == 'subscription' and 'azure' in kind:
config_field = 'subscription_id'
@@ -159,10 +149,8 @@ class CredentialType(HasCreate, base.Base):
def payload(self, kind='cloud', **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'CredentialType - {}'.format(
random_title()),
description=kwargs.get('description') or random_title(10),
kind=kind)
name=kwargs.get('name') or 'CredentialType - {}'.format(random_title()), description=kwargs.get('description') or random_title(10), kind=kind
)
fields = ('inputs', 'injectors')
update_payload(payload, fields, kwargs)
return payload
@@ -174,17 +162,13 @@ class CredentialType(HasCreate, base.Base):
def create(self, kind='cloud', **kwargs):
payload = self.create_payload(kind=kind, **kwargs)
return self.update_identity(
CredentialTypes(
self.connection).post(payload))
return self.update_identity(CredentialTypes(self.connection).post(payload))
def test(self, data):
"""Test the credential type endpoint."""
response = self.connection.post(urljoin(str(self.url), 'test/'), data)
exception = exception_from_status_code(response.status_code)
exc_str = "%s (%s) received" % (
http.responses[response.status_code], response.status_code
)
exc_str = "%s (%s) received" % (http.responses[response.status_code], response.status_code)
if exception:
raise exception(exc_str, response.json())
elif response.status_code == http.FORBIDDEN:
@@ -192,8 +176,7 @@ class CredentialType(HasCreate, base.Base):
return response
page.register_page([resources.credential_type,
(resources.credential_types, 'post')], CredentialType)
page.register_page([resources.credential_type, (resources.credential_types, 'post')], CredentialType)
class CredentialTypes(page.PageList, CredentialType):
@@ -210,27 +193,19 @@ class Credential(HasCopy, HasCreate, base.Base):
optional_dependencies = [Organization, User, Team]
NATURAL_KEY = ('organization', 'name', 'credential_type')
def payload(
self,
credential_type,
user=None,
team=None,
organization=None,
inputs=None,
**kwargs):
def payload(self, credential_type, user=None, team=None, organization=None, inputs=None, **kwargs):
if not any((user, team, organization)):
raise TypeError(
'{0.__class__.__name__} requires user, team, and/or organization instances.'.format(self))
raise TypeError('{0.__class__.__name__} requires user, team, and/or organization instances.'.format(self))
if inputs is None:
inputs = {}
payload = PseudoNamespace(
name=kwargs.get('name') or 'Credential - {}'.format(
random_title()),
name=kwargs.get('name') or 'Credential - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
credential_type=credential_type.id,
inputs=inputs)
inputs=inputs,
)
if user:
payload.user = user.id
if team:
@@ -238,38 +213,26 @@ class Credential(HasCopy, HasCreate, base.Base):
if organization:
payload.organization = organization.id
kind, config_cred = kind_and_config_cred_from_credential_type(
credential_type)
kind, config_cred = kind_and_config_cred_from_credential_type(credential_type)
for field in credential_input_fields:
field, value = get_payload_field_and_value_from_kwargs_or_config_cred(
field, kind, inputs or kwargs, config_cred)
field, value = get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, inputs or kwargs, config_cred)
if value != not_provided:
payload.inputs[field] = value
if kind == 'net':
payload.inputs.authorize = inputs.get(
'authorize', bool(inputs.get('authorize_password')))
payload.inputs.authorize = inputs.get('authorize', bool(inputs.get('authorize_password')))
if kind in ('ssh', 'net') and 'ssh_key_data' not in payload.inputs:
payload.inputs.ssh_key_data = inputs.get(
'ssh_key_data', generate_private_key())
payload.inputs.ssh_key_data = inputs.get('ssh_key_data', generate_private_key())
return payload
def create_payload(
self,
credential_type=CredentialType,
user=None,
team=None,
organization=Organization,
inputs=None,
**kwargs):
def create_payload(self, credential_type=CredentialType, user=None, team=None, organization=Organization, inputs=None, **kwargs):
if isinstance(credential_type, int):
# if an int was passed, it is assumed to be the pk id of a
# credential type
credential_type = CredentialTypes(
self.connection).get(id=credential_type).results.pop()
credential_type = CredentialTypes(self.connection).get(id=credential_type).results.pop()
if credential_type == CredentialType:
kind = kwargs.pop('kind', 'ssh')
@@ -282,57 +245,29 @@ class Credential(HasCopy, HasCreate, base.Base):
inputs = config.credentials.cloud['openstack']
else:
credential_type_name = config_kind_to_credential_type_name_map[kind]
credential_type = CredentialTypes(
self.connection).get(
managed_by_tower=True,
name__icontains=credential_type_name).results.pop()
credential_type = CredentialTypes(self.connection).get(managed_by_tower=True, name__icontains=credential_type_name).results.pop()
credential_type, organization, user, team = filter_by_class(
(credential_type, CredentialType), (organization, Organization), (user, User), (team, Team))
credential_type, organization, user, team = filter_by_class((credential_type, CredentialType), (organization, Organization), (user, User), (team, Team))
if not any((user, team, organization)):
organization = Organization
self.create_and_update_dependencies(
credential_type, organization, user, team)
self.create_and_update_dependencies(credential_type, organization, user, team)
user = self.ds.user if user else None
team = self.ds.team if team else None
organization = self.ds.organization if organization else None
payload = self.payload(
self.ds.credential_type,
user=user,
team=team,
organization=organization,
inputs=inputs,
**kwargs)
payload = self.payload(self.ds.credential_type, user=user, team=team, organization=organization, inputs=inputs, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
credential_type=CredentialType,
user=None,
team=None,
organization=None,
inputs=None,
**kwargs):
payload = self.create_payload(
credential_type=credential_type,
user=user,
team=team,
organization=organization,
inputs=inputs,
**kwargs)
return self.update_identity(
Credentials(
self.connection)).post(payload)
def create(self, credential_type=CredentialType, user=None, team=None, organization=None, inputs=None, **kwargs):
payload = self.create_payload(credential_type=credential_type, user=user, team=team, organization=organization, inputs=inputs, **kwargs)
return self.update_identity(Credentials(self.connection)).post(payload)
def test(self, data):
"""Test the credential endpoint."""
response = self.connection.post(urljoin(str(self.url), 'test/'), data)
exception = exception_from_status_code(response.status_code)
exc_str = "%s (%s) received" % (
http.responses[response.status_code], response.status_code
)
exc_str = "%s (%s) received" % (http.responses[response.status_code], response.status_code)
if exception:
raise exception(exc_str, response.json())
elif response.status_code == http.FORBIDDEN:
@@ -343,11 +278,7 @@ class Credential(HasCopy, HasCreate, base.Base):
def expected_passwords_needed_to_start(self):
"""Return a list of expected passwords needed to start a job using this credential."""
passwords = []
for field in (
'password',
'become_password',
'ssh_key_unlock',
'vault_password'):
for field in ('password', 'become_password', 'ssh_key_unlock', 'vault_password'):
if getattr(self.inputs, field, None) == 'ASK':
if field == 'password':
passwords.append('ssh_password')
@@ -356,9 +287,7 @@ class Credential(HasCopy, HasCreate, base.Base):
return passwords
page.register_page([resources.credential,
(resources.credentials, 'post'),
(resources.credential_copy, 'post')], Credential)
page.register_page([resources.credential, (resources.credentials, 'post'), (resources.credential_copy, 'post')], Credential)
class Credentials(page.PageList, Credential):
@@ -366,9 +295,7 @@ class Credentials(page.PageList, Credential):
pass
page.register_page([resources.credentials,
resources.related_credentials],
Credentials)
page.register_page([resources.credentials, resources.related_credentials], Credentials)
class CredentialCopy(base.Base):

View File

@@ -46,14 +46,13 @@ class ExecutionEnvironment(HasCreate, HasCopy, base.Base):
return payload
page.register_page([resources.execution_environment,
(resources.execution_environments, 'post'),
(resources.organization_execution_environments, 'post')], ExecutionEnvironment)
page.register_page(
[resources.execution_environment, (resources.execution_environments, 'post'), (resources.organization_execution_environments, 'post')], ExecutionEnvironment
)
class ExecutionEnvironments(page.PageList, ExecutionEnvironment):
pass
page.register_page([resources.execution_environments,
resources.organization_execution_environments], ExecutionEnvironments)
page.register_page([resources.execution_environments, resources.organization_execution_environments], ExecutionEnvironments)

View File

@@ -7,7 +7,6 @@ from . import page
class InstanceGroup(HasCreate, base.Base):
def add_instance(self, instance):
with suppress(exc.NoContent):
self.related.instances.post(dict(id=instance.id))
@@ -17,8 +16,7 @@ class InstanceGroup(HasCreate, base.Base):
self.related.instances.post(dict(id=instance.id, disassociate=True))
def payload(self, **kwargs):
payload = PseudoNamespace(name=kwargs.get('name') or
'Instance Group - {}'.format(random_title()))
payload = PseudoNamespace(name=kwargs.get('name') or 'Instance Group - {}'.format(random_title()))
fields = ('policy_instance_percentage', 'policy_instance_minimum', 'policy_instance_list', 'is_container_group')
update_payload(payload, fields, kwargs)
@@ -35,8 +33,7 @@ class InstanceGroup(HasCreate, base.Base):
return self.update_identity(InstanceGroups(self.connection).post(payload))
page.register_page([resources.instance_group,
(resources.instance_groups, 'post')], InstanceGroup)
page.register_page([resources.instance_group, (resources.instance_groups, 'post')], InstanceGroup)
class InstanceGroups(page.PageList, InstanceGroup):
@@ -44,5 +41,4 @@ class InstanceGroups(page.PageList, InstanceGroup):
pass
page.register_page([resources.instance_groups,
resources.related_instance_groups], InstanceGroups)
page.register_page([resources.instance_groups, resources.related_instance_groups], InstanceGroups)

View File

@@ -16,5 +16,4 @@ class Instances(page.PageList, Instance):
pass
page.register_page([resources.instances,
resources.related_instances], Instances)
page.register_page([resources.instances, resources.related_instances], Instances)

View File

@@ -2,23 +2,8 @@ import logging
import json
import re
from awxkit.api.pages import (
Credential,
Organization,
Project,
UnifiedJob,
UnifiedJobTemplate
)
from awxkit.utils import (
filter_by_class,
random_title,
update_payload,
suppress,
not_provided,
PseudoNamespace,
poll_until,
random_utf8
)
from awxkit.api.pages import Credential, Organization, Project, UnifiedJob, UnifiedJobTemplate
from awxkit.utils import filter_by_class, random_title, update_payload, suppress, not_provided, PseudoNamespace, poll_until, random_utf8
from awxkit.api.mixins import DSAdapter, HasCreate, HasInstanceGroups, HasNotifications, HasVariables, HasCopy
from awxkit.api.resources import resources
import awxkit.exceptions as exc
@@ -68,56 +53,31 @@ class Inventory(HasCopy, HasCreate, HasInstanceGroups, HasVariables, base.Base):
def payload(self, organization, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Inventory - {}'.format(
random_title()),
name=kwargs.get('name') or 'Inventory - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id)
organization=organization.id,
)
optional_fields = (
'host_filter',
'insights_credential',
'kind',
'variables')
optional_fields = ('host_filter', 'insights_credential', 'kind', 'variables')
update_payload(payload, optional_fields, kwargs)
if 'variables' in payload and isinstance(payload.variables, dict):
payload.variables = json.dumps(payload.variables)
if 'insights_credential' in payload and isinstance(
payload.insights_credential, Credential):
if 'insights_credential' in payload and isinstance(payload.insights_credential, Credential):
payload.insights_credential = payload.insights_credential.id
return payload
def create_payload(
self,
name='',
description='',
organization=Organization,
**kwargs):
def create_payload(self, name='', description='', organization=Organization, **kwargs):
self.create_and_update_dependencies(organization)
payload = self.payload(
name=name,
description=description,
organization=self.ds.organization,
**kwargs)
payload = self.payload(name=name, description=description, organization=self.ds.organization, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
organization=Organization,
**kwargs):
payload = self.create_payload(
name=name,
description=description,
organization=organization,
**kwargs)
return self.update_identity(
Inventories(
self.connection).post(payload))
def create(self, name='', description='', organization=Organization, **kwargs):
payload = self.create_payload(name=name, description=description, organization=organization, **kwargs)
return self.update_identity(Inventories(self.connection).post(payload))
def add_host(self, host=None):
if host is None:
@@ -135,17 +95,16 @@ class Inventory(HasCopy, HasCreate, HasInstanceGroups, HasVariables, base.Base):
self.get()
except exc.NotFound:
return True
poll_until(_wait, interval=1, timeout=60)
def update_inventory_sources(self, wait=False):
response = self.related.update_inventory_sources.post()
source_ids = [entry['inventory_source']
for entry in response if entry['status'] == 'started']
source_ids = [entry['inventory_source'] for entry in response if entry['status'] == 'started']
inv_updates = []
for source_id in source_ids:
inv_source = self.related.inventory_sources.get(
id=source_id).results.pop()
inv_source = self.related.inventory_sources.get(id=source_id).results.pop()
inv_updates.append(inv_source.related.current_job.get())
if wait:
@@ -154,9 +113,7 @@ class Inventory(HasCopy, HasCreate, HasInstanceGroups, HasVariables, base.Base):
return inv_updates
page.register_page([resources.inventory,
(resources.inventories, 'post'),
(resources.inventory_copy, 'post')], Inventory)
page.register_page([resources.inventory, (resources.inventories, 'post'), (resources.inventory_copy, 'post')], Inventory)
class Inventories(page.PageList, Inventory):
@@ -164,8 +121,7 @@ class Inventories(page.PageList, Inventory):
pass
page.register_page([resources.inventories,
resources.related_inventories], Inventories)
page.register_page([resources.inventories, resources.related_inventories], Inventories)
class InventoryScript(HasCopy, HasCreate, base.Base):
@@ -174,77 +130,48 @@ class InventoryScript(HasCopy, HasCreate, base.Base):
def payload(self, organization, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Inventory Script - {}'.format(
random_title()),
name=kwargs.get('name') or 'Inventory Script - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id,
script=kwargs.get('script') or self._generate_script())
script=kwargs.get('script') or self._generate_script(),
)
return payload
def create_payload(
self,
name='',
description='',
organization=Organization,
script='',
**kwargs):
def create_payload(self, name='', description='', organization=Organization, script='', **kwargs):
self.create_and_update_dependencies(organization)
payload = self.payload(
name=name,
description=description,
organization=self.ds.organization,
script=script,
**kwargs)
payload = self.payload(name=name, description=description, organization=self.ds.organization, script=script, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
organization=Organization,
script='',
**kwargs):
payload = self.create_payload(
name=name,
description=description,
organization=organization,
script=script,
**kwargs)
return self.update_identity(
InventoryScripts(
self.connection).post(payload))
def create(self, name='', description='', organization=Organization, script='', **kwargs):
payload = self.create_payload(name=name, description=description, organization=organization, script=script, **kwargs)
return self.update_identity(InventoryScripts(self.connection).post(payload))
def _generate_script(self):
script = '\n'.join([
'#!/usr/bin/env python',
'# -*- coding: utf-8 -*-',
'import json',
'inventory = dict()',
'inventory["{0}"] = dict()',
'inventory["{0}"]["hosts"] = list()',
'inventory["{0}"]["hosts"].append("{1}")',
'inventory["{0}"]["hosts"].append("{2}")',
'inventory["{0}"]["hosts"].append("{3}")',
'inventory["{0}"]["hosts"].append("{4}")',
'inventory["{0}"]["hosts"].append("{5}")',
'inventory["{0}"]["vars"] = dict(ansible_host="127.0.0.1", ansible_connection="local")',
'print(json.dumps(inventory))'
])
script = '\n'.join(
[
'#!/usr/bin/env python',
'# -*- coding: utf-8 -*-',
'import json',
'inventory = dict()',
'inventory["{0}"] = dict()',
'inventory["{0}"]["hosts"] = list()',
'inventory["{0}"]["hosts"].append("{1}")',
'inventory["{0}"]["hosts"].append("{2}")',
'inventory["{0}"]["hosts"].append("{3}")',
'inventory["{0}"]["hosts"].append("{4}")',
'inventory["{0}"]["hosts"].append("{5}")',
'inventory["{0}"]["vars"] = dict(ansible_host="127.0.0.1", ansible_connection="local")',
'print(json.dumps(inventory))',
]
)
group_name = re.sub(r"[\']", "", "group_{}".format(random_title(non_ascii=False)))
host_names = [
re.sub(
r"[\':]",
"",
"host_{}".format(
random_utf8())) for _ in range(5)]
host_names = [re.sub(r"[\':]", "", "host_{}".format(random_utf8())) for _ in range(5)]
return script.format(group_name, *host_names)
page.register_page([resources.inventory_script,
(resources.inventory_scripts, 'post'),
(resources.inventory_script_copy, 'post')], InventoryScript)
page.register_page([resources.inventory_script, (resources.inventory_scripts, 'post'), (resources.inventory_script_copy, 'post')], InventoryScript)
class InventoryScripts(page.PageList, InventoryScript):
@@ -272,11 +199,10 @@ class Group(HasCreate, HasVariables, base.Base):
def payload(self, inventory, credential=None, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Group{}'.format(
random_title(
non_ascii=False)),
name=kwargs.get('name') or 'Group{}'.format(random_title(non_ascii=False)),
description=kwargs.get('description') or random_title(10),
inventory=inventory.id)
inventory=inventory.id,
)
if credential:
payload.credential = credential.id
@@ -288,38 +214,19 @@ class Group(HasCreate, HasVariables, base.Base):
return payload
def create_payload(
self,
name='',
description='',
inventory=Inventory,
credential=None,
source_script=None,
**kwargs):
credential, source_script = filter_by_class(
(credential, Credential), (source_script, InventoryScript))
self.create_and_update_dependencies(
inventory, credential, source_script)
def create_payload(self, name='', description='', inventory=Inventory, credential=None, source_script=None, **kwargs):
credential, source_script = filter_by_class((credential, Credential), (source_script, InventoryScript))
self.create_and_update_dependencies(inventory, credential, source_script)
credential = self.ds.credential if credential else None
payload = self.payload(
inventory=self.ds.inventory,
credential=credential,
name=name,
description=description,
**kwargs)
payload = self.payload(inventory=self.ds.inventory, credential=credential, name=name, description=description, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(self, name='', description='', inventory=Inventory, **kwargs):
payload = self.create_payload(
name=name,
description=description,
inventory=inventory,
**kwargs)
payload = self.create_payload(name=name, description=description, inventory=inventory, **kwargs)
parent = kwargs.get('parent', None) # parent must be a Group instance
resource = parent.related.children if parent else Groups(
self.connection)
resource = parent.related.children if parent else Groups(self.connection)
return self.update_identity(resource.post(payload))
def add_host(self, host=None):
@@ -348,8 +255,7 @@ class Group(HasCreate, HasVariables, base.Base):
self.related.children.post(dict(id=group.id, disassociate=True))
page.register_page([resources.group,
(resources.groups, 'post')], Group)
page.register_page([resources.group, (resources.groups, 'post')], Group)
class Groups(page.PageList, Group):
@@ -357,12 +263,17 @@ class Groups(page.PageList, Group):
pass
page.register_page([resources.groups,
resources.host_groups,
resources.inventory_related_groups,
resources.inventory_related_root_groups,
resources.group_children,
resources.group_potential_children], Groups)
page.register_page(
[
resources.groups,
resources.host_groups,
resources.inventory_related_groups,
resources.inventory_related_root_groups,
resources.group_children,
resources.group_potential_children,
],
Groups,
)
class Host(HasCreate, HasVariables, base.Base):
@@ -372,11 +283,10 @@ class Host(HasCreate, HasVariables, base.Base):
def payload(self, inventory, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Host{}'.format(
random_title(
non_ascii=False)),
name=kwargs.get('name') or 'Host{}'.format(random_title(non_ascii=False)),
description=kwargs.get('description') or random_title(10),
inventory=inventory.id)
inventory=inventory.id,
)
optional_fields = ('enabled', 'instance_id')
@@ -385,9 +295,7 @@ class Host(HasCreate, HasVariables, base.Base):
variables = kwargs.get('variables', not_provided)
if variables is None:
variables = dict(
ansible_host='127.0.0.1',
ansible_connection='local')
variables = dict(ansible_host='127.0.0.1', ansible_connection='local')
if variables != not_provided:
if isinstance(variables, dict):
@@ -396,42 +304,18 @@ class Host(HasCreate, HasVariables, base.Base):
return payload
def create_payload(
self,
name='',
description='',
variables=None,
inventory=Inventory,
**kwargs):
self.create_and_update_dependencies(
*filter_by_class((inventory, Inventory)))
payload = self.payload(
inventory=self.ds.inventory,
name=name,
description=description,
variables=variables,
**kwargs)
def create_payload(self, name='', description='', variables=None, inventory=Inventory, **kwargs):
self.create_and_update_dependencies(*filter_by_class((inventory, Inventory)))
payload = self.payload(inventory=self.ds.inventory, name=name, description=description, variables=variables, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
variables=None,
inventory=Inventory,
**kwargs):
payload = self.create_payload(
name=name,
description=description,
variables=variables,
inventory=inventory,
**kwargs)
def create(self, name='', description='', variables=None, inventory=Inventory, **kwargs):
payload = self.create_payload(name=name, description=description, variables=variables, inventory=inventory, **kwargs)
return self.update_identity(Hosts(self.connection).post(payload))
page.register_page([resources.host,
(resources.hosts, 'post')], Host)
page.register_page([resources.host, (resources.hosts, 'post')], Host)
class Hosts(page.PageList, Host):
@@ -439,10 +323,7 @@ class Hosts(page.PageList, Host):
pass
page.register_page([resources.hosts,
resources.group_related_hosts,
resources.inventory_related_hosts,
resources.inventory_sources_related_hosts], Hosts)
page.register_page([resources.hosts, resources.group_related_hosts, resources.inventory_related_hosts, resources.inventory_sources_related_hosts], Hosts)
class FactVersion(base.Base):
@@ -454,7 +335,6 @@ page.register_page(resources.host_related_fact_version, FactVersion)
class FactVersions(page.PageList, FactVersion):
@property
def count(self):
return len(self.results)
@@ -478,20 +358,13 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
optional_dependencies = [Credential, InventoryScript, Project]
NATURAL_KEY = ('organization', 'name', 'inventory')
def payload(
self,
inventory,
source='custom',
credential=None,
source_script=None,
project=None,
**kwargs):
def payload(self, inventory, source='custom', credential=None, source_script=None, project=None, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'InventorySource - {}'.format(
random_title()),
name=kwargs.get('name') or 'InventorySource - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
inventory=inventory.id,
source=source)
source=source,
)
if credential:
payload.credential = credential.id
@@ -509,22 +382,16 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
'update_cache_timeout',
'update_on_launch',
'update_on_project_update',
'verbosity')
'verbosity',
)
update_payload(payload, optional_fields, kwargs)
return payload
def create_payload(
self,
name='',
description='',
source='custom',
inventory=Inventory,
credential=None,
source_script=InventoryScript,
project=None,
**kwargs):
self, name='', description='', source='custom', inventory=Inventory, credential=None, source_script=InventoryScript, project=None, **kwargs
):
if source != 'custom' and source_script == InventoryScript:
source_script = None
if source == 'scm':
@@ -532,12 +399,10 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
if project is None:
project = Project
inventory, credential, source_script, project = filter_by_class((inventory, Inventory),
(credential, Credential),
(source_script, InventoryScript),
(project, Project))
self.create_and_update_dependencies(
inventory, credential, source_script, project)
inventory, credential, source_script, project = filter_by_class(
(inventory, Inventory), (credential, Credential), (source_script, InventoryScript), (project, Project)
)
self.create_and_update_dependencies(inventory, credential, source_script, project)
if credential:
credential = self.ds.credential
@@ -554,20 +419,12 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
project=project,
name=name,
description=description,
**kwargs)
**kwargs
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
source='custom',
inventory=Inventory,
credential=None,
source_script=InventoryScript,
project=None,
**kwargs):
def create(self, name='', description='', source='custom', inventory=Inventory, credential=None, source_script=InventoryScript, project=None, **kwargs):
payload = self.create_payload(
name=name,
description=description,
@@ -576,10 +433,9 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
credential=credential,
source_script=source_script,
project=project,
**kwargs)
return self.update_identity(
InventorySources(
self.connection).post(payload))
**kwargs
)
return self.update_identity(InventorySources(self.connection).post(payload))
def update(self):
"""Update the inventory_source using related->update endpoint"""
@@ -587,45 +443,37 @@ class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
update_pg = self.get_related('update')
# assert can_update == True
assert update_pg.can_update, \
"The specified inventory_source (id:%s) is not able to update (can_update:%s)" % \
(self.id, update_pg.can_update)
assert update_pg.can_update, "The specified inventory_source (id:%s) is not able to update (can_update:%s)" % (self.id, update_pg.can_update)
# start the inventory_update
result = update_pg.post()
# assert JSON response
assert 'inventory_update' in result.json, \
"Unexpected JSON response when starting an inventory_update.\n%s" % \
json.dumps(result.json, indent=2)
assert 'inventory_update' in result.json, "Unexpected JSON response when starting an inventory_update.\n%s" % json.dumps(result.json, indent=2)
# locate and return the inventory_update
jobs_pg = self.related.inventory_updates.get(
id=result.json['inventory_update'])
assert jobs_pg.count == 1, \
"An inventory_update started (id:%s) but job not found in response at %s/inventory_updates/" % \
(result.json['inventory_update'], self.url)
jobs_pg = self.related.inventory_updates.get(id=result.json['inventory_update'])
assert jobs_pg.count == 1, "An inventory_update started (id:%s) but job not found in response at %s/inventory_updates/" % (
result.json['inventory_update'],
self.url,
)
return jobs_pg.results[0]
@property
def is_successful(self):
"""An inventory_source is considered successful when source != "" and super().is_successful ."""
return self.source != "" and super(
InventorySource, self).is_successful
return self.source != "" and super(InventorySource, self).is_successful
def add_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, associate=True))
self.related.credentials.post(dict(id=credential.id, associate=True))
def remove_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, disassociate=True))
self.related.credentials.post(dict(id=credential.id, disassociate=True))
page.register_page([resources.inventory_source,
(resources.inventory_sources, 'post')], InventorySource)
page.register_page([resources.inventory_source, (resources.inventory_sources, 'post')], InventorySource)
class InventorySources(page.PageList, InventorySource):
@@ -633,9 +481,7 @@ class InventorySources(page.PageList, InventorySource):
pass
page.register_page([resources.inventory_sources,
resources.related_inventory_sources],
InventorySources)
page.register_page([resources.inventory_sources, resources.related_inventory_sources], InventorySources)
class InventorySourceGroups(page.PageList, Group):
@@ -643,9 +489,7 @@ class InventorySourceGroups(page.PageList, Group):
pass
page.register_page(
resources.inventory_sources_related_groups,
InventorySourceGroups)
page.register_page(resources.inventory_sources_related_groups, InventorySourceGroups)
class InventorySourceUpdate(base.Base):
@@ -653,9 +497,7 @@ class InventorySourceUpdate(base.Base):
pass
page.register_page([resources.inventory_sources_related_update,
resources.inventory_related_update_inventory_sources],
InventorySourceUpdate)
page.register_page([resources.inventory_sources_related_update, resources.inventory_related_update_inventory_sources], InventorySourceUpdate)
class InventoryUpdate(UnifiedJob):
@@ -671,10 +513,7 @@ class InventoryUpdates(page.PageList, InventoryUpdate):
pass
page.register_page([resources.inventory_updates,
resources.inventory_source_updates,
resources.project_update_scm_inventory_updates],
InventoryUpdates)
page.register_page([resources.inventory_updates, resources.inventory_source_updates, resources.project_update_scm_inventory_updates], InventoryUpdates)
class InventoryUpdateCancel(base.Base):

View File

@@ -1,13 +1,6 @@
import json
from awxkit.utils import (
filter_by_class,
not_provided,
random_title,
suppress,
update_payload,
set_payload_foreign_key_args,
PseudoNamespace)
from awxkit.utils import filter_by_class, not_provided, random_title, suppress, update_payload, set_payload_foreign_key_args, PseudoNamespace
from awxkit.api.pages import Credential, Inventory, Project, UnifiedJobTemplate
from awxkit.api.mixins import HasCreate, HasInstanceGroups, HasNotifications, HasSurvey, HasCopy, DSAdapter
from awxkit.api.resources import resources
@@ -16,13 +9,7 @@ from . import base
from . import page
class JobTemplate(
HasCopy,
HasCreate,
HasInstanceGroups,
HasNotifications,
HasSurvey,
UnifiedJobTemplate):
class JobTemplate(HasCopy, HasCreate, HasInstanceGroups, HasNotifications, HasSurvey, UnifiedJobTemplate):
optional_dependencies = [Inventory, Credential, Project]
NATURAL_KEY = ('organization', 'name')
@@ -38,16 +25,13 @@ class JobTemplate(
# return job
if result.json['type'] == 'job':
jobs_pg = self.get_related('jobs', id=result.json['job'])
assert jobs_pg.count == 1, \
"job_template launched (id:%s) but job not found in response at %s/jobs/" % \
(result.json['job'], self.url)
assert jobs_pg.count == 1, "job_template launched (id:%s) but job not found in response at %s/jobs/" % (result.json['job'], self.url)
return jobs_pg.results[0]
elif result.json['type'] == 'workflow_job':
slice_workflow_jobs = self.get_related(
'slice_workflow_jobs', id=result.json['id'])
assert slice_workflow_jobs.count == 1, (
"job_template launched sliced job (id:%s) but not found in related %s/slice_workflow_jobs/" %
(result.json['id'], self.url)
slice_workflow_jobs = self.get_related('slice_workflow_jobs', id=result.json['id'])
assert slice_workflow_jobs.count == 1, "job_template launched sliced job (id:%s) but not found in related %s/slice_workflow_jobs/" % (
result.json['id'],
self.url,
)
return slice_workflow_jobs.results[0]
else:
@@ -56,10 +40,7 @@ class JobTemplate(
def payload(self, job_type='run', playbook='ping.yml', **kwargs):
name = kwargs.get('name') or 'JobTemplate - {}'.format(random_title())
description = kwargs.get('description') or random_title(10)
payload = PseudoNamespace(
name=name,
description=description,
job_type=job_type)
payload = PseudoNamespace(name=name, description=description, job_type=job_type)
optional_fields = (
'ask_scm_branch_on_launch',
@@ -90,7 +71,8 @@ class JobTemplate(
'job_slice_count',
'webhook_service',
'webhook_credential',
'scm_branch')
'scm_branch',
)
update_payload(payload, optional_fields, kwargs)
@@ -113,94 +95,53 @@ class JobTemplate(
with suppress(exc.NoContent):
self.related.labels.post(label)
def create_payload(
self,
name='',
description='',
job_type='run',
playbook='ping.yml',
credential=Credential,
inventory=Inventory,
project=None,
**kwargs):
def create_payload(self, name='', description='', job_type='run', playbook='ping.yml', credential=Credential, inventory=Inventory, project=None, **kwargs):
if not project:
project = Project
if not inventory and not kwargs.get('ask_inventory_on_launch', False):
inventory = Inventory
self.create_and_update_dependencies(
*
filter_by_class(
(credential,
Credential),
(inventory,
Inventory),
(project,
Project)))
self.create_and_update_dependencies(*filter_by_class((credential, Credential), (inventory, Inventory), (project, Project)))
project = self.ds.project if project else None
inventory = self.ds.inventory if inventory else None
credential = self.ds.credential if credential else None
payload = self.payload(
name=name,
description=description,
job_type=job_type,
playbook=playbook,
credential=credential,
inventory=inventory,
project=project,
**kwargs)
name=name, description=description, job_type=job_type, playbook=playbook, credential=credential, inventory=inventory, project=project, **kwargs
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload, credential
def create(
self,
name='',
description='',
job_type='run',
playbook='ping.yml',
credential=Credential,
inventory=Inventory,
project=None,
**kwargs):
payload, credential = self.create_payload(name=name, description=description, job_type=job_type,
playbook=playbook, credential=credential, inventory=inventory,
project=project, **kwargs)
ret = self.update_identity(
JobTemplates(
self.connection).post(payload))
def create(self, name='', description='', job_type='run', playbook='ping.yml', credential=Credential, inventory=Inventory, project=None, **kwargs):
payload, credential = self.create_payload(
name=name, description=description, job_type=job_type, playbook=playbook, credential=credential, inventory=inventory, project=project, **kwargs
)
ret = self.update_identity(JobTemplates(self.connection).post(payload))
if credential:
with suppress(exc.NoContent):
self.related.credentials.post(dict(id=credential.id))
if 'vault_credential' in kwargs:
with suppress(exc.NoContent):
if not isinstance(kwargs['vault_credential'], int):
raise ValueError(
"Expected 'vault_credential' value to be an integer, the id of the desired vault credential")
self.related.credentials.post(
dict(id=kwargs['vault_credential']))
raise ValueError("Expected 'vault_credential' value to be an integer, the id of the desired vault credential")
self.related.credentials.post(dict(id=kwargs['vault_credential']))
return ret
def add_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, associate=True))
self.related.credentials.post(dict(id=credential.id, associate=True))
def remove_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, disassociate=True))
self.related.credentials.post(dict(id=credential.id, disassociate=True))
def remove_all_credentials(self):
for cred in self.related.credentials.get().results:
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=cred.id, disassociate=True))
self.related.credentials.post(dict(id=cred.id, disassociate=True))
page.register_page([resources.job_template,
(resources.job_templates, 'post'),
(resources.job_template_copy, 'post')], JobTemplate)
page.register_page([resources.job_template, (resources.job_templates, 'post'), (resources.job_template_copy, 'post')], JobTemplate)
class JobTemplates(page.PageList, JobTemplate):
@@ -208,8 +149,7 @@ class JobTemplates(page.PageList, JobTemplate):
pass
page.register_page([resources.job_templates,
resources.related_job_templates], JobTemplates)
page.register_page([resources.job_templates, resources.related_job_templates], JobTemplates)
class JobTemplateCallback(base.Base):

View File

@@ -5,7 +5,6 @@ from . import page
class Job(UnifiedJob):
def relaunch(self, payload={}):
result = self.related.relaunch.post(payload)
return self.walk(result.endpoint)
@@ -19,9 +18,7 @@ class Jobs(page.PageList, Job):
pass
page.register_page([resources.jobs,
resources.job_template_jobs,
resources.system_job_template_jobs], Jobs)
page.register_page([resources.jobs, resources.job_template_jobs, resources.system_job_template_jobs], Jobs)
class JobCancel(UnifiedJob):
@@ -37,8 +34,7 @@ class JobEvent(base.Base):
pass
page.register_page([resources.job_event,
resources.job_job_event], JobEvent)
page.register_page([resources.job_event, resources.job_job_event], JobEvent)
class JobEvents(page.PageList, JobEvent):
@@ -46,10 +42,7 @@ class JobEvents(page.PageList, JobEvent):
pass
page.register_page([resources.job_events,
resources.job_job_events,
resources.job_event_children,
resources.group_related_job_events], JobEvents)
page.register_page([resources.job_events, resources.job_job_events, resources.job_event_children, resources.group_related_job_events], JobEvents)
class JobPlay(base.Base):
@@ -97,8 +90,7 @@ class JobHostSummaries(page.PageList, JobHostSummary):
pass
page.register_page([resources.job_host_summaries,
resources.group_related_job_host_summaries], JobHostSummaries)
page.register_page([resources.job_host_summaries, resources.group_related_job_host_summaries], JobHostSummaries)
class JobRelaunch(base.Base):

View File

@@ -19,43 +19,24 @@ class Label(HasCreate, base.Base):
def payload(self, organization, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Label - {}'.format(
random_title()),
name=kwargs.get('name') or 'Label - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id)
organization=organization.id,
)
return payload
def create_payload(
self,
name='',
description='',
organization=Organization,
**kwargs):
def create_payload(self, name='', description='', organization=Organization, **kwargs):
self.create_and_update_dependencies(organization)
payload = self.payload(
organization=self.ds.organization,
name=name,
description=description,
**kwargs)
payload = self.payload(organization=self.ds.organization, name=name, description=description, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
organization=Organization,
**kwargs):
payload = self.create_payload(
name=name,
description=description,
organization=organization,
**kwargs)
def create(self, name='', description='', organization=Organization, **kwargs):
payload = self.create_payload(name=name, description=description, organization=organization, **kwargs)
return self.update_identity(Labels(self.connection).post(payload))
page.register_page([resources.label,
(resources.labels, 'post')], Label)
page.register_page([resources.label, (resources.labels, 'post')], Label)
class Labels(page.PageList, Label):
@@ -63,7 +44,4 @@ class Labels(page.PageList, Label):
pass
page.register_page([resources.labels,
resources.job_labels,
resources.job_template_labels,
resources.workflow_job_template_labels], Labels)
page.register_page([resources.labels, resources.job_labels, resources.job_template_labels, resources.workflow_job_template_labels], Labels)

View File

@@ -4,12 +4,9 @@ from . import page
class Metrics(base.Base):
def get(self, **query_parameters):
request = self.connection.get(self.endpoint, query_parameters,
headers={'Accept': 'application/json'})
request = self.connection.get(self.endpoint, query_parameters, headers={'Accept': 'application/json'})
return self.page_identity(request)
page.register_page([resources.metrics,
(resources.metrics, 'get')], Metrics)
page.register_page([resources.metrics, (resources.metrics, 'get')], Metrics)

View File

@@ -9,16 +9,7 @@ from . import page
job_results = ('any', 'error', 'success')
notification_types = (
'email',
'irc',
'pagerduty',
'slack',
'twilio',
'webhook',
'mattermost',
'grafana',
'rocketchat')
notification_types = ('email', 'irc', 'pagerduty', 'slack', 'twilio', 'webhook', 'mattermost', 'grafana', 'rocketchat')
class NotificationTemplate(HasCopy, HasCreate, base.Base):
@@ -28,18 +19,17 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
def test(self):
"""Create test notification"""
assert 'test' in self.related, \
"No such related attribute 'test'"
assert 'test' in self.related, "No such related attribute 'test'"
# trigger test notification
notification_id = self.related.test.post().notification
# return notification page
notifications_pg = self.get_related(
'notifications', id=notification_id).wait_until_count(1)
assert notifications_pg.count == 1, \
"test notification triggered (id:%s) but notification not found in response at %s/notifications/" % \
(notification_id, self.url)
notifications_pg = self.get_related('notifications', id=notification_id).wait_until_count(1)
assert notifications_pg.count == 1, "test notification triggered (id:%s) but notification not found in response at %s/notifications/" % (
notification_id,
self.url,
)
return notifications_pg.results[0]
def silent_delete(self):
@@ -53,41 +43,25 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
def payload(self, organization, notification_type='slack', messages=not_provided, **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'NotificationTemplate ({0}) - {1}' .format(
notification_type,
random_title()),
name=kwargs.get('name') or 'NotificationTemplate ({0}) - {1}'.format(notification_type, random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id,
notification_type=notification_type)
notification_type=notification_type,
)
if messages != not_provided:
payload['messages'] = messages
notification_configuration = kwargs.get(
'notification_configuration', {})
notification_configuration = kwargs.get('notification_configuration', {})
payload.notification_configuration = notification_configuration
if payload.notification_configuration == {}:
services = config.credentials.notification_services
if notification_type == 'email':
fields = (
'host',
'username',
'password',
'port',
'use_ssl',
'use_tls',
'sender',
'recipients')
fields = ('host', 'username', 'password', 'port', 'use_ssl', 'use_tls', 'sender', 'recipients')
cred = services.email
elif notification_type == 'irc':
fields = (
'server',
'port',
'use_ssl',
'password',
'nickname',
'targets')
fields = ('server', 'port', 'use_ssl', 'password', 'nickname', 'targets')
cred = services.irc
elif notification_type == 'pagerduty':
fields = ('client_name', 'service_key', 'subdomain', 'token')
@@ -96,34 +70,22 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
fields = ('channels', 'token')
cred = services.slack
elif notification_type == 'twilio':
fields = (
'account_sid',
'account_token',
'from_number',
'to_numbers')
fields = ('account_sid', 'account_token', 'from_number', 'to_numbers')
cred = services.twilio
elif notification_type == 'webhook':
fields = ('url', 'headers')
cred = services.webhook
elif notification_type == 'mattermost':
fields = (
'mattermost_url',
'mattermost_username',
'mattermost_channel',
'mattermost_icon_url',
'mattermost_no_verify_ssl')
fields = ('mattermost_url', 'mattermost_username', 'mattermost_channel', 'mattermost_icon_url', 'mattermost_no_verify_ssl')
cred = services.mattermost
elif notification_type == 'grafana':
fields = ('grafana_url',
'grafana_key')
fields = ('grafana_url', 'grafana_key')
cred = services.grafana
elif notification_type == 'rocketchat':
fields = ('rocketchat_url',
'rocketchat_no_verify_ssl')
fields = ('rocketchat_url', 'rocketchat_no_verify_ssl')
cred = services.rocketchat
else:
raise ValueError(
'Unknown notification_type {0}'.format(notification_type))
raise ValueError('Unknown notification_type {0}'.format(notification_type))
for field in fields:
if field == 'bot_token':
@@ -136,47 +98,21 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
return payload
def create_payload(
self,
name='',
description='',
notification_type='slack',
organization=Organization,
messages=not_provided,
**kwargs):
def create_payload(self, name='', description='', notification_type='slack', organization=Organization, messages=not_provided, **kwargs):
if notification_type not in notification_types:
raise ValueError(
'Unsupported notification type "{0}". Please use one of {1}.' .format(
notification_type, notification_types))
raise ValueError('Unsupported notification type "{0}". Please use one of {1}.'.format(notification_type, notification_types))
self.create_and_update_dependencies(organization)
payload = self.payload(
organization=self.ds.organization,
notification_type=notification_type,
name=name,
description=description,
messages=messages,
**kwargs)
organization=self.ds.organization, notification_type=notification_type, name=name, description=description, messages=messages, **kwargs
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
notification_type='slack',
organization=Organization,
messages=not_provided,
**kwargs):
def create(self, name='', description='', notification_type='slack', organization=Organization, messages=not_provided, **kwargs):
payload = self.create_payload(
name=name,
description=description,
notification_type=notification_type,
organization=organization,
messages=messages,
**kwargs)
return self.update_identity(
NotificationTemplates(
self.connection).post(payload))
name=name, description=description, notification_type=notification_type, organization=organization, messages=messages, **kwargs
)
return self.update_identity(NotificationTemplates(self.connection).post(payload))
def associate(self, resource, job_result='any'):
"""Associates a NotificationTemplate with the provided resource"""
@@ -188,15 +124,11 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
def _associate(self, resource, job_result='any', disassociate=False):
if job_result not in job_results:
raise ValueError(
'Unsupported job_result type "{0}". Please use one of {1}.' .format(
job_result, job_results))
raise ValueError('Unsupported job_result type "{0}". Please use one of {1}.'.format(job_result, job_results))
result_attr = 'notification_templates_{0}'.format(job_result)
if result_attr not in resource.related:
raise ValueError(
'Unsupported resource "{0}". Does not have a related {1} field.' .format(
resource, result_attr))
raise ValueError('Unsupported resource "{0}". Does not have a related {1} field.'.format(resource, result_attr))
payload = dict(id=self.id)
if disassociate:
@@ -206,14 +138,19 @@ class NotificationTemplate(HasCopy, HasCreate, base.Base):
getattr(resource.related, result_attr).post(payload)
page.register_page([resources.notification_template,
(resources.notification_templates, 'post'),
(resources.notification_template_copy, 'post'),
resources.notification_template_any,
resources.notification_template_started,
resources.notification_template_error,
resources.notification_template_success,
resources.notification_template_approval], NotificationTemplate)
page.register_page(
[
resources.notification_template,
(resources.notification_templates, 'post'),
(resources.notification_template_copy, 'post'),
resources.notification_template_any,
resources.notification_template_started,
resources.notification_template_error,
resources.notification_template_success,
resources.notification_template_approval,
],
NotificationTemplate,
)
class NotificationTemplates(page.PageList, NotificationTemplate):
@@ -221,14 +158,18 @@ class NotificationTemplates(page.PageList, NotificationTemplate):
pass
page.register_page([resources.notification_templates,
resources.related_notification_templates,
resources.notification_templates_any,
resources.notification_templates_started,
resources.notification_templates_error,
resources.notification_templates_success,
resources.notification_templates_approvals],
NotificationTemplates)
page.register_page(
[
resources.notification_templates,
resources.related_notification_templates,
resources.notification_templates_any,
resources.notification_templates_started,
resources.notification_templates_error,
resources.notification_templates_success,
resources.notification_templates_approvals,
],
NotificationTemplates,
)
class NotificationTemplateCopy(base.Base):
@@ -244,6 +185,4 @@ class NotificationTemplateTest(base.Base):
pass
page.register_page(
resources.notification_template_test,
NotificationTemplateTest)
page.register_page(resources.notification_template_test, NotificationTemplateTest)

View File

@@ -6,10 +6,8 @@ from . import page
class Notification(HasStatus, base.Base):
def __str__(self):
items = ['id', 'notification_type', 'status', 'error', 'notifications_sent',
'subject', 'recipients']
items = ['id', 'notification_type', 'status', 'error', 'notifications_sent', 'subject', 'recipients']
info = []
for item in [x for x in items if hasattr(self, x)]:
info.append('{0}:{1}'.format(item, getattr(self, item)))
@@ -40,13 +38,10 @@ page.register_page(resources.notification, Notification)
class Notifications(page.PageList, Notification):
def wait_until_count(self, count, interval=10, timeout=60, **kw):
"""Poll notifications page until it is populated with `count` number of notifications."""
poll_until(lambda: getattr(self.get(), 'count') == count,
interval=interval, timeout=timeout, **kw)
poll_until(lambda: getattr(self.get(), 'count') == count, interval=interval, timeout=timeout, **kw)
return self
page.register_page([resources.notifications,
resources.related_notifications], Notifications)
page.register_page([resources.notifications, resources.related_notifications], Notifications)

View File

@@ -26,22 +26,27 @@ class Organization(HasCreate, HasInstanceGroups, HasNotifications, base.Base):
if isinstance(credential, page.Page):
credential = credential.json
with suppress(exc.NoContent):
self.related.galaxy_credentials.post({
"id": credential.id,
})
self.related.galaxy_credentials.post(
{
"id": credential.id,
}
)
def remove_galaxy_credential(self, credential):
if isinstance(credential, page.Page):
credential = credential.json
with suppress(exc.NoContent):
self.related.galaxy_credentials.post({
"id": credential.id,
"disassociate": True,
})
self.related.galaxy_credentials.post(
{
"id": credential.id,
"disassociate": True,
}
)
def payload(self, **kwargs):
payload = PseudoNamespace(name=kwargs.get('name') or 'Organization - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10))
payload = PseudoNamespace(
name=kwargs.get('name') or 'Organization - {}'.format(random_title()), description=kwargs.get('description') or random_title(10)
)
payload = set_payload_foreign_key_args(payload, ('default_environment',), kwargs)
@@ -57,8 +62,7 @@ class Organization(HasCreate, HasInstanceGroups, HasNotifications, base.Base):
return self.update_identity(Organizations(self.connection).post(payload))
page.register_page([resources.organization,
(resources.organizations, 'post')], Organization)
page.register_page([resources.organization, (resources.organizations, 'post')], Organization)
class Organizations(page.PageList, Organization):
@@ -66,6 +70,4 @@ class Organizations(page.PageList, Organization):
pass
page.register_page([resources.organizations,
resources.user_organizations,
resources.project_organizations], Organizations)
page.register_page([resources.organizations, resources.user_organizations, resources.project_organizations], Organizations)

View File

@@ -6,15 +6,7 @@ import re
from requests import Response
import http.client as http
from awxkit.utils import (
PseudoNamespace,
is_relative_endpoint,
are_same_endpoint,
super_dir_set,
suppress,
is_list_or_tuple,
to_str
)
from awxkit.utils import PseudoNamespace, is_relative_endpoint, are_same_endpoint, super_dir_set, suppress, is_list_or_tuple, to_str
from awxkit.api import utils
from awxkit.api.client import Connection
from awxkit.api.registry import URLRegistry
@@ -41,17 +33,11 @@ def is_license_invalid(response):
def is_license_exceeded(response):
if re.match(
r".*license range of.*instances has been exceeded.*",
response.text):
if re.match(r".*license range of.*instances has been exceeded.*", response.text):
return True
if re.match(
r".*License count of.*instances has been reached.*",
response.text):
if re.match(r".*License count of.*instances has been reached.*", response.text):
return True
if re.match(
r".*License count of.*instances has been exceeded.*",
response.text):
if re.match(r".*License count of.*instances has been exceeded.*", response.text):
return True
if re.match(r".*License has expired.*", response.text):
return True
@@ -67,6 +53,7 @@ def is_duplicate_error(response):
def register_page(urls, page_cls):
if not _page_registry.default:
from awxkit.api.pages import Base
_page_registry.setdefault(Base)
if not is_list_or_tuple(urls):
@@ -108,32 +95,23 @@ class Page(object):
if 'endpoint' in kw:
self.endpoint = kw['endpoint']
self.connection = connection or Connection(
config.base_url, kw.get(
'verify', not config.assume_untrusted))
self.connection = connection or Connection(config.base_url, kw.get('verify', not config.assume_untrusted))
self.r = kw.get('r', None)
self.json = kw.get(
'json', objectify_response_json(
self.r) if self.r else {})
self.json = kw.get('json', objectify_response_json(self.r) if self.r else {})
self.last_elapsed = kw.get('last_elapsed', None)
def __getattr__(self, name):
if 'json' in self.__dict__ and name in self.json:
value = self.json[name]
if not isinstance(
value,
TentativePage) and is_relative_endpoint(value):
if not isinstance(value, TentativePage) and is_relative_endpoint(value):
value = TentativePage(value, self.connection)
elif isinstance(value, dict):
for key, item in value.items():
if not isinstance(
item, TentativePage) and is_relative_endpoint(item):
if not isinstance(item, TentativePage) and is_relative_endpoint(item):
value[key] = TentativePage(item, self.connection)
return value
raise AttributeError(
"{!r} object has no attribute {!r}".format(
self.__class__.__name__, name))
raise AttributeError("{!r} object has no attribute {!r}".format(self.__class__.__name__, name))
def __setattr__(self, name, value):
if 'json' in self.__dict__ and name in self.json:
@@ -200,20 +178,15 @@ class Page(object):
text = response.text
if len(text) > 1024:
text = text[:1024] + '... <<< Truncated >>> ...'
log.debug(
"Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))
log.debug("Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))
exc_str = "%s (%s) received" % (
http.responses[response.status_code], response.status_code)
exc_str = "%s (%s) received" % (http.responses[response.status_code], response.status_code)
exception = exception_from_status_code(response.status_code)
if exception:
raise exception(exc_str, data)
if response.status_code in (
http.OK,
http.CREATED,
http.ACCEPTED):
if response.status_code in (http.OK, http.CREATED, http.ACCEPTED):
# Not all JSON responses include a URL. Grab it from the request
# object, if needed.
@@ -232,13 +205,7 @@ class Page(object):
return self
registered_type = get_registered_page(request_path, request_method)
return registered_type(
self.connection,
endpoint=endpoint,
json=data,
last_elapsed=response.elapsed,
r=response,
ds=ds)
return registered_type(self.connection, endpoint=endpoint, json=data, last_elapsed=response.elapsed, r=response, ds=ds)
elif response.status_code == http.FORBIDDEN:
if is_license_invalid(response):
@@ -341,14 +308,16 @@ class Page(object):
return natural_key
_exception_map = {http.NO_CONTENT: exc.NoContent,
http.NOT_FOUND: exc.NotFound,
http.INTERNAL_SERVER_ERROR: exc.InternalServerError,
http.BAD_GATEWAY: exc.BadGateway,
http.METHOD_NOT_ALLOWED: exc.MethodNotAllowed,
http.UNAUTHORIZED: exc.Unauthorized,
http.PAYMENT_REQUIRED: exc.PaymentRequired,
http.CONFLICT: exc.Conflict}
_exception_map = {
http.NO_CONTENT: exc.NoContent,
http.NOT_FOUND: exc.NotFound,
http.INTERNAL_SERVER_ERROR: exc.InternalServerError,
http.BAD_GATEWAY: exc.BadGateway,
http.METHOD_NOT_ALLOWED: exc.MethodNotAllowed,
http.UNAUTHORIZED: exc.Unauthorized,
http.PAYMENT_REQUIRED: exc.PaymentRequired,
http.CONFLICT: exc.Conflict,
}
def exception_from_status_code(status_code):
@@ -380,12 +349,7 @@ class PageList(object):
registered_type = self.__item_class__
else:
registered_type = get_registered_page(endpoint)
items.append(
registered_type(
self.connection,
endpoint=endpoint,
json=item,
r=self.r))
items.append(registered_type(self.connection, endpoint=endpoint, json=item, r=self.r))
return items
def go_to_next(self):
@@ -407,7 +371,6 @@ class PageList(object):
class TentativePage(str):
def __new__(cls, endpoint, connection):
return super(TentativePage, cls).__new__(cls, to_str(endpoint))
@@ -416,10 +379,7 @@ class TentativePage(str):
self.connection = connection
def _create(self):
return get_registered_page(
self.endpoint)(
self.connection,
endpoint=self.endpoint)
return get_registered_page(self.endpoint)(self.connection, endpoint=self.endpoint)
def get(self, **params):
return self._create().get(**params)
@@ -436,21 +396,15 @@ class TentativePage(str):
page = None
# look up users by username not name
if 'users' in self:
assert query_parameters.get(
'username'), 'For this resource, you must call this method with a "username" to look up the object by'
assert query_parameters.get('username'), 'For this resource, you must call this method with a "username" to look up the object by'
page = self.get(username=query_parameters['username'])
else:
assert query_parameters.get(
'name'), 'For this resource, you must call this method with a "name" to look up the object by'
assert query_parameters.get('name'), 'For this resource, you must call this method with a "name" to look up the object by'
if query_parameters.get('organization'):
if isinstance(query_parameters.get('organization'), int):
page = self.get(
name=query_parameters['name'],
organization=query_parameters.get('organization'))
page = self.get(name=query_parameters['name'], organization=query_parameters.get('organization'))
else:
page = self.get(
name=query_parameters['name'],
organization=query_parameters.get('organization').id)
page = self.get(name=query_parameters['name'], organization=query_parameters.get('organization').id)
else:
page = self.get(name=query_parameters['name'])
if page and page.results:
@@ -476,13 +430,9 @@ class TentativePage(str):
if query_parameters.get('name'):
if query_parameters.get('organization'):
if isinstance(query_parameters.get('organization'), int):
page = self.get(
name=query_parameters['name'],
organization=query_parameters.get('organization'))
page = self.get(name=query_parameters['name'], organization=query_parameters.get('organization'))
else:
page = self.get(
name=query_parameters['name'],
organization=query_parameters.get('organization').id)
page = self.get(name=query_parameters['name'], organization=query_parameters.get('organization').id)
else:
page = self.get(name=query_parameters['name'])

View File

@@ -18,13 +18,11 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
def payload(self, organization, scm_type='git', **kwargs):
payload = PseudoNamespace(
name=kwargs.get('name') or 'Project - {}'.format(
random_title()),
name=kwargs.get('name') or 'Project - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
scm_type=scm_type,
scm_url=kwargs.get('scm_url') or config.project_urls.get(
scm_type,
''))
scm_url=kwargs.get('scm_url') or config.project_urls.get(scm_type, ''),
)
if organization is not None:
payload.organization = organization.id
@@ -40,43 +38,25 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
'scm_update_cache_timeout',
'scm_update_on_launch',
'scm_refspec',
'allow_override')
'allow_override',
)
update_payload(payload, fields, kwargs)
payload = set_payload_foreign_key_args(payload, ('execution_environment', 'default_environment'), kwargs)
return payload
def create_payload(
self,
name='',
description='',
scm_type='git',
scm_url='',
scm_branch='',
organization=Organization,
credential=None,
**kwargs):
def create_payload(self, name='', description='', scm_type='git', scm_url='', scm_branch='', organization=Organization, credential=None, **kwargs):
if credential:
if isinstance(credential, Credential):
if credential.ds.credential_type.namespace not in (
'scm', 'insights'):
if credential.ds.credential_type.namespace not in ('scm', 'insights'):
credential = None # ignore incompatible credential from HasCreate dependency injection
elif credential in (Credential,):
credential = (
Credential, dict(
credential_type=(
True, dict(
kind='scm'))))
credential = (Credential, dict(credential_type=(True, dict(kind='scm'))))
elif credential is True:
credential = (
Credential, dict(
credential_type=(
True, dict(
kind='scm'))))
credential = (Credential, dict(credential_type=(True, dict(kind='scm'))))
self.create_and_update_dependencies(
*filter_by_class((credential, Credential), (organization, Organization)))
self.create_and_update_dependencies(*filter_by_class((credential, Credential), (organization, Organization)))
credential = self.ds.credential if credential else None
organization = self.ds.organization if organization else None
@@ -89,20 +69,12 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
scm_url=scm_url,
scm_branch=scm_branch,
credential=credential,
**kwargs)
**kwargs
)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
name='',
description='',
scm_type='git',
scm_url='',
scm_branch='',
organization=Organization,
credential=None,
**kwargs):
def create(self, name='', description='', scm_type='git', scm_url='', scm_branch='', organization=Organization, credential=None, **kwargs):
payload = self.create_payload(
name=name,
description=description,
@@ -111,7 +83,8 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
scm_branch=scm_branch,
organization=organization,
credential=credential,
**kwargs)
**kwargs
)
self.update_identity(Projects(self.connection).post(payload))
if kwargs.get('wait', True):
@@ -127,25 +100,20 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
update_pg = self.get_related('update')
# assert can_update == True
assert update_pg.can_update, \
"The specified project (id:%s) is not able to update (can_update:%s)" % \
(self.id, update_pg.can_update)
assert update_pg.can_update, "The specified project (id:%s) is not able to update (can_update:%s)" % (self.id, update_pg.can_update)
# start the update
result = update_pg.post()
# assert JSON response
assert 'project_update' in result.json, \
"Unexpected JSON response when starting an project_update.\n%s" % \
json.dumps(result.json, indent=2)
assert 'project_update' in result.json, "Unexpected JSON response when starting an project_update.\n%s" % json.dumps(result.json, indent=2)
# locate and return the specific update
jobs_pg = self.get_related(
'project_updates',
id=result.json['project_update'])
assert jobs_pg.count == 1, \
"An project_update started (id:%s) but job not found in response at %s/inventory_updates/" % \
(result.json['project_update'], self.url)
jobs_pg = self.get_related('project_updates', id=result.json['project_update'])
assert jobs_pg.count == 1, "An project_update started (id:%s) but job not found in response at %s/inventory_updates/" % (
result.json['project_update'],
self.url,
)
return jobs_pg.results[0]
@property
@@ -154,13 +122,10 @@ class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
0) scm_type != ""
1) unified_job_template.is_successful
"""
return self.scm_type != "" and \
super(Project, self).is_successful
return self.scm_type != "" and super(Project, self).is_successful
page.register_page([resources.project,
(resources.projects, 'post'),
(resources.project_copy, 'post')], Project)
page.register_page([resources.project, (resources.projects, 'post'), (resources.project_copy, 'post')], Project)
class Projects(page.PageList, Project):
@@ -168,8 +133,7 @@ class Projects(page.PageList, Project):
pass
page.register_page([resources.projects,
resources.related_projects], Projects)
page.register_page([resources.projects, resources.related_projects], Projects)
class ProjectUpdate(UnifiedJob):
@@ -185,8 +149,7 @@ class ProjectUpdates(page.PageList, ProjectUpdate):
pass
page.register_page([resources.project_updates,
resources.project_project_updates], ProjectUpdates)
page.register_page([resources.project_updates, resources.project_project_updates], ProjectUpdates)
class ProjectUpdateLaunch(base.Base):

View File

@@ -18,15 +18,11 @@ class Role(base.Base):
cache = page.PageCache()
natural_key = super(Role, self).get_natural_key(cache=cache)
related_objs = [
related for name, related in self.related.items()
if name not in ('users', 'teams')
]
related_objs = [related for name, related in self.related.items() if name not in ('users', 'teams')]
if related_objs:
related_endpoint = cache.get_page(related_objs[0])
if related_endpoint is None:
log.error("Unable to obtain content_object %s for role %s",
related_objs[0], self.endpoint)
log.error("Unable to obtain content_object %s for role %s", related_objs[0], self.endpoint)
return None
natural_key['content_object'] = related_endpoint.get_natural_key(cache=cache)
@@ -41,6 +37,4 @@ class Roles(page.PageList, Role):
pass
page.register_page([resources.roles,
resources.related_roles,
resources.related_object_roles], Roles)
page.register_page([resources.roles, resources.related_roles, resources.related_object_roles], Roles)

View File

@@ -11,12 +11,10 @@ class Schedule(UnifiedJob):
NATURAL_KEY = ('unified_job_template', 'name')
page.register_page([resources.schedule,
resources.related_schedule], Schedule)
page.register_page([resources.schedule, resources.related_schedule], Schedule)
class Schedules(page.PageList, Schedule):
def get_zoneinfo(self):
return SchedulesZoneInfo(self.connection).get()
@@ -33,8 +31,7 @@ class Schedules(page.PageList, Schedule):
self.related.credentials.post(dict(id=cred.id, disassociate=True))
page.register_page([resources.schedules,
resources.related_schedules], Schedules)
page.register_page([resources.schedules, resources.related_schedules], Schedules)
class SchedulesPreview(base.Base):
@@ -46,7 +43,6 @@ page.register_page(((resources.schedules_preview, 'post'),), SchedulesPreview)
class SchedulesZoneInfo(base.Base):
def __getitem__(self, idx):
return self.json[idx]

View File

@@ -8,27 +8,31 @@ class Setting(base.Base):
pass
page.register_page([resources.setting,
resources.settings_all,
resources.settings_authentication,
resources.settings_changed,
resources.settings_github,
resources.settings_github_org,
resources.settings_github_team,
resources.settings_google_oauth2,
resources.settings_jobs,
resources.settings_ldap,
resources.settings_radius,
resources.settings_saml,
resources.settings_system,
resources.settings_tacacsplus,
resources.settings_ui,
resources.settings_user,
resources.settings_user_defaults], Setting)
page.register_page(
[
resources.setting,
resources.settings_all,
resources.settings_authentication,
resources.settings_changed,
resources.settings_github,
resources.settings_github_org,
resources.settings_github_team,
resources.settings_google_oauth2,
resources.settings_jobs,
resources.settings_ldap,
resources.settings_radius,
resources.settings_saml,
resources.settings_system,
resources.settings_tacacsplus,
resources.settings_ui,
resources.settings_user,
resources.settings_user_defaults,
],
Setting,
)
class Settings(page.PageList, Setting):
def get_endpoint(self, endpoint):
"""Helper method used to navigate to a specific settings endpoint.
(Pdb) settings_pg.get_endpoint('all')

View File

@@ -3,7 +3,6 @@ from . import page
class Subscriptions(page.Page):
def get_possible_licenses(self, **kwargs):
return self.post(json=kwargs).json

View File

@@ -5,7 +5,6 @@ from awxkit.api.resources import resources
class SurveySpec(base.Base):
def get_variable_default(self, var):
for item in self.spec:
if item.get('variable') == var:
@@ -26,5 +25,4 @@ class SurveySpec(base.Base):
return required_vars
page.register_page([resources.job_template_survey_spec,
resources.workflow_job_template_survey_spec], SurveySpec)
page.register_page([resources.job_template_survey_spec, resources.workflow_job_template_survey_spec], SurveySpec)

View File

@@ -5,16 +5,13 @@ from . import page
class SystemJobTemplate(UnifiedJobTemplate, HasNotifications):
def launch(self, payload={}):
"""Launch the system_job_template using related->launch endpoint."""
result = self.related.launch.post(payload)
# return job
jobs_pg = self.get_related('jobs', id=result.json['system_job'])
assert jobs_pg.count == 1, \
"system_job_template launched (id:%s) but unable to find matching " \
"job at %s/jobs/" % (result.json['job'], self.url)
assert jobs_pg.count == 1, "system_job_template launched (id:%s) but unable to find matching " "job at %s/jobs/" % (result.json['job'], self.url)
return jobs_pg.results[0]

View File

@@ -20,9 +20,11 @@ class Team(HasCreate, base.Base):
self.related.users.post(user)
def payload(self, organization, **kwargs):
payload = PseudoNamespace(name=kwargs.get('name') or 'Team - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id)
payload = PseudoNamespace(
name=kwargs.get('name') or 'Team - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10),
organization=organization.id,
)
return payload
def create_payload(self, name='', description='', organization=Organization, **kwargs):
@@ -36,8 +38,7 @@ class Team(HasCreate, base.Base):
return self.update_identity(Teams(self.connection).post(payload))
page.register_page([resources.team,
(resources.teams, 'post')], Team)
page.register_page([resources.team, (resources.teams, 'post')], Team)
class Teams(page.PageList, Team):
@@ -45,6 +46,4 @@ class Teams(page.PageList, Team):
pass
page.register_page([resources.teams,
resources.credential_owner_teams,
resources.related_teams], Teams)
page.register_page([resources.teams, resources.credential_owner_teams, resources.related_teams], Teams)

View File

@@ -26,38 +26,19 @@ class UnifiedJobTemplate(HasStatus, base.Base):
# formatting issue where result_stdout contained '%s'. This later caused
# a python traceback when attempting to display output from this
# method.
items = [
'id',
'name',
'status',
'source',
'last_update_failed',
'last_updated',
'result_traceback',
'job_explanation',
'job_args']
items = ['id', 'name', 'status', 'source', 'last_update_failed', 'last_updated', 'result_traceback', 'job_explanation', 'job_args']
info = []
for item in [x for x in items if hasattr(self, x)]:
info.append('{0}:{1}'.format(item, getattr(self, item)))
output = '<{0.__class__.__name__} {1}>'.format(self, ', '.join(info))
return output.replace('%', '%%')
def add_schedule(
self,
name='',
description='',
enabled=True,
rrule=None,
**kwargs):
def add_schedule(self, name='', description='', enabled=True, rrule=None, **kwargs):
if rrule is None:
rrule = "DTSTART:30180101T000000Z RRULE:FREQ=YEARLY;INTERVAL=1"
payload = dict(
name=name or "{0} Schedule {1}".format(
self.name,
random_title()),
description=description or random_title(10),
enabled=enabled,
rrule=str(rrule))
name=name or "{0} Schedule {1}".format(self.name, random_title()), description=description or random_title(10), enabled=enabled, rrule=str(rrule)
)
update_payload(payload, self.optional_schedule_fields, kwargs)
@@ -70,9 +51,7 @@ class UnifiedJobTemplate(HasStatus, base.Base):
2) not last_update_failed
3) last_updated
"""
return super(
UnifiedJobTemplate,
self).is_successful and not self.last_update_failed and self.last_updated is not None
return super(UnifiedJobTemplate, self).is_successful and not self.last_update_failed and self.last_updated is not None
page.register_page(resources.unified_job_template, UnifiedJobTemplate)

View File

@@ -21,8 +21,7 @@ class UnifiedJob(HasStatus, base.Base):
# NOTE: I use .replace('%', '%%') to workaround an odd string
# formatting issue where result_stdout contained '%s'. This later caused
# a python traceback when attempting to display output from this method.
items = ['id', 'name', 'status', 'failed', 'result_stdout', 'result_traceback',
'job_explanation', 'job_args']
items = ['id', 'name', 'status', 'failed', 'result_stdout', 'result_traceback', 'job_explanation', 'job_args']
info = []
for item in [x for x in items if hasattr(self, x)]:
info.append('{0}:{1}'.format(item, getattr(self, item)))
@@ -32,9 +31,7 @@ class UnifiedJob(HasStatus, base.Base):
@property
def result_stdout(self):
if 'result_stdout' not in self.json and 'stdout' in self.related:
return self.connection.get(
self.related.stdout, query_parameters=dict(format='txt_download')
).content.decode()
return self.connection.get(self.related.stdout, query_parameters=dict(format='txt_download')).content.decode()
return self.json.result_stdout.decode()
def assert_text_in_stdout(self, expected_text, replace_spaces=None, replace_newlines=' '):
@@ -55,9 +52,7 @@ class UnifiedJob(HasStatus, base.Base):
stdout = stdout.replace(' ', replace_spaces)
if expected_text not in stdout:
pretty_stdout = pformat(stdout)
raise AssertionError(
'Expected "{}", but it was not found in stdout. Full stdout:\n {}'.format(expected_text, pretty_stdout)
)
raise AssertionError('Expected "{}", but it was not found in stdout. Full stdout:\n {}'.format(expected_text, pretty_stdout))
@property
def is_successful(self):
@@ -103,7 +98,7 @@ class UnifiedJob(HasStatus, base.Base):
# Race condition where job finishes between can_cancel
# check and post.
if not any("not allowed" in field for field in e.msg.values()):
raise(e)
raise (e)
return self.get()
@property
@@ -114,6 +109,7 @@ class UnifiedJob(HasStatus, base.Base):
```assert dict(extra_var=extra_var_val) in unified_job.job_args```
If you need to ensure the job_args are of awx-provided format use raw unified_job.json.job_args.
"""
def attempt_yaml_load(arg):
try:
return yaml.safe_load(arg)
@@ -151,10 +147,7 @@ class UnifiedJob(HasStatus, base.Base):
if host_loc.startswith(expected_prefix):
return host_loc
raise RuntimeError(
'Could not find a controller private_data_dir for this job. '
'Searched for volume mount to {} inside of args {}'.format(
expected_prefix, job_args
)
'Could not find a controller private_data_dir for this job. ' 'Searched for volume mount to {} inside of args {}'.format(expected_prefix, job_args)
)
@@ -163,7 +156,4 @@ class UnifiedJobs(page.PageList, UnifiedJob):
pass
page.register_page([resources.unified_jobs,
resources.instance_related_jobs,
resources.instance_group_related_jobs,
resources.schedules_jobs], UnifiedJobs)
page.register_page([resources.unified_jobs, resources.instance_related_jobs, resources.instance_group_related_jobs, resources.schedules_jobs], UnifiedJobs)

View File

@@ -13,26 +13,13 @@ class User(HasCreate, base.Base):
def payload(self, **kwargs):
payload = PseudoNamespace(
username=kwargs.get('username') or 'User-{}'.format(
random_title(
non_ascii=False)),
username=kwargs.get('username') or 'User-{}'.format(random_title(non_ascii=False)),
password=kwargs.get('password') or config.credentials.default.password,
is_superuser=kwargs.get(
'is_superuser',
False),
is_system_auditor=kwargs.get(
'is_system_auditor',
False),
first_name=kwargs.get(
'first_name',
random_title()),
last_name=kwargs.get(
'last_name',
random_title()),
email=kwargs.get(
'email',
'{}@example.com'.format(random_title(5, non_ascii=False))
)
is_superuser=kwargs.get('is_superuser', False),
is_system_auditor=kwargs.get('is_system_auditor', False),
first_name=kwargs.get('first_name', random_title()),
last_name=kwargs.get('last_name', random_title()),
email=kwargs.get('email', '{}@example.com'.format(random_title(5, non_ascii=False))),
)
return payload
@@ -42,8 +29,7 @@ class User(HasCreate, base.Base):
return payload
def create(self, username='', password='', organization=None, **kwargs):
payload = self.create_payload(
username=username, password=password, **kwargs)
payload = self.create_payload(username=username, password=password, **kwargs)
self.password = payload.password
self.update_identity(Users(self.connection).post(payload))
@@ -54,8 +40,7 @@ class User(HasCreate, base.Base):
return self
page.register_page([resources.user,
(resources.users, 'post')], User)
page.register_page([resources.user, (resources.users, 'post')], User)
class Users(page.PageList, User):
@@ -63,11 +48,9 @@ class Users(page.PageList, User):
pass
page.register_page([resources.users,
resources.organization_admins,
resources.related_users,
resources.credential_owner_users,
resources.user_admin_organizations], Users)
page.register_page(
[resources.users, resources.organization_admins, resources.related_users, resources.credential_owner_users, resources.user_admin_organizations], Users
)
class Me(Users):

View File

@@ -5,7 +5,6 @@ from awxkit import exceptions
class WorkflowApproval(UnifiedJob):
def approve(self):
try:
self.related.approve.post()

View File

@@ -5,7 +5,6 @@ from . import page
class WorkflowJobNode(base.Base):
def wait_for_job(self, interval=5, timeout=60, **kw):
"""Waits until node's job exists"""
adjusted_timeout = timeout - seconds_since_date_string(self.created)
@@ -30,8 +29,13 @@ class WorkflowJobNodes(page.PageList, WorkflowJobNode):
pass
page.register_page([resources.workflow_job_nodes,
resources.workflow_job_workflow_nodes,
resources.workflow_job_node_always_nodes,
resources.workflow_job_node_failure_nodes,
resources.workflow_job_node_success_nodes], WorkflowJobNodes)
page.register_page(
[
resources.workflow_job_nodes,
resources.workflow_job_workflow_nodes,
resources.workflow_job_node_always_nodes,
resources.workflow_job_node_failure_nodes,
resources.workflow_job_node_success_nodes,
],
WorkflowJobNodes,
)

View File

@@ -15,12 +15,9 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
def payload(self, workflow_job_template, unified_job_template, **kwargs):
if not unified_job_template:
# May pass "None" to explicitly create an approval node
payload = PseudoNamespace(
workflow_job_template=workflow_job_template.id)
payload = PseudoNamespace(workflow_job_template=workflow_job_template.id)
else:
payload = PseudoNamespace(
workflow_job_template=workflow_job_template.id,
unified_job_template=unified_job_template.id)
payload = PseudoNamespace(workflow_job_template=workflow_job_template.id, unified_job_template=unified_job_template.id)
optional_fields = (
'diff_mode',
@@ -33,7 +30,8 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
'verbosity',
'extra_data',
'identifier',
'all_parents_must_converge')
'all_parents_must_converge',
)
update_payload(payload, optional_fields, kwargs)
@@ -42,45 +40,23 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
return payload
def create_payload(
self,
workflow_job_template=WorkflowJobTemplate,
unified_job_template=JobTemplate,
**kwargs):
def create_payload(self, workflow_job_template=WorkflowJobTemplate, unified_job_template=JobTemplate, **kwargs):
if not unified_job_template:
self.create_and_update_dependencies(workflow_job_template)
payload = self.payload(
workflow_job_template=self.ds.workflow_job_template,
unified_job_template=None,
**kwargs)
payload = self.payload(workflow_job_template=self.ds.workflow_job_template, unified_job_template=None, **kwargs)
else:
self.create_and_update_dependencies(
workflow_job_template, unified_job_template)
payload = self.payload(
workflow_job_template=self.ds.workflow_job_template,
unified_job_template=self.ds.unified_job_template,
**kwargs)
self.create_and_update_dependencies(workflow_job_template, unified_job_template)
payload = self.payload(workflow_job_template=self.ds.workflow_job_template, unified_job_template=self.ds.unified_job_template, **kwargs)
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
return payload
def create(
self,
workflow_job_template=WorkflowJobTemplate,
unified_job_template=JobTemplate,
**kwargs):
payload = self.create_payload(
workflow_job_template=workflow_job_template,
unified_job_template=unified_job_template,
**kwargs)
return self.update_identity(
WorkflowJobTemplateNodes(
self.connection).post(payload))
def create(self, workflow_job_template=WorkflowJobTemplate, unified_job_template=JobTemplate, **kwargs):
payload = self.create_payload(workflow_job_template=workflow_job_template, unified_job_template=unified_job_template, **kwargs)
return self.update_identity(WorkflowJobTemplateNodes(self.connection).post(payload))
def _add_node(self, endpoint, unified_job_template, **kwargs):
node = endpoint.post(
dict(unified_job_template=unified_job_template.id, **kwargs))
node.create_and_update_dependencies(
self.ds.workflow_job_template, unified_job_template)
node = endpoint.post(dict(unified_job_template=unified_job_template.id, **kwargs))
node.create_and_update_dependencies(self.ds.workflow_job_template, unified_job_template)
return node
def add_always_node(self, unified_job_template, **kwargs):
@@ -94,24 +70,18 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
def add_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, associate=True))
self.related.credentials.post(dict(id=credential.id, associate=True))
def remove_credential(self, credential):
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=credential.id, disassociate=True))
self.related.credentials.post(dict(id=credential.id, disassociate=True))
def remove_all_credentials(self):
for cred in self.related.credentials.get().results:
with suppress(exc.NoContent):
self.related.credentials.post(
dict(id=cred.id, disassociate=True))
self.related.credentials.post(dict(id=cred.id, disassociate=True))
def make_approval_node(
self,
**kwargs
):
def make_approval_node(self, **kwargs):
if 'name' not in kwargs:
kwargs['name'] = 'approval node {}'.format(random_title())
self.related.create_approval_template.post(kwargs)
@@ -122,10 +92,10 @@ class WorkflowJobTemplateNode(HasCreate, base.Base):
return candidates.results.pop()
page.register_page([resources.workflow_job_template_node,
(resources.workflow_job_template_nodes, 'post'),
(resources.workflow_job_template_workflow_nodes, 'post')],
WorkflowJobTemplateNode)
page.register_page(
[resources.workflow_job_template_node, (resources.workflow_job_template_nodes, 'post'), (resources.workflow_job_template_workflow_nodes, 'post')],
WorkflowJobTemplateNode,
)
class WorkflowJobTemplateNodes(page.PageList, WorkflowJobTemplateNode):
@@ -133,9 +103,13 @@ class WorkflowJobTemplateNodes(page.PageList, WorkflowJobTemplateNode):
pass
page.register_page([resources.workflow_job_template_nodes,
resources.workflow_job_template_workflow_nodes,
resources.workflow_job_template_node_always_nodes,
resources.workflow_job_template_node_failure_nodes,
resources.workflow_job_template_node_success_nodes],
WorkflowJobTemplateNodes)
page.register_page(
[
resources.workflow_job_template_nodes,
resources.workflow_job_template_workflow_nodes,
resources.workflow_job_template_node_always_nodes,
resources.workflow_job_template_node_failure_nodes,
resources.workflow_job_template_node_success_nodes,
],
WorkflowJobTemplateNodes,
)

View File

@@ -26,15 +26,14 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi
# return job
jobs_pg = self.related.workflow_jobs.get(id=result.workflow_job)
if jobs_pg.count != 1:
msg = "workflow_job_template launched (id:{}) but job not found in response at {}/workflow_jobs/".format(
result.json['workflow_job'], self.url
)
msg = "workflow_job_template launched (id:{}) but job not found in response at {}/workflow_jobs/".format(result.json['workflow_job'], self.url)
raise exc.UnexpectedAWXState(msg)
return jobs_pg.results[0]
def payload(self, **kwargs):
payload = PseudoNamespace(name=kwargs.get('name') or 'WorkflowJobTemplate - {}'.format(random_title()),
description=kwargs.get('description') or random_title(10))
payload = PseudoNamespace(
name=kwargs.get('name') or 'WorkflowJobTemplate - {}'.format(random_title()), description=kwargs.get('description') or random_title(10)
)
optional_fields = (
"allow_simultaneous",
@@ -91,9 +90,9 @@ class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, Unifi
self.related.labels.post(label)
page.register_page([resources.workflow_job_template,
(resources.workflow_job_templates, 'post'),
(resources.workflow_job_template_copy, 'post')], WorkflowJobTemplate)
page.register_page(
[resources.workflow_job_template, (resources.workflow_job_templates, 'post'), (resources.workflow_job_template_copy, 'post')], WorkflowJobTemplate
)
class WorkflowJobTemplates(page.PageList, WorkflowJobTemplate):
@@ -101,8 +100,7 @@ class WorkflowJobTemplates(page.PageList, WorkflowJobTemplate):
pass
page.register_page([resources.workflow_job_templates,
resources.related_workflow_job_templates], WorkflowJobTemplates)
page.register_page([resources.workflow_job_templates, resources.related_workflow_job_templates], WorkflowJobTemplates)
class WorkflowJobTemplateLaunch(base.Base):

View File

@@ -4,7 +4,6 @@ from . import page
class WorkflowJob(UnifiedJob):
def __str__(self):
# TODO: Update after endpoint's fields are finished filling out
return super(UnifiedJob, self).__str__()
@@ -56,7 +55,4 @@ class WorkflowJobs(page.PageList, WorkflowJob):
pass
page.register_page([resources.workflow_jobs,
resources.workflow_job_template_jobs,
resources.job_template_slice_workflow_jobs],
WorkflowJobs)
page.register_page([resources.workflow_jobs, resources.workflow_job_template_jobs, resources.job_template_slice_workflow_jobs], WorkflowJobs)

View File

@@ -8,7 +8,6 @@ log = logging.getLogger(__name__)
class URLRegistry(object):
def __init__(self):
self.store = defaultdict(dict)
self.default = {}
@@ -81,8 +80,7 @@ class URLRegistry(object):
if method_pattern.pattern == not_provided:
exc_msg = '"{0.pattern}" already has methodless registration.'.format(url_pattern)
else:
exc_msg = ('"{0.pattern}" already has registered method "{1.pattern}"'
.format(url_pattern, method_pattern))
exc_msg = '"{0.pattern}" already has registered method "{1.pattern}"'.format(url_pattern, method_pattern)
raise TypeError(exc_msg)
self.store[url_pattern][method_pattern] = resource

View File

@@ -1,4 +1,3 @@
class Resources(object):
_activity = r'activity_stream/\d+/'

View File

@@ -15,12 +15,11 @@ def freeze(key):
def parse_description(desc):
options = {}
for line in desc[desc.index('POST'):].splitlines():
for line in desc[desc.index('POST') :].splitlines():
match = descRE.match(line)
if not match:
continue
options[match.group(1)] = {'type': match.group(2),
'required': match.group(3) == 'required'}
options[match.group(1)] = {'type': match.group(2), 'required': match.group(3) == 'required'}
return options
@@ -45,6 +44,5 @@ def get_post_fields(page, cache):
if 'POST' in options_page.json['actions']:
return options_page.json['actions']['POST']
else:
log.warning(
"Insufficient privileges on %s, inferring POST fields from description.", options_page.endpoint)
log.warning("Insufficient privileges on %s, inferring POST fields from description.", options_page.endpoint)
return parse_description(options_page.json['description'])

View File

@@ -17,13 +17,14 @@ def upload_inventory(ansible_runner, nhosts=10, ini=False):
copy_content = '''#!/bin/bash
cat <<EOF
%s
EOF''' % json_inventory(nhosts)
EOF''' % json_inventory(
nhosts
)
# Copy script to test system
contacted = ansible_runner.copy(dest=copy_dest, force=True, mode=copy_mode, content=copy_content)
for result in contacted.values():
assert not result.get('failed', False), \
"Failed to create inventory file: %s" % result
assert not result.get('failed', False), "Failed to create inventory file: %s" % result
return copy_dest
@@ -49,8 +50,7 @@ def generate_inventory(nhosts=100):
group_by_10s = 'group-%07dX.example.com' % (n / 10)
group_by_100s = 'group-%06dXX.example.com' % (n / 100)
group_by_1000s = 'group-%05dXXX.example.com' % (n / 1000)
for group in [group_evens_odds, group_threes, group_fours, group_fives, group_sixes, group_sevens,
group_eights, group_nines, group_tens, group_by_10s]:
for group in [group_evens_odds, group_threes, group_fours, group_fives, group_sixes, group_sevens, group_eights, group_nines, group_tens, group_by_10s]:
if not group:
continue
if group in inv_list:
@@ -58,11 +58,9 @@ def generate_inventory(nhosts=100):
else:
inv_list[group] = {'hosts': [hostname], 'children': [], 'vars': {'group_prefix': group.split('.')[0]}}
if group_by_1000s not in inv_list:
inv_list[group_by_1000s] = {'hosts': [], 'children': [],
'vars': {'group_prefix': group_by_1000s.split('.')[0]}}
inv_list[group_by_1000s] = {'hosts': [], 'children': [], 'vars': {'group_prefix': group_by_1000s.split('.')[0]}}
if group_by_100s not in inv_list:
inv_list[group_by_100s] = {'hosts': [], 'children': [],
'vars': {'group_prefix': group_by_100s.split('.')[0]}}
inv_list[group_by_100s] = {'hosts': [], 'children': [], 'vars': {'group_prefix': group_by_100s.split('.')[0]}}
if group_by_100s not in inv_list[group_by_1000s]['children']:
inv_list[group_by_1000s]['children'].append(group_by_100s)
if group_by_10s not in inv_list[group_by_100s]['children']:

View File

@@ -31,9 +31,22 @@ def _delete_all(endpoint):
def delete_all(v):
for endpoint in (v.unified_jobs, v.job_templates, v.workflow_job_templates, v.notification_templates,
v.projects, v.inventory, v.hosts, v.inventory_scripts, v.labels, v.credentials,
v.teams, v.users, v.organizations, v.schedules):
for endpoint in (
v.unified_jobs,
v.job_templates,
v.workflow_job_templates,
v.notification_templates,
v.projects,
v.inventory,
v.hosts,
v.inventory_scripts,
v.labels,
v.credentials,
v.teams,
v.users,
v.organizations,
v.schedules,
):
_delete_all(endpoint)

View File

@@ -56,14 +56,7 @@ def run(stdout=sys.stdout, stderr=sys.stderr, argv=[]):
json.dump(e.msg, sys.stdout)
print('')
elif cli.get_config('format') == 'yaml':
sys.stdout.write(to_str(
yaml.safe_dump(
e.msg,
default_flow_style=False,
encoding='utf-8',
allow_unicode=True
)
))
sys.stdout.write(to_str(yaml.safe_dump(e.msg, default_flow_style=False, encoding='utf-8', allow_unicode=True)))
elif cli.get_config('format') == 'human':
sys.stdout.write(e.__class__.__name__)
print('')

View File

@@ -8,9 +8,7 @@ import sys
from requests.exceptions import RequestException
from .custom import handle_custom_actions
from .format import (add_authentication_arguments,
add_output_formatting_arguments,
FORMATTERS, format_response)
from .format import add_authentication_arguments, add_output_formatting_arguments, FORMATTERS, format_response
from .options import ResourceOptionsParser, UNIQUENESS_RULES
from .resource import parse_resource, is_control_resource
from awxkit import api, config, utils, exceptions, WSClient # noqa
@@ -88,7 +86,9 @@ class CLI(object):
token = self.get_config('token')
if token:
self.root.connection.login(
None, None, token=token,
None,
None,
token=token,
)
else:
config.use_sessions = True
@@ -102,12 +102,14 @@ class CLI(object):
if self.get_config('insecure'):
config.assume_untrusted = True
config.credentials = utils.PseudoNamespace({
'default': {
'username': self.get_config('username'),
'password': self.get_config('password'),
config.credentials = utils.PseudoNamespace(
{
'default': {
'username': self.get_config('username'),
'password': self.get_config('password'),
}
}
})
)
_, remainder = self.parser.parse_known_args()
if remainder and remainder[0] == 'config':
@@ -133,11 +135,7 @@ class CLI(object):
try:
self.v2 = self.root.get().available_versions.v2.get()
except AttributeError:
raise RuntimeError(
'An error occurred while fetching {}/api/'.format(
self.get_config('host')
)
)
raise RuntimeError('An error occurred while fetching {}/api/'.format(self.get_config('host')))
def parse_resource(self, skip_deprecated=False):
"""Attempt to parse the <resource> (e.g., jobs) specified on the CLI
@@ -170,33 +168,15 @@ class CLI(object):
_filter = self.get_config('filter')
# human format for metrics, settings is special
if (
self.resource in ('metrics', 'settings') and
self.get_config('format') == 'human'
):
response.json = {
'count': len(response.json),
'results': [
{'key': k, 'value': v}
for k, v in response.json.items()
]
}
if self.resource in ('metrics', 'settings') and self.get_config('format') == 'human':
response.json = {'count': len(response.json), 'results': [{'key': k, 'value': v} for k, v in response.json.items()]}
_filter = 'key, value'
if (
self.get_config('format') == 'human' and
_filter == '.' and
self.resource in UNIQUENESS_RULES
):
if self.get_config('format') == 'human' and _filter == '.' and self.resource in UNIQUENESS_RULES:
_filter = ', '.join(UNIQUENESS_RULES[self.resource])
formatted = format_response(
response,
fmt=self.get_config('format'),
filter=_filter,
changed=self.original_action in (
'modify', 'create', 'associate', 'disassociate'
)
response, fmt=self.get_config('format'), filter=_filter, changed=self.original_action in ('modify', 'create', 'associate', 'disassociate')
)
if formatted:
print(utils.to_str(formatted), file=self.stdout)
@@ -219,10 +199,7 @@ class CLI(object):
_without_ triggering a SystemExit (argparse's
behavior if required arguments are missing)
"""
subparsers = self.subparsers[self.resource].add_subparsers(
dest='action',
metavar='action'
)
subparsers = self.subparsers[self.resource].add_subparsers(dest='action', metavar='action')
subparsers.required = True
# parse the action from OPTIONS
@@ -252,10 +229,7 @@ class CLI(object):
if self.resource != 'settings':
for method in ('list', 'modify', 'create'):
if method in parser.parser.choices:
parser.build_query_arguments(
method,
'GET' if method == 'list' else 'POST'
)
parser.build_query_arguments(method, 'GET' if method == 'list' else 'POST')
if from_sphinx:
parsed, extra = self.parser.parse_known_args(self.argv)
else:
@@ -263,10 +237,7 @@ class CLI(object):
if extra and self.verbose:
# If extraneous arguments were provided, warn the user
cprint('{}: unrecognized arguments: {}'.format(
self.parser.prog,
' '.join(extra)
), 'yellow', file=self.stdout)
cprint('{}: unrecognized arguments: {}'.format(self.parser.prog, ' '.join(extra)), 'yellow', file=self.stdout)
# build a dictionary of all of the _valid_ flags specified on the
# command line so we can pass them on to the underlying awxkit call
@@ -275,14 +246,7 @@ class CLI(object):
# everything else is a flag used as a query argument for the HTTP
# request we'll make (e.g., --username="Joe", --verbosity=3)
parsed = parsed.__dict__
parsed = dict(
(k, v) for k, v in parsed.items()
if (
v is not None and
k not in ('help', 'resource') and
not k.startswith('conf.')
)
)
parsed = dict((k, v) for k, v in parsed.items() if (v is not None and k not in ('help', 'resource') and not k.startswith('conf.')))
# if `id` is one of the arguments, it's a detail view
if 'id' in parsed:
@@ -290,9 +254,7 @@ class CLI(object):
# determine the awxkit method to call
action = self.original_action = parsed.pop('action')
page, action = handle_custom_actions(
self.resource, action, page
)
page, action = handle_custom_actions(self.resource, action, page)
self.method = {
'list': 'get',
'modify': 'patch',
@@ -327,13 +289,7 @@ class CLI(object):
action='store_true',
help='prints usage information for the awx tool',
)
self.parser.add_argument(
'--version',
dest='conf.version',
action='version',
help='display awx CLI version',
version=__version__
)
self.parser.add_argument('--version', dest='conf.version', action='version', help='display awx CLI version', version=__version__)
add_authentication_arguments(self.parser, env)
add_output_formatting_arguments(self.parser, env)

View File

@@ -16,7 +16,6 @@ def handle_custom_actions(resource, action, page):
class CustomActionRegistryMeta(CustomRegistryMeta):
@property
def name(self):
return ' '.join([self.resource, self.action])
@@ -45,33 +44,16 @@ class CustomAction(metaclass=CustomActionRegistryMeta):
class Launchable(object):
def add_arguments(self, parser, resource_options_parser, with_pk=True):
from .options import pk_or_name
if with_pk:
parser.choices[self.action].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
parser.choices[self.action].add_argument(
'--monitor', action='store_true',
help='If set, prints stdout of the launched job until it finishes.'
)
parser.choices[self.action].add_argument(
'--timeout', type=int,
help='If set with --monitor or --wait, time out waiting on job completion.' # noqa
)
parser.choices[self.action].add_argument(
'--wait', action='store_true',
help='If set, waits until the launched job finishes.'
)
launch_time_options = self.page.connection.options(
self.page.endpoint + '1/{}/'.format(self.action)
)
if with_pk:
parser.choices[self.action].add_argument('id', type=functools.partial(pk_or_name, None, self.resource, page=self.page), help='')
parser.choices[self.action].add_argument('--monitor', action='store_true', help='If set, prints stdout of the launched job until it finishes.')
parser.choices[self.action].add_argument('--timeout', type=int, help='If set with --monitor or --wait, time out waiting on job completion.') # noqa
parser.choices[self.action].add_argument('--wait', action='store_true', help='If set, waits until the launched job finishes.')
launch_time_options = self.page.connection.options(self.page.endpoint + '1/{}/'.format(self.action))
if launch_time_options.ok:
launch_time_options = launch_time_options.json()['actions']['POST']
resource_options_parser.options['LAUNCH'] = launch_time_options
@@ -118,24 +100,15 @@ class ProjectCreate(CustomAction):
resource = 'projects'
def add_arguments(self, parser, resource_options_parser):
parser.choices[self.action].add_argument(
'--monitor', action='store_true',
help=('If set, prints stdout of the project update until '
'it finishes.')
)
parser.choices[self.action].add_argument(
'--wait', action='store_true',
help='If set, waits until the new project has updated.'
)
parser.choices[self.action].add_argument('--monitor', action='store_true', help=('If set, prints stdout of the project update until ' 'it finishes.'))
parser.choices[self.action].add_argument('--wait', action='store_true', help='If set, waits until the new project has updated.')
def post(self, kwargs):
should_monitor = kwargs.pop('monitor', False)
wait = kwargs.pop('wait', False)
response = self.page.post(kwargs)
if should_monitor or wait:
update = response.related.project_updates.get(
order_by='-created'
).results[0]
update = response.related.project_updates.get(order_by='-created').results[0]
monitor(
update,
self.page.connection.session,
@@ -154,9 +127,7 @@ class AdhocCommandLaunch(Launchable, CustomAction):
resource = 'ad_hoc_commands'
def add_arguments(self, parser, resource_options_parser):
Launchable.add_arguments(
self, parser, resource_options_parser, with_pk=False
)
Launchable.add_arguments(self, parser, resource_options_parser, with_pk=False)
def perform(self, **kwargs):
monitor_kwargs = {
@@ -182,22 +153,14 @@ class HasStdout(object):
def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name
parser.choices['stdout'].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
parser.choices['stdout'].add_argument('id', type=functools.partial(pk_or_name, None, self.resource, page=self.page), help='')
def perform(self):
fmt = 'txt_download'
if color_enabled():
fmt = 'ansi_download'
return self.page.connection.get(
self.page.get().related.stdout,
query_parameters=dict(format=fmt)
).content.decode('utf-8')
return self.page.connection.get(self.page.get().related.stdout, query_parameters=dict(format=fmt)).content.decode('utf-8')
class JobStdout(HasStdout, CustomAction):
@@ -222,13 +185,8 @@ class AssociationMixin(object):
def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name
parser.choices[self.action].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
parser.choices[self.action].add_argument('id', type=functools.partial(pk_or_name, None, self.resource, page=self.page), help='')
group = parser.choices[self.action].add_mutually_exclusive_group(required=True)
for param, endpoint in self.targets.items():
field, model_name = endpoint
@@ -237,7 +195,6 @@ class AssociationMixin(object):
help_text = 'The ID (or name) of the {} to {}'.format(model_name, self.action)
class related_page(object):
def __init__(self, connection, resource):
self.conn = connection
self.resource = {
@@ -256,20 +213,15 @@ class AssociationMixin(object):
group.add_argument(
'--{}'.format(param),
metavar='',
type=functools.partial(
pk_or_name, None, param,
page=related_page(self.page.connection, param)
),
help=help_text
type=functools.partial(pk_or_name, None, param, page=related_page(self.page.connection, param)),
help=help_text,
)
def perform(self, **kwargs):
for k, v in kwargs.items():
endpoint, _ = self.targets[k]
try:
self.page.get().related[endpoint].post(
{'id': v, self.action: True}
)
self.page.get().related[endpoint].post({'id': v, self.action: True})
except NoContent:
# we expect to enter this block because these endpoints return
# HTTP 204 on success
@@ -279,18 +231,9 @@ class AssociationMixin(object):
class NotificationAssociateMixin(AssociationMixin):
targets = {
'start_notification': [
'notification_templates_started',
'notification_template'
],
'success_notification': [
'notification_templates_success',
'notification_template'
],
'failure_notification': [
'notification_templates_error',
'notification_template'
],
'start_notification': ['notification_templates_started', 'notification_template'],
'success_notification': ['notification_templates_success', 'notification_template'],
'failure_notification': ['notification_templates_error', 'notification_template'],
}
@@ -306,12 +249,16 @@ class JobTemplateNotificationDisAssociation(NotificationAssociateMixin, CustomAc
targets = NotificationAssociateMixin.targets.copy()
JobTemplateNotificationAssociation.targets.update({
'credential': ['credentials', None],
})
JobTemplateNotificationDisAssociation.targets.update({
'credential': ['credentials', None],
})
JobTemplateNotificationAssociation.targets.update(
{
'credential': ['credentials', None],
}
)
JobTemplateNotificationDisAssociation.targets.update(
{
'credential': ['credentials', None],
}
)
class WorkflowJobTemplateNotificationAssociation(NotificationAssociateMixin, CustomAction):
@@ -326,12 +273,16 @@ class WorkflowJobTemplateNotificationDisAssociation(NotificationAssociateMixin,
targets = NotificationAssociateMixin.targets.copy()
WorkflowJobTemplateNotificationAssociation.targets.update({
'approval_notification': ['notification_templates_approvals', 'notification_template'],
})
WorkflowJobTemplateNotificationDisAssociation.targets.update({
'approval_notification': ['notification_templates_approvals', 'notification_template'],
})
WorkflowJobTemplateNotificationAssociation.targets.update(
{
'approval_notification': ['notification_templates_approvals', 'notification_template'],
}
)
WorkflowJobTemplateNotificationDisAssociation.targets.update(
{
'approval_notification': ['notification_templates_approvals', 'notification_template'],
}
)
class ProjectNotificationAssociation(NotificationAssociateMixin, CustomAction):
@@ -366,14 +317,18 @@ class OrganizationNotificationDisAssociation(NotificationAssociateMixin, CustomA
targets = NotificationAssociateMixin.targets.copy()
OrganizationNotificationAssociation.targets.update({
'approval_notification': ['notification_templates_approvals', 'notification_template'],
'galaxy_credential': ['galaxy_credentials', 'credential'],
})
OrganizationNotificationDisAssociation.targets.update({
'approval_notification': ['notification_templates_approvals', 'notification_template'],
'galaxy_credential': ['galaxy_credentials', 'credential'],
})
OrganizationNotificationAssociation.targets.update(
{
'approval_notification': ['notification_templates_approvals', 'notification_template'],
'galaxy_credential': ['galaxy_credentials', 'credential'],
}
)
OrganizationNotificationDisAssociation.targets.update(
{
'approval_notification': ['notification_templates_approvals', 'notification_template'],
'galaxy_credential': ['galaxy_credentials', 'credential'],
}
)
class SettingsList(CustomAction):
@@ -381,9 +336,7 @@ class SettingsList(CustomAction):
resource = 'settings'
def add_arguments(self, parser, resource_options_parser):
parser.choices['list'].add_argument(
'--slug', help='optional setting category/slug', default='all'
)
parser.choices['list'].add_argument('--slug', help='optional setting category/slug', default='all')
def perform(self, slug):
self.page.endpoint = self.page.endpoint + '{}/'.format(slug)
@@ -409,30 +362,18 @@ class RoleMixin(object):
if not RoleMixin.roles:
for resource, flag in self.has_roles:
options = self.page.__class__(
self.page.endpoint.replace(self.resource, resource),
self.page.connection
).options()
RoleMixin.roles[flag] = [
role.replace('_role', '')
for role in options.json.get('object_roles', [])
]
options = self.page.__class__(self.page.endpoint.replace(self.resource, resource), self.page.connection).options()
RoleMixin.roles[flag] = [role.replace('_role', '') for role in options.json.get('object_roles', [])]
possible_roles = set()
for v in RoleMixin.roles.values():
possible_roles.update(v)
resource_group = parser.choices[self.action].add_mutually_exclusive_group(
required=True
)
resource_group = parser.choices[self.action].add_mutually_exclusive_group(required=True)
parser.choices[self.action].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help='The ID (or name) of the {} to {} access to/from'.format(
self.resource, self.action
)
type=functools.partial(pk_or_name, None, self.resource, page=self.page),
help='The ID (or name) of the {} to {} access to/from'.format(self.resource, self.action),
)
for _type in RoleMixin.roles.keys():
if _type == 'team' and self.resource == 'team':
@@ -440,7 +381,6 @@ class RoleMixin(object):
continue
class related_page(object):
def __init__(self, connection, resource):
self.conn = connection
if resource == 'inventories':
@@ -453,19 +393,12 @@ class RoleMixin(object):
resource_group.add_argument(
'--{}'.format(_type),
type=functools.partial(
pk_or_name, None, _type,
page=related_page(
self.page.connection,
dict((v, k) for k, v in self.has_roles)[_type]
)
),
type=functools.partial(pk_or_name, None, _type, page=related_page(self.page.connection, dict((v, k) for k, v in self.has_roles)[_type])),
metavar='ID',
help='The ID (or name) of the target {}'.format(_type),
)
parser.choices[self.action].add_argument(
'--role', type=str, choices=possible_roles, required=True,
help='The name of the role to {}'.format(self.action)
'--role', type=str, choices=possible_roles, required=True, help='The name of the role to {}'.format(self.action)
)
def perform(self, **kwargs):
@@ -474,17 +407,10 @@ class RoleMixin(object):
role = kwargs['role']
if role not in RoleMixin.roles[flag]:
options = ', '.join(RoleMixin.roles[flag])
raise ValueError(
"invalid choice: '{}' must be one of {}".format(
role, options
)
)
raise ValueError("invalid choice: '{}' must be one of {}".format(role, options))
value = kwargs[flag]
target = '/api/v2/{}/{}'.format(resource, value)
detail = self.page.__class__(
target,
self.page.connection
).get()
detail = self.page.__class__(target, self.page.connection).get()
object_roles = detail['summary_fields']['object_roles']
actual_role = object_roles[role + '_role']
params = {'id': actual_role['id']}
@@ -530,15 +456,8 @@ class SettingsModify(CustomAction):
resource = 'settings'
def add_arguments(self, parser, resource_options_parser):
options = self.page.__class__(
self.page.endpoint + 'all/', self.page.connection
).options()
parser.choices['modify'].add_argument(
'key',
choices=sorted(options['actions']['PUT'].keys()),
metavar='key',
help=''
)
options = self.page.__class__(self.page.endpoint + 'all/', self.page.connection).options()
parser.choices['modify'].add_argument('key', choices=sorted(options['actions']['PUT'].keys()), metavar='key', help='')
parser.choices['modify'].add_argument('value', help='')
def perform(self, key, value):
@@ -563,13 +482,8 @@ class HasMonitor(object):
def add_arguments(self, parser, resource_options_parser):
from .options import pk_or_name
parser.choices[self.action].add_argument(
'id',
type=functools.partial(
pk_or_name, None, self.resource, page=self.page
),
help=''
)
parser.choices[self.action].add_argument('id', type=functools.partial(pk_or_name, None, self.resource, page=self.page), help='')
def perform(self, **kwargs):
response = self.page.get()

View File

@@ -27,9 +27,7 @@ author = 'Ansible by Red Hat'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'awxkit.cli.sphinx'
]
extensions = ['awxkit.cli.sphinx']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

View File

@@ -48,24 +48,21 @@ def add_output_formatting_arguments(parser, env):
dest='conf.format',
choices=FORMATTERS.keys(),
default=env.get('TOWER_FORMAT', 'json'),
help=(
'specify a format for the input and output'
),
help=('specify a format for the input and output'),
)
formatting.add_argument(
'--filter',
dest='conf.filter',
default='.',
metavar='TEXT',
help=(
'specify an output filter (only valid with jq or human format)'
),
help=('specify an output filter (only valid with jq or human format)'),
)
formatting.add_argument(
'--conf.color',
metavar='BOOLEAN',
help='Display colorized output. Defaults to True',
default=env.get('TOWER_COLOR', 't'), type=strtobool,
default=env.get('TOWER_COLOR', 't'),
type=strtobool,
)
formatting.add_argument(
'-v',
@@ -73,7 +70,7 @@ def add_output_formatting_arguments(parser, env):
dest='conf.verbose',
help='print debug-level logs, including requests made',
default=strtobool(env.get('TOWER_VERBOSE', 'f')),
action="store_true"
action="store_true",
)
@@ -105,11 +102,10 @@ def format_jq(output, fmt):
if fmt == '.':
return output
raise ImportError(
'To use `-f jq`, you must install the optional jq dependency.\n'
'`pip install jq`\n',
'To use `-f jq`, you must install the optional jq dependency.\n' '`pip install jq`\n',
'Note that some platforms may require additional programs to '
'build jq from source (like `libtool`).\n'
'See https://pypi.org/project/jq/ for instructions.'
'See https://pypi.org/project/jq/ for instructions.',
)
results = []
for x in jq.jq(fmt).transform(output, multiple_output=True):
@@ -127,11 +123,7 @@ def format_json(output, fmt):
def format_yaml(output, fmt):
output = json.loads(json.dumps(output))
return yaml.safe_dump(
output,
default_flow_style=False,
allow_unicode=True
)
return yaml.safe_dump(output, default_flow_style=False, allow_unicode=True)
def format_human(output, fmt):
@@ -151,10 +143,7 @@ def format_human(output, fmt):
column_names.remove(k)
table = [column_names]
table.extend([
[record.get(col, '') for col in column_names]
for record in output
])
table.extend([[record.get(col, '') for col in column_names] for record in output])
col_paddings = []
def format_num(v):
@@ -184,9 +173,4 @@ def format_human(output, fmt):
return '\n'.join(lines)
FORMATTERS = {
'json': format_json,
'yaml': format_yaml,
'jq': format_jq,
'human': format_human
}
FORMATTERS = {'json': format_json, 'yaml': format_yaml, 'jq': format_jq, 'human': format_human}

View File

@@ -21,10 +21,7 @@ UNIQUENESS_RULES = {
def pk_or_name_list(v2, model_name, value, page=None):
return [
pk_or_name(v2, model_name, v.strip(), page=page)
for v in value.split(',')
]
return [pk_or_name(v2, model_name, v.strip(), page=page) for v in value.split(',')]
def pk_or_name(v2, model_name, value, page=None):
@@ -58,17 +55,9 @@ def pk_or_name(v2, model_name, value, page=None):
return int(results.results[0].id)
if results.count > 1:
raise argparse.ArgumentTypeError(
'Multiple {0} exist with that {1}. '
'To look up an ID, run:\n'
'awx {0} list --{1} "{2}" -f human'.format(
model_name, identity, value
)
'Multiple {0} exist with that {1}. ' 'To look up an ID, run:\n' 'awx {0} list --{1} "{2}" -f human'.format(model_name, identity, value)
)
raise argparse.ArgumentTypeError(
'Could not find any {0} with that {1}.'.format(
model_name, identity
)
)
raise argparse.ArgumentTypeError('Could not find any {0} with that {1}.'.format(model_name, identity))
return value
@@ -90,9 +79,7 @@ class ResourceOptionsParser(object):
self.page = page
self.resource = resource
self.parser = parser
self.options = getattr(
self.page.options().json, 'actions', {'GET': {}}
)
self.options = getattr(self.page.options().json, 'actions', {'GET': {}})
self.get_allowed_options()
if self.resource != 'settings':
# /api/v2/settings is a special resource that doesn't have
@@ -103,9 +90,7 @@ class ResourceOptionsParser(object):
self.handle_custom_actions()
def get_allowed_options(self):
options = self.page.connection.options(
self.page.endpoint + '1/'
)
options = self.page.connection.options(self.page.endpoint + '1/')
warning = options.headers.get('Warning', '')
if '299' in warning and 'deprecated' in warning:
self.deprecated = True
@@ -121,11 +106,10 @@ class ResourceOptionsParser(object):
parser = self.parser.add_parser(method, help='')
if method == 'list':
parser.add_argument(
'--all', dest='all_pages', action='store_true',
help=(
'fetch all pages of content from the API when '
'returning results (instead of just the first page)'
)
'--all',
dest='all_pages',
action='store_true',
help=('fetch all pages of content from the API when ' 'returning results (instead of just the first page)'),
)
add_output_formatting_arguments(parser, {})
@@ -138,9 +122,7 @@ class ResourceOptionsParser(object):
for method in allowed:
parser = self.parser.add_parser(method, help='')
self.parser.choices[method].add_argument(
'id',
type=functools.partial(pk_or_name, self.v2, self.resource),
help='the ID (or unique name) of the resource'
'id', type=functools.partial(pk_or_name, self.v2, self.resource), help='the ID (or unique name) of the resource'
)
if method == 'get':
add_output_formatting_arguments(parser, {})
@@ -148,10 +130,7 @@ class ResourceOptionsParser(object):
def build_query_arguments(self, method, http_method):
required_group = None
for k, param in self.options.get(http_method, {}).items():
required = (
method == 'create' and
param.get('required', False) is True
)
required = method == 'create' and param.get('required', False) is True
help_text = param.get('help_text', '')
if method == 'list':
@@ -159,10 +138,7 @@ class ResourceOptionsParser(object):
# don't allow `awx <resource> list` to filter on `--id`
# it's weird, and that's what awx <resource> get is for
continue
help_text = 'only list {} with the specified {}'.format(
self.resource,
k
)
help_text = 'only list {} with the specified {}'.format(self.resource, k)
if method == 'list' and param.get('filterable') is False:
continue
@@ -256,9 +232,8 @@ class ResourceOptionsParser(object):
# unlike *other* actual JSON fields in the API, inventory and JT
# variables *actually* want json.dumps() strings (ugh)
# see: https://github.com/ansible/awx/issues/2371
if (
(self.resource in ('job_templates', 'workflow_job_templates') and k == 'extra_vars') or
(self.resource in ('inventory', 'groups', 'hosts') and k == 'variables')
if (self.resource in ('job_templates', 'workflow_job_templates') and k == 'extra_vars') or (
self.resource in ('inventory', 'groups', 'hosts') and k == 'variables'
):
kwargs['type'] = jsonstr
@@ -267,15 +242,9 @@ class ResourceOptionsParser(object):
required_group = self.parser.choices[method].add_argument_group('required arguments')
# put the required group first (before the optional args group)
self.parser.choices[method]._action_groups.reverse()
required_group.add_argument(
'--{}'.format(k),
**kwargs
)
required_group.add_argument('--{}'.format(k), **kwargs)
else:
self.parser.choices[method].add_argument(
'--{}'.format(k),
**kwargs
)
self.parser.choices[method].add_argument('--{}'.format(k), **kwargs)
def handle_custom_actions(self):
for _, action in CustomAction.registry.items():

View File

@@ -40,11 +40,9 @@ DEPRECATED_RESOURCES = {
'teams': 'team',
'workflow_job_templates': 'workflow',
'workflow_jobs': 'workflow_job',
'users': 'user'
'users': 'user',
}
DEPRECATED_RESOURCES_REVERSE = dict(
(v, k) for k, v in DEPRECATED_RESOURCES.items()
)
DEPRECATED_RESOURCES_REVERSE = dict((v, k) for k, v in DEPRECATED_RESOURCES.items())
class CustomCommand(metaclass=CustomRegistryMeta):
@@ -81,9 +79,7 @@ class Login(CustomCommand):
auth.add_argument('--description', help='description of the generated OAuth2.0 token', metavar='TEXT')
auth.add_argument('--conf.client_id', metavar='TEXT')
auth.add_argument('--conf.client_secret', metavar='TEXT')
auth.add_argument(
'--conf.scope', choices=['read', 'write'], default='write'
)
auth.add_argument('--conf.scope', choices=['read', 'write'], default='write')
if client.help:
self.print_help(parser)
raise SystemExit()
@@ -99,10 +95,7 @@ class Login(CustomCommand):
token = api.Api().get_oauth2_token(**kwargs)
except Exception as e:
self.print_help(parser)
cprint(
'Error retrieving an OAuth2.0 token ({}).'.format(e.__class__),
'red'
)
cprint('Error retrieving an OAuth2.0 token ({}).'.format(e.__class__), 'red')
else:
fmt = client.get_config('format')
if fmt == 'human':
@@ -186,9 +179,7 @@ def parse_resource(client, skip_deprecated=False):
# check if the user is running a custom command
for command in CustomCommand.__subclasses__():
client.subparsers[command.name] = subparsers.add_parser(
command.name, help=command.help_text
)
client.subparsers[command.name] = subparsers.add_parser(command.name, help=command.help_text)
if hasattr(client, 'v2'):
for k in client.v2.json.keys():
@@ -202,15 +193,11 @@ def parse_resource(client, skip_deprecated=False):
if k in DEPRECATED_RESOURCES:
kwargs['aliases'] = [DEPRECATED_RESOURCES[k]]
client.subparsers[k] = subparsers.add_parser(
k, help='', **kwargs
)
client.subparsers[k] = subparsers.add_parser(k, help='', **kwargs)
resource = client.parser.parse_known_args()[0].resource
if resource in DEPRECATED_RESOURCES.values():
client.argv[
client.argv.index(resource)
] = DEPRECATED_RESOURCES_REVERSE[resource]
client.argv[client.argv.index(resource)] = DEPRECATED_RESOURCES_REVERSE[resource]
resource = DEPRECATED_RESOURCES_REVERSE[resource]
if resource in CustomCommand.registry:
@@ -219,27 +206,14 @@ def parse_resource(client, skip_deprecated=False):
response = command.handle(client, parser)
if response:
_filter = client.get_config('filter')
if (
resource == 'config' and
client.get_config('format') == 'human'
):
response = {
'count': len(response),
'results': [
{'key': k, 'value': v}
for k, v in response.items()
]
}
if resource == 'config' and client.get_config('format') == 'human':
response = {'count': len(response), 'results': [{'key': k, 'value': v} for k, v in response.items()]}
_filter = 'key, value'
try:
connection = client.root.connection
except AttributeError:
connection = None
formatted = format_response(
Page.from_json(response, connection=connection),
fmt=client.get_config('format'),
filter=_filter
)
formatted = format_response(Page.from_json(response, connection=connection), fmt=client.get_config('format'), filter=_filter)
print(formatted)
raise SystemExit()
else:

View File

@@ -8,7 +8,6 @@ from .resource import is_control_resource, CustomCommand
class CustomAutoprogramDirective(AutoprogramDirective):
def run(self):
nodes = super(CustomAutoprogramDirective, self).run()
@@ -23,12 +22,7 @@ class CustomAutoprogramDirective(AutoprogramDirective):
nodes[0][0].children = [heading]
# add a descriptive top synopsis of the reference guide
nodes[0].children.insert(1, paragraph(
text=(
'This is an exhaustive guide of every available command in '
'the awx CLI tool.'
)
))
nodes[0].children.insert(1, paragraph(text=('This is an exhaustive guide of every available command in ' 'the awx CLI tool.')))
disclaimer = (
'The commands and parameters documented here can (and will) '
'vary based on a variety of factors, such as the AWX API '
@@ -51,9 +45,7 @@ def render():
# Sphinx document from.
for e in ('TOWER_HOST', 'TOWER_USERNAME', 'TOWER_PASSWORD'):
if not os.environ.get(e):
raise SystemExit(
'Please specify a valid {} for a real (running) Tower install.'.format(e) # noqa
)
raise SystemExit('Please specify a valid {} for a real (running) Tower install.'.format(e)) # noqa
cli = CLI()
cli.parse_args(['awx', '--help'])
cli.connect()

View File

@@ -9,8 +9,7 @@ from .utils import cprint, color_enabled, STATUS_COLORS
from awxkit.utils import to_str
def monitor_workflow(response, session, print_stdout=True, timeout=None,
interval=.25):
def monitor_workflow(response, session, print_stdout=True, timeout=None, interval=0.25):
get = response.url.get
payload = {
'order_by': 'finished',
@@ -18,9 +17,7 @@ def monitor_workflow(response, session, print_stdout=True, timeout=None,
}
def fetch(seen):
results = response.connection.get(
'/api/v2/unified_jobs', payload
).json()['results']
results = response.connection.get('/api/v2/unified_jobs', payload).json()['results']
# erase lines we've previously printed
if print_stdout and sys.stdout.isatty():
@@ -61,7 +58,7 @@ def monitor_workflow(response, session, print_stdout=True, timeout=None,
# all at the end
fetch(seen)
time.sleep(.25)
time.sleep(0.25)
json = get().json
if json.finished:
fetch(seen)
@@ -71,7 +68,7 @@ def monitor_workflow(response, session, print_stdout=True, timeout=None,
return get().json.status
def monitor(response, session, print_stdout=True, timeout=None, interval=.25):
def monitor(response, session, print_stdout=True, timeout=None, interval=0.25):
get = response.url.get
payload = {'order_by': 'start_line', 'no_truncate': True}
if response.type == 'job':
@@ -108,12 +105,9 @@ def monitor(response, session, print_stdout=True, timeout=None, interval=.25):
if next_line:
payload['start_line__gte'] = next_line
time.sleep(.25)
time.sleep(0.25)
json = get().json
if (
json.event_processing_finished is True or
json.status in ('error', 'canceled')
):
if json.event_processing_finished is True or json.status in ('error', 'canceled'):
fetch(next_line)
break
if print_stdout:

View File

@@ -9,8 +9,7 @@ _color = threading.local()
_color.enabled = True
__all__ = ['CustomRegistryMeta', 'HelpfulArgumentParser', 'disable_color',
'color_enabled', 'colored', 'cprint', 'STATUS_COLORS']
__all__ = ['CustomRegistryMeta', 'HelpfulArgumentParser', 'disable_color', 'color_enabled', 'colored', 'cprint', 'STATUS_COLORS']
STATUS_COLORS = {
@@ -25,17 +24,12 @@ STATUS_COLORS = {
class CustomRegistryMeta(type):
@property
def registry(cls):
return dict(
(command.name, command)
for command in cls.__subclasses__()
)
return dict((command.name, command) for command in cls.__subclasses__())
class HelpfulArgumentParser(ArgumentParser):
def error(self, message): # pragma: nocover
"""Prints a usage message incorporating the message to stderr and
exits.
@@ -67,10 +61,16 @@ COLORS = dict(
list(
zip(
[
'grey', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan',
'grey',
'red',
'green',
'yellow',
'blue',
'magenta',
'cyan',
'white',
],
list(range(30, 38))
list(range(30, 38)),
)
)
)

View File

@@ -1,6 +1,4 @@
class Common(Exception):
def __init__(self, status_string='', message=''):
if isinstance(status_string, Exception):
self.status_string = ''

View File

@@ -17,35 +17,26 @@ def parse_args():
parser.add_argument(
'--base-url',
dest='base_url',
default=os.getenv(
'AWXKIT_BASE_URL',
'http://127.0.0.1:8013'),
help='URL for AWX. Defaults to env var AWXKIT_BASE_URL or http://127.0.0.1:8013')
default=os.getenv('AWXKIT_BASE_URL', 'http://127.0.0.1:8013'),
help='URL for AWX. Defaults to env var AWXKIT_BASE_URL or http://127.0.0.1:8013',
)
parser.add_argument(
'-c',
'--credential-file',
dest='credential_file',
default=os.getenv(
'AWXKIT_CREDENTIAL_FILE',
utils.not_provided),
default=os.getenv('AWXKIT_CREDENTIAL_FILE', utils.not_provided),
help='Path for yml credential file. If not provided or set by AWXKIT_CREDENTIAL_FILE, set '
'AWXKIT_USER and AWXKIT_USER_PASSWORD env vars for awx user credentials.')
'AWXKIT_USER and AWXKIT_USER_PASSWORD env vars for awx user credentials.',
)
parser.add_argument(
'-p',
'--project-file',
dest='project_file',
default=os.getenv(
'AWXKIT_PROJECT_FILE'),
help='Path for yml project config file.'
'If not provided or set by AWXKIT_PROJECT_FILE, projects will not have default SCM_URL')
parser.add_argument('-f', '--file', dest='akit_script', default=False,
help='akit script file to run in interactive session.')
parser.add_argument(
'-x',
'--non-interactive',
action='store_true',
dest='non_interactive',
help='Do not run in interactive mode.')
default=os.getenv('AWXKIT_PROJECT_FILE'),
help='Path for yml project config file.' 'If not provided or set by AWXKIT_PROJECT_FILE, projects will not have default SCM_URL',
)
parser.add_argument('-f', '--file', dest='akit_script', default=False, help='akit script file to run in interactive session.')
parser.add_argument('-x', '--non-interactive', action='store_true', dest='non_interactive', help='Do not run in interactive mode.')
return parser.parse_known_args()[0]
@@ -57,19 +48,14 @@ def main():
config.base_url = akit_args.base_url
if akit_args.credential_file != utils.not_provided:
config.credentials = utils.load_credentials(
akit_args.credential_file)
config.credentials = utils.load_credentials(akit_args.credential_file)
else:
config.credentials = utils.PseudoNamespace({
'default': {
'username': os.getenv('AWXKIT_USER', 'admin'),
'password': os.getenv('AWXKIT_USER_PASSWORD', 'password')
}
})
config.credentials = utils.PseudoNamespace(
{'default': {'username': os.getenv('AWXKIT_USER', 'admin'), 'password': os.getenv('AWXKIT_USER_PASSWORD', 'password')}}
)
if akit_args.project_file != utils.not_provided:
config.project_urls = utils.load_projects(
akit_args.project_file)
config.project_urls = utils.load_projects(akit_args.project_file)
global root
root = api.Api()
@@ -106,6 +92,7 @@ def load_interactive():
try:
from IPython import start_ipython
basic_session_path = os.path.abspath(__file__)
if basic_session_path[-1] == 'c': # start_ipython doesn't work w/ .pyc
basic_session_path = basic_session_path[:-1]
@@ -115,6 +102,7 @@ def load_interactive():
return start_ipython(argv=sargs)
except ImportError:
from code import interact
main()
interact('', local=dict(globals(), **locals()))

View File

@@ -34,7 +34,8 @@ cloud_types = (
'rhv',
'satellite6',
'tower',
'vmware')
'vmware',
)
credential_type_kinds = ('cloud', 'net')
not_provided = 'xx__NOT_PROVIDED__xx'
@@ -52,7 +53,6 @@ class NoReloadError(Exception):
class PseudoNamespace(dict):
def __init__(self, _d=None, **loaded):
if not isinstance(_d, dict):
_d = {}
@@ -79,9 +79,7 @@ class PseudoNamespace(dict):
try:
return self.__getitem__(attr)
except KeyError:
raise AttributeError(
"{!r} has no attribute {!r}".format(
self.__class__.__name__, attr))
raise AttributeError("{!r} has no attribute {!r}".format(self.__class__.__name__, attr))
def __setattr__(self, attr, value):
self.__setitem__(attr, value)
@@ -116,11 +114,7 @@ class PseudoNamespace(dict):
# PseudoNamespaces if applicable
def update(self, iterable=None, **kw):
if iterable:
if (hasattr(iterable,
'keys') and isinstance(iterable.keys,
(types.FunctionType,
types.BuiltinFunctionType,
types.MethodType))):
if hasattr(iterable, 'keys') and isinstance(iterable.keys, (types.FunctionType, types.BuiltinFunctionType, types.MethodType)):
for key in iterable:
self[key] = iterable[key]
else:
@@ -161,11 +155,7 @@ def filter_by_class(*item_class_tuples):
examined_item = item[0]
else:
examined_item = item
if is_class_or_instance(
examined_item,
cls) or is_proper_subclass(
examined_item,
cls):
if is_class_or_instance(examined_item, cls) or is_proper_subclass(examined_item, cls):
results.append(item)
else:
updated = (cls, item[1]) if was_tuple else cls
@@ -249,7 +239,7 @@ def gen_utf_char():
is_char = False
b = 'b'
while not is_char:
b = random.randint(32, 0x10ffff)
b = random.randint(32, 0x10FFFF)
is_char = chr(b).isprintable()
return chr(b)
@@ -266,20 +256,12 @@ def random_ipv4():
def random_ipv6():
"""Generates a random ipv6 address;; useful for testing."""
return ':'.join(
'{0:x}'.format(
random.randint(
0,
2 ** 16 -
1)) for i in range(8))
return ':'.join('{0:x}'.format(random.randint(0, 2 ** 16 - 1)) for i in range(8))
def random_loopback_ip():
"""Generates a random loopback ipv4 address;; useful for testing."""
return "127.{}.{}.{}".format(
random_int(255),
random_int(255),
random_int(255))
return "127.{}.{}.{}".format(random_int(255), random_int(255), random_int(255))
def random_utf8(*args, **kwargs):
@@ -289,8 +271,7 @@ def random_utf8(*args, **kwargs):
"""
pattern = re.compile('[^\u0000-\uD7FF\uE000-\uFFFF]', re.UNICODE)
length = args[0] if len(args) else kwargs.get('length', 10)
scrubbed = pattern.sub('\uFFFD', ''.join(
[gen_utf_char() for _ in range(length)]))
scrubbed = pattern.sub('\uFFFD', ''.join([gen_utf_char() for _ in range(length)]))
return scrubbed
@@ -374,8 +355,10 @@ def is_proper_subclass(obj, cls):
def are_same_endpoint(first, second):
"""Equivalence check of two urls, stripped of query parameters"""
def strip(url):
return url.replace('www.', '').split('?')[0]
return strip(first) == strip(second)
@@ -421,10 +404,7 @@ class UTC(tzinfo):
return timedelta(0)
def seconds_since_date_string(
date_str,
fmt='%Y-%m-%dT%H:%M:%S.%fZ',
default_tz=UTC()):
def seconds_since_date_string(date_str, fmt='%Y-%m-%dT%H:%M:%S.%fZ', default_tz=UTC()):
"""Return the number of seconds since the date and time indicated by a date
string and its corresponding format string.

View File

@@ -42,18 +42,19 @@ class CircularDependencyError(ValueError):
def __init__(self, data):
# Sort the data just to make the output consistent, for use in
# error messages. That's convenient for doctests.
s = 'Circular dependencies exist among these items: {{{}}}'.format(', '.join('{!r}:{!r}'.format(key, value) for key, value in sorted(data.items()))) # noqa
s = 'Circular dependencies exist among these items: {{{}}}'.format(
', '.join('{!r}:{!r}'.format(key, value) for key, value in sorted(data.items()))
) # noqa
super(CircularDependencyError, self).__init__(s)
self.data = data
def toposort(data):
"""Dependencies are expressed as a dictionary whose keys are items
and whose values are a set of dependent items. Output is a list of
sets in topological order. The first set consists of items with no
dependences, each subsequent set consists of items that depend upon
items in the preceeding sets.
"""
and whose values are a set of dependent items. Output is a list of
sets in topological order. The first set consists of items with no
dependences, each subsequent set consists of items that depend upon
items in the preceeding sets."""
# Special case empty input.
if len(data) == 0:
@@ -74,9 +75,6 @@ items in the preceeding sets.
if not ordered:
break
yield ordered
data = {
item: (dep - ordered)
for item, dep in data.items() if item not in ordered
}
data = {item: (dep - ordered) for item, dep in data.items() if item not in ordered}
if len(data) != 0:
raise CircularDependencyError(data)

File diff suppressed because it is too large Load Diff

View File

@@ -84,12 +84,9 @@ class WSClient(object):
auth_cookie = ''
pref = 'wss://' if self._use_ssl else 'ws://'
url = '{0}{1.hostname}:{1.port}/websocket/'.format(pref, self)
self.ws = websocket.WebSocketApp(url,
on_open=self._on_open,
on_message=self._on_message,
on_error=self._on_error,
on_close=self._on_close,
cookie=auth_cookie)
self.ws = websocket.WebSocketApp(
url, on_open=self._on_open, on_message=self._on_message, on_error=self._on_error, on_close=self._on_close, cookie=auth_cookie
)
self._message_cache = []
self._should_subscribe_to_pending_job = False
self._pending_unsubscribe = threading.Event()
@@ -199,12 +196,8 @@ class WSClient(object):
message = json.loads(message)
log.debug('received message: {}'.format(message))
if all([message.get('group_name') == 'jobs',
message.get('status') == 'pending',
message.get('unified_job_id'),
self._should_subscribe_to_pending_job]):
if bool(message.get('project_id')) == (
self._should_subscribe_to_pending_job['events'] == 'project_update_events'):
if all([message.get('group_name') == 'jobs', message.get('status') == 'pending', message.get('unified_job_id'), self._should_subscribe_to_pending_job]):
if bool(message.get('project_id')) == (self._should_subscribe_to_pending_job['events'] == 'project_update_events'):
self._update_subscription(message['unified_job_id'])
ret = self._recv_queue.put(message)

View File

@@ -12,7 +12,6 @@ file_path_cache = {}
class Loader(yaml.SafeLoader):
def __init__(self, stream):
self._root = os.path.split(stream.name)[0]
super(Loader, self).__init__(stream)
@@ -82,6 +81,7 @@ def load_file(filename):
random_thing: "{random_string:24}"
"""
from py.path import local
if filename is None:
this_file = os.path.abspath(__file__)
path = local(this_file).new(basename='../data.yaml')

View File

@@ -68,11 +68,7 @@ setup(
'requests',
],
python_requires=">=3.6",
extras_require={
'formatting': ['jq'],
'websockets': ['websocket-client==0.57.0'],
'crypto': ['cryptography']
},
extras_require={'formatting': ['jq'], 'websockets': ['websocket-client==0.57.0'], 'crypto': ['cryptography']},
license='Apache 2.0',
classifiers=[
'Development Status :: 5 - Production/Stable',
@@ -87,10 +83,5 @@ setup(
'Topic :: System :: Software Distribution',
'Topic :: System :: Systems Administration',
],
entry_points={
'console_scripts': [
'akit=awxkit.scripts.basic_session:load_interactive',
'awx=awxkit.cli:run'
]
}
entry_points={'console_scripts': ['akit=awxkit.scripts.basic_session:load_interactive', 'awx=awxkit.cli:run']},
)

View File

@@ -7,7 +7,6 @@ from awxkit.cli import run, CLI
class MockedCLI(CLI):
def fetch_version_root(self):
pass
@@ -17,9 +16,7 @@ class MockedCLI(CLI):
@property
def json(self):
return {
'users': None
}
return {'users': None}
@pytest.mark.parametrize('help_param', ['-h', '--help'])
@@ -29,10 +26,7 @@ def test_help(capfd, help_param):
out, err = capfd.readouterr()
assert "usage:" in out
for snippet in (
'--conf.host https://example.awx.org]',
'-v, --verbose'
):
for snippet in ('--conf.host https://example.awx.org]', '-v, --verbose'):
assert snippet in out
@@ -59,8 +53,5 @@ def test_list_resources(capfd, resource):
_, out = capfd.readouterr()
assert "usage:" in out
for snippet in (
'--conf.host https://example.awx.org]',
'-v, --verbose'
):
for snippet in ('--conf.host https://example.awx.org]', '-v, --verbose'):
assert snippet in out

View File

@@ -4,16 +4,15 @@ from requests.exceptions import ConnectionError
from awxkit.cli import CLI
from awxkit import config
def test_host_from_environment():
cli = CLI()
cli.parse_args(
['awx'],
env={'TOWER_HOST': 'https://xyz.local'}
)
cli.parse_args(['awx'], env={'TOWER_HOST': 'https://xyz.local'})
with pytest.raises(ConnectionError):
cli.connect()
assert config.base_url == 'https://xyz.local'
def test_host_from_argv():
cli = CLI()
cli.parse_args(['awx', '--conf.host', 'https://xyz.local'])
@@ -21,43 +20,30 @@ def test_host_from_argv():
cli.connect()
assert config.base_url == 'https://xyz.local'
def test_username_and_password_from_environment():
cli = CLI()
cli.parse_args(
['awx'],
env={
'TOWER_USERNAME': 'mary',
'TOWER_PASSWORD': 'secret'
}
)
cli.parse_args(['awx'], env={'TOWER_USERNAME': 'mary', 'TOWER_PASSWORD': 'secret'})
with pytest.raises(ConnectionError):
cli.connect()
assert config.credentials.default.username == 'mary'
assert config.credentials.default.password == 'secret'
def test_username_and_password_argv():
cli = CLI()
cli.parse_args([
'awx', '--conf.username', 'mary', '--conf.password', 'secret'
])
cli.parse_args(['awx', '--conf.username', 'mary', '--conf.password', 'secret'])
with pytest.raises(ConnectionError):
cli.connect()
assert config.credentials.default.username == 'mary'
assert config.credentials.default.password == 'secret'
def test_config_precedence():
cli = CLI()
cli.parse_args(
[
'awx', '--conf.username', 'mary', '--conf.password', 'secret'
],
env={
'TOWER_USERNAME': 'IGNORE',
'TOWER_PASSWORD': 'IGNORE'
}
)
cli.parse_args(['awx', '--conf.username', 'mary', '--conf.password', 'secret'], env={'TOWER_USERNAME': 'IGNORE', 'TOWER_PASSWORD': 'IGNORE'})
with pytest.raises(ConnectionError):
cli.connect()

View File

@@ -11,19 +11,17 @@ from awxkit.cli.resource import Import
def test_json_empty_list():
page = Page.from_json({
'results': []
})
page = Page.from_json({'results': []})
formatted = format_response(page)
assert json.loads(formatted) == {'results': []}
def test_yaml_empty_list():
page = Page.from_json({
'results': []
})
page = Page.from_json({'results': []})
formatted = format_response(page, fmt='yaml')
assert yaml.safe_load(formatted) == {'results': []}
def test_json_list():
users = {
'results': [
@@ -36,6 +34,7 @@ def test_json_list():
formatted = format_response(page)
assert json.loads(formatted) == users
def test_yaml_list():
users = {
'results': [

View File

@@ -11,13 +11,11 @@ from awxkit.cli.options import ResourceOptionsParser
class ResourceOptionsParser(ResourceOptionsParser):
def get_allowed_options(self):
self.allowed_options = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
class OptionsPage(Page):
def options(self):
return self
@@ -33,30 +31,31 @@ class OptionsPage(Page):
class TestOptions(unittest.TestCase):
def setUp(self):
_parser = argparse.ArgumentParser()
self.parser = _parser.add_subparsers(help='action')
def test_list(self):
page = OptionsPage.from_json({
'actions': {
'GET': {},
'POST': {},
page = OptionsPage.from_json(
{
'actions': {
'GET': {},
'POST': {},
}
}
})
)
ResourceOptionsParser(None, page, 'users', self.parser)
assert 'list' in self.parser.choices
def test_list_filtering(self):
page = OptionsPage.from_json({
'actions': {
'GET': {},
'POST': {
'first_name': {'type': 'string'}
},
page = OptionsPage.from_json(
{
'actions': {
'GET': {},
'POST': {'first_name': {'type': 'string'}},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices
@@ -66,14 +65,14 @@ class TestOptions(unittest.TestCase):
assert '--first_name TEXT' in out.getvalue()
def test_list_not_filterable(self):
page = OptionsPage.from_json({
'actions': {
'GET': {},
'POST': {
'middle_name': {'type': 'string', 'filterable': False}
},
page = OptionsPage.from_json(
{
'actions': {
'GET': {},
'POST': {'middle_name': {'type': 'string', 'filterable': False}},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('list', 'POST')
assert 'list' in self.parser.choices
@@ -83,16 +82,18 @@ class TestOptions(unittest.TestCase):
assert '--middle_name' not in out.getvalue()
def test_creation_optional_argument(self):
page = OptionsPage.from_json({
'actions': {
'POST': {
'first_name': {
'type': 'string',
'help_text': 'Please specify your first name',
}
},
page = OptionsPage.from_json(
{
'actions': {
'POST': {
'first_name': {
'type': 'string',
'help_text': 'Please specify your first name',
}
},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices
@@ -102,17 +103,13 @@ class TestOptions(unittest.TestCase):
assert '--first_name TEXT Please specify your first name' in out.getvalue()
def test_creation_required_argument(self):
page = OptionsPage.from_json({
'actions': {
'POST': {
'username': {
'type': 'string',
'help_text': 'Please specify a username',
'required': True
}
},
page = OptionsPage.from_json(
{
'actions': {
'POST': {'username': {'type': 'string', 'help_text': 'Please specify a username', 'required': True}},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices
@@ -122,13 +119,13 @@ class TestOptions(unittest.TestCase):
assert '--username TEXT Please specify a username'
def test_integer_argument(self):
page = OptionsPage.from_json({
'actions': {
'POST': {
'max_hosts': {'type': 'integer'}
},
page = OptionsPage.from_json(
{
'actions': {
'POST': {'max_hosts': {'type': 'integer'}},
}
}
})
)
options = ResourceOptionsParser(None, page, 'organizations', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices
@@ -138,13 +135,13 @@ class TestOptions(unittest.TestCase):
assert '--max_hosts INTEGER' in out.getvalue()
def test_boolean_argument(self):
page = OptionsPage.from_json({
'actions': {
'POST': {
'diff_mode': {'type': 'boolean'}
},
page = OptionsPage.from_json(
{
'actions': {
'POST': {'diff_mode': {'type': 'boolean'}},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices
@@ -154,23 +151,25 @@ class TestOptions(unittest.TestCase):
assert '--diff_mode BOOLEAN' in out.getvalue()
def test_choices(self):
page = OptionsPage.from_json({
'actions': {
'POST': {
'verbosity': {
'type': 'integer',
'choices': [
(0, '0 (Normal)'),
(1, '1 (Verbose)'),
(2, '2 (More Verbose)'),
(3, '3 (Debug)'),
(4, '4 (Connection Debug)'),
(5, '5 (WinRM Debug)'),
]
}
},
page = OptionsPage.from_json(
{
'actions': {
'POST': {
'verbosity': {
'type': 'integer',
'choices': [
(0, '0 (Normal)'),
(1, '1 (Verbose)'),
(2, '2 (More Verbose)'),
(3, '3 (Debug)'),
(4, '4 (Connection Debug)'),
(5, '5 (WinRM Debug)'),
],
}
},
}
}
})
)
options = ResourceOptionsParser(None, page, 'users', self.parser)
options.build_query_arguments('create', 'POST')
assert 'create' in self.parser.choices
@@ -181,9 +180,7 @@ class TestOptions(unittest.TestCase):
def test_actions_with_primary_key(self):
for method in ('get', 'modify', 'delete'):
page = OptionsPage.from_json({
'actions': {'GET': {}, 'POST': {}}
})
page = OptionsPage.from_json({'actions': {'GET': {}, 'POST': {}}})
ResourceOptionsParser(None, page, 'jobs', self.parser)
assert method in self.parser.choices
@@ -193,19 +190,20 @@ class TestOptions(unittest.TestCase):
class TestSettingsOptions(unittest.TestCase):
def setUp(self):
_parser = argparse.ArgumentParser()
self.parser = _parser.add_subparsers(help='action')
def test_list(self):
page = OptionsPage.from_json({
'actions': {
'GET': {},
'POST': {},
'PUT': {},
page = OptionsPage.from_json(
{
'actions': {
'GET': {},
'POST': {},
'PUT': {},
}
}
})
)
page.endpoint = '/settings/all/'
ResourceOptionsParser(None, page, 'settings', self.parser)
assert 'list' in self.parser.choices

View File

@@ -14,32 +14,39 @@ def set_config_cred_to_desired(config, location):
config_ref = config_ref[_location]
setattr(config_ref, split[-1], 'desired')
class MockCredentialType(object):
class MockCredentialType(object):
def __init__(self, name, kind, managed_by_tower=True):
self.name = name
self.kind = kind
self.managed_by_tower = managed_by_tower
@pytest.mark.parametrize('field, kind, config_cred, desired_field, desired_value',
[('field', 'ssh', PseudoNamespace(field=123), 'field', 123),
('subscription', 'azure', PseudoNamespace(subscription_id=123), 'subscription', 123),
('project_id', 'gce', PseudoNamespace(project=123), 'project', 123),
('authorize_password', 'net', PseudoNamespace(authorize=123), 'authorize_password', 123)])
@pytest.mark.parametrize(
'field, kind, config_cred, desired_field, desired_value',
[
('field', 'ssh', PseudoNamespace(field=123), 'field', 123),
('subscription', 'azure', PseudoNamespace(subscription_id=123), 'subscription', 123),
('project_id', 'gce', PseudoNamespace(project=123), 'project', 123),
('authorize_password', 'net', PseudoNamespace(authorize=123), 'authorize_password', 123),
],
)
def test_get_payload_field_and_value_from_config_cred(field, kind, config_cred, desired_field, desired_value):
ret_field, ret_val = credentials.get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, {},
config_cred)
ret_field, ret_val = credentials.get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, {}, config_cred)
assert ret_field == desired_field
assert ret_val == desired_value
@pytest.mark.parametrize('field, kind, kwargs, desired_field, desired_value',
[('field', 'ssh', dict(field=123), 'field', 123),
('subscription', 'azure', dict(subscription=123), 'subscription', 123),
('project_id', 'gce', dict(project_id=123), 'project', 123),
('authorize_password', 'net', dict(authorize_password=123), 'authorize_password', 123)])
@pytest.mark.parametrize(
'field, kind, kwargs, desired_field, desired_value',
[
('field', 'ssh', dict(field=123), 'field', 123),
('subscription', 'azure', dict(subscription=123), 'subscription', 123),
('project_id', 'gce', dict(project_id=123), 'project', 123),
('authorize_password', 'net', dict(authorize_password=123), 'authorize_password', 123),
],
)
def test_get_payload_field_and_value_from_kwarg(field, kind, kwargs, desired_field, desired_value):
ret_field, ret_val = credentials.get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, kwargs,
PseudoNamespace())
ret_field, ret_val = credentials.get_payload_field_and_value_from_kwargs_or_config_cred(field, kind, kwargs, PseudoNamespace())
assert ret_field == desired_field
assert ret_val == desired_value

View File

@@ -21,7 +21,6 @@ class MockHasCreate(has_create.HasCreate):
class A(MockHasCreate):
def create(self, **kw):
return self
@@ -87,13 +86,12 @@ class H(MockHasCreate):
optional_dependencies = [E, A]
def create(self, a=None, e=None, **kw):
def create(self, a=None, e=None, **kw):
self.create_and_update_dependencies(*filter_by_class((a, A), (e, E)))
return self
class MultipleWordClassName(MockHasCreate):
def create(self, **kw):
return self
@@ -102,7 +100,7 @@ class AnotherMultipleWordClassName(MockHasCreate):
optional_dependencies = [MultipleWordClassName]
def create(self, multiple_word_class_name=None, **kw):
def create(self, multiple_word_class_name=None, **kw):
self.create_and_update_dependencies(*filter_by_class((multiple_word_class_name, MultipleWordClassName)))
return self
@@ -183,19 +181,17 @@ def test_optional_dependency_graph_with_additional():
def test_creation_order():
"""confirms that `has_create.creation_order()` returns a valid creation order in the desired list of sets format"""
dependency_graph = dict(eight=set(['seven', 'six']),
seven=set(['five']),
six=set(),
five=set(['two', 'one']),
four=set(['one']),
three=set(['two']),
two=set(['one']),
one=set())
desired = [set(['one', 'six']),
set(['two', 'four']),
set(['three', 'five']),
set(['seven']),
set(['eight'])]
dependency_graph = dict(
eight=set(['seven', 'six']),
seven=set(['five']),
six=set(),
five=set(['two', 'one']),
four=set(['one']),
three=set(['two']),
two=set(['one']),
one=set(),
)
desired = [set(['one', 'six']), set(['two', 'four']), set(['three', 'five']), set(['seven']), set(['eight'])]
assert has_create.creation_order(dependency_graph) == desired
@@ -203,14 +199,16 @@ def test_creation_order_with_loop():
"""confirms that `has_create.creation_order()` raises toposort.CircularDependencyError when evaluating
a cyclic dependency graph
"""
dependency_graph = dict(eight=set(['seven', 'six']),
seven=set(['five']),
six=set(),
five=set(['two', 'one']),
four=set(['one']),
three=set(['two']),
two=set(['one']),
one=set(['eight']))
dependency_graph = dict(
eight=set(['seven', 'six']),
seven=set(['five']),
six=set(),
five=set(['two', 'one']),
four=set(['one']),
three=set(['two']),
two=set(['one']),
one=set(['eight']),
)
with pytest.raises(CircularDependencyError):
assert has_create.creation_order(dependency_graph)
@@ -239,9 +237,11 @@ class Five(MockHasCreate):
class IsntAHasCreate(object):
pass
class Six(MockHasCreate, IsntAHasCreate):
dependencies = [Two]
class Seven(MockHasCreate):
dependencies = [IsntAHasCreate]
@@ -265,8 +265,7 @@ def test_separate_async_optionals_three_exist():
the class that has shared item as a dependency occurs first in a separate creation group
"""
order = has_create.creation_order(has_create.optional_dependency_graph(Five, Four, Three))
assert has_create.separate_async_optionals(order) == [set([One]), set([Two]), set([Three]),
set([Five]), set([Four])]
assert has_create.separate_async_optionals(order) == [set([One]), set([Two]), set([Three]), set([Five]), set([Four])]
def test_separate_async_optionals_not_has_create():
@@ -345,8 +344,7 @@ def test_dependency_resolution_complete():
for item in (h, a, e, d, c, b):
if item._dependency_store:
assert all(item._dependency_store.values()
), "{0} missing dependency: {0._dependency_store}".format(item)
assert all(item._dependency_store.values()), "{0} missing dependency: {0._dependency_store}".format(item)
assert a == b._dependency_store[A], "Duplicate dependency detected"
assert a == c._dependency_store[A], "Duplicate dependency detected"
@@ -468,7 +466,6 @@ def test_teardown_ds_cleared():
class OneWithArgs(MockHasCreate):
def create(self, **kw):
self.kw = kw
return self
@@ -492,18 +489,17 @@ class ThreeWithArgs(MockHasCreate):
optional_dependencies = [TwoWithArgs]
def create(self, one_with_args=OneWithArgs, two_with_args=None, **kw):
self.create_and_update_dependencies(*filter_by_class((one_with_args, OneWithArgs),
(two_with_args, TwoWithArgs)))
self.create_and_update_dependencies(*filter_by_class((one_with_args, OneWithArgs), (two_with_args, TwoWithArgs)))
self.kw = kw
return self
class FourWithArgs(MockHasCreate):
dependencies = [TwoWithArgs, ThreeWithArgs]
def create(self, two_with_args=TwoWithArgs, three_with_args=ThreeWithArgs, **kw):
self.create_and_update_dependencies(*filter_by_class((two_with_args, TwoWithArgs),
(three_with_args, ThreeWithArgs)))
self.create_and_update_dependencies(*filter_by_class((two_with_args, TwoWithArgs), (three_with_args, ThreeWithArgs)))
self.kw = kw
return self
@@ -536,10 +532,9 @@ def test_no_tuple_for_class_arg_causes_shared_dependencies_nested_staggering():
def test_tuple_for_class_arg_causes_unshared_dependencies_when_downstream():
"""Confirms that provided arg-tuple for dependency type is applied instead of chained dependency"""
three_wa = ThreeWithArgs().create(two_with_args=(TwoWithArgs, dict(one_with_args=False,
make_one_with_args=True,
two_with_args_kw_arg=234)),
three_with_args_kw_arg=345)
three_wa = ThreeWithArgs().create(
two_with_args=(TwoWithArgs, dict(one_with_args=False, make_one_with_args=True, two_with_args_kw_arg=234)), three_with_args_kw_arg=345
)
assert isinstance(three_wa.ds.one_with_args, OneWithArgs)
assert isinstance(three_wa.ds.two_with_args, TwoWithArgs)
assert isinstance(three_wa.ds.two_with_args.ds.one_with_args, OneWithArgs)
@@ -552,13 +547,12 @@ def test_tuple_for_class_arg_causes_unshared_dependencies_when_downstream():
def test_tuples_for_class_arg_cause_unshared_dependencies_when_downstream():
"""Confirms that provided arg-tuple for dependency type is applied instead of chained dependency"""
four_wa = FourWithArgs().create(two_with_args=(TwoWithArgs, dict(one_with_args=False,
make_one_with_args=True,
two_with_args_kw_arg=456)),
# No shared dependencies with four_wa.ds.two_with_args
three_with_args=(ThreeWithArgs, dict(one_with_args=(OneWithArgs, {}),
two_with_args=False)),
four_with_args_kw=567)
four_wa = FourWithArgs().create(
two_with_args=(TwoWithArgs, dict(one_with_args=False, make_one_with_args=True, two_with_args_kw_arg=456)),
# No shared dependencies with four_wa.ds.two_with_args
three_with_args=(ThreeWithArgs, dict(one_with_args=(OneWithArgs, {}), two_with_args=False)),
four_with_args_kw=567,
)
assert isinstance(four_wa.ds.two_with_args, TwoWithArgs)
assert isinstance(four_wa.ds.three_with_args, ThreeWithArgs)
assert isinstance(four_wa.ds.two_with_args.ds.one_with_args, OneWithArgs)
@@ -575,25 +569,21 @@ class NotHasCreate(object):
class MixinUserA(MockHasCreate, NotHasCreate):
def create(self, **kw):
return self
class MixinUserB(MockHasCreate, NotHasCreate):
def create(self, **kw):
return self
class MixinUserC(MixinUserB):
def create(self, **kw):
return self
class MixinUserD(MixinUserC):
def create(self, **kw):
return self
@@ -646,17 +636,12 @@ class DynamicallyDeclaresNotHasCreateDependency(MockHasCreate):
dependencies = [NotHasCreate]
def create(self, not_has_create=MixinUserA):
dynamic_dependency = dict(mixinusera=MixinUserA,
mixinuserb=MixinUserB,
mixinuserc=MixinUserC)
dynamic_dependency = dict(mixinusera=MixinUserA, mixinuserb=MixinUserB, mixinuserc=MixinUserC)
self.create_and_update_dependencies(dynamic_dependency[not_has_create])
return self
@pytest.mark.parametrize('dependency,dependency_class',
[('mixinusera', MixinUserA),
('mixinuserb', MixinUserB),
('mixinuserc', MixinUserC)])
@pytest.mark.parametrize('dependency,dependency_class', [('mixinusera', MixinUserA), ('mixinuserb', MixinUserB), ('mixinuserc', MixinUserC)])
def test_subclass_or_parent_dynamic_not_has_create_dependency_declaration(dependency, dependency_class):
"""Confirms that dependencies that dynamically declare dependencies subclassed from not HasCreate
are properly linked
@@ -670,17 +655,12 @@ class DynamicallyDeclaresHasCreateDependency(MockHasCreate):
dependencies = [MixinUserB]
def create(self, mixin_user_b=MixinUserB):
dynamic_dependency = dict(mixinuserb=MixinUserB,
mixinuserc=MixinUserC,
mixinuserd=MixinUserD)
dynamic_dependency = dict(mixinuserb=MixinUserB, mixinuserc=MixinUserC, mixinuserd=MixinUserD)
self.create_and_update_dependencies(dynamic_dependency[mixin_user_b])
return self
@pytest.mark.parametrize('dependency,dependency_class',
[('mixinuserb', MixinUserB),
('mixinuserc', MixinUserC),
('mixinuserd', MixinUserD)])
@pytest.mark.parametrize('dependency,dependency_class', [('mixinuserb', MixinUserB), ('mixinuserc', MixinUserC), ('mixinuserd', MixinUserD)])
def test_subclass_or_parent_dynamic_has_create_dependency_declaration(dependency, dependency_class):
"""Confirms that dependencies that dynamically declare dependencies subclassed from not HasCreate
are properly linked

View File

@@ -169,8 +169,7 @@ def test_wildcard_and_specific_method_registration_acts_as_default(reg):
def test_multiple_method_registrations_disallowed_for_single_path_single_registration(reg, method):
with pytest.raises(TypeError) as e:
reg.register((('some_path', method), ('some_path', method)), One)
assert str(e.value) == ('"{0.pattern}" already has registered method "{1}"'
.format(reg.url_pattern('some_path'), method))
assert str(e.value) == ('"{0.pattern}" already has registered method "{1}"'.format(reg.url_pattern('some_path'), method))
@pytest.mark.parametrize('method', ('method', '.*'))
@@ -178,8 +177,7 @@ def test_multiple_method_registrations_disallowed_for_single_path_multiple_regis
reg.register('some_path', method, One)
with pytest.raises(TypeError) as e:
reg.register('some_path', method, One)
assert str(e.value) == ('"{0.pattern}" already has registered method "{1}"'
.format(reg.url_pattern('some_path'), method))
assert str(e.value) == ('"{0.pattern}" already has registered method "{1}"'.format(reg.url_pattern('some_path'), method))
def test_paths_can_be_patterns(reg):
@@ -188,10 +186,9 @@ def test_paths_can_be_patterns(reg):
def test_mixed_form_single_registration(reg):
reg.register([('some_path_one', 'method_one'),
'some_path_two',
('some_path_three', ('method_two', 'method_three')),
'some_path_four', 'some_path_five'], One)
reg.register(
[('some_path_one', 'method_one'), 'some_path_two', ('some_path_three', ('method_two', 'method_three')), 'some_path_four', 'some_path_five'], One
)
assert reg.get('some_path_one', 'method_one') is One
assert reg.get('some_path_one') is None
assert reg.get('some_path_one', 'nonexistent') is None
@@ -209,10 +206,9 @@ def test_mixed_form_single_registration(reg):
def test_mixed_form_single_registration_with_methodless_default(reg):
reg.setdefault(One)
reg.register([('some_path_one', 'method_one'),
'some_path_two',
('some_path_three', ('method_two', 'method_three')),
'some_path_four', 'some_path_five'], Two)
reg.register(
[('some_path_one', 'method_one'), 'some_path_two', ('some_path_three', ('method_two', 'method_three')), 'some_path_four', 'some_path_five'], Two
)
assert reg.get('some_path_one', 'method_one') is Two
assert reg.get('some_path_one') is One
assert reg.get('some_path_one', 'nonexistent') is One
@@ -230,10 +226,9 @@ def test_mixed_form_single_registration_with_methodless_default(reg):
def test_mixed_form_single_registration_with_method_default(reg):
reg.setdefault('existent', One)
reg.register([('some_path_one', 'method_one'),
'some_path_two',
('some_path_three', ('method_two', 'method_three')),
'some_path_four', 'some_path_five'], Two)
reg.register(
[('some_path_one', 'method_one'), 'some_path_two', ('some_path_three', ('method_two', 'method_three')), 'some_path_four', 'some_path_five'], Two
)
assert reg.get('some_path_one', 'method_one') is Two
assert reg.get('some_path_one') is None
assert reg.get('some_path_one', 'existent') is One

View File

@@ -9,60 +9,68 @@ from awxkit import utils
from awxkit import exceptions as exc
@pytest.mark.parametrize('inp, out',
[[True, True],
[False, False],
[1, True],
[0, False],
[1.0, True],
[0.0, False],
['TrUe', True],
['FalSe', False],
['yEs', True],
['No', False],
['oN', True],
['oFf', False],
['asdf', True],
['0', False],
['', False],
[{1: 1}, True],
[{}, False],
[(0,), True],
[(), False],
[[1], True],
[[], False]])
@pytest.mark.parametrize(
'inp, out',
[
[True, True],
[False, False],
[1, True],
[0, False],
[1.0, True],
[0.0, False],
['TrUe', True],
['FalSe', False],
['yEs', True],
['No', False],
['oN', True],
['oFf', False],
['asdf', True],
['0', False],
['', False],
[{1: 1}, True],
[{}, False],
[(0,), True],
[(), False],
[[1], True],
[[], False],
],
)
def test_to_bool(inp, out):
assert utils.to_bool(inp) == out
@pytest.mark.parametrize('inp, out',
[["{}", {}],
["{'null': null}", {"null": None}],
["{'bool': true}", {"bool": True}],
["{'bool': false}", {"bool": False}],
["{'int': 0}", {"int": 0}],
["{'float': 1.0}", {"float": 1.0}],
["{'str': 'abc'}", {"str": "abc"}],
["{'obj': {}}", {"obj": {}}],
["{'list': []}", {"list": []}],
["---", None],
["---\n'null': null", {'null': None}],
["---\n'bool': true", {'bool': True}],
["---\n'bool': false", {'bool': False}],
["---\n'int': 0", {'int': 0}],
["---\n'float': 1.0", {'float': 1.0}],
["---\n'string': 'abc'", {'string': 'abc'}],
["---\n'obj': {}", {'obj': {}}],
["---\n'list': []", {'list': []}],
["", None],
["'null': null", {'null': None}],
["'bool': true", {'bool': True}],
["'bool': false", {'bool': False}],
["'int': 0", {'int': 0}],
["'float': 1.0", {'float': 1.0}],
["'string': 'abc'", {'string': 'abc'}],
["'obj': {}", {'obj': {}}],
["'list': []", {'list': []}]])
@pytest.mark.parametrize(
'inp, out',
[
["{}", {}],
["{'null': null}", {"null": None}],
["{'bool': true}", {"bool": True}],
["{'bool': false}", {"bool": False}],
["{'int': 0}", {"int": 0}],
["{'float': 1.0}", {"float": 1.0}],
["{'str': 'abc'}", {"str": "abc"}],
["{'obj': {}}", {"obj": {}}],
["{'list': []}", {"list": []}],
["---", None],
["---\n'null': null", {'null': None}],
["---\n'bool': true", {'bool': True}],
["---\n'bool': false", {'bool': False}],
["---\n'int': 0", {'int': 0}],
["---\n'float': 1.0", {'float': 1.0}],
["---\n'string': 'abc'", {'string': 'abc'}],
["---\n'obj': {}", {'obj': {}}],
["---\n'list': []", {'list': []}],
["", None],
["'null': null", {'null': None}],
["'bool': true", {'bool': True}],
["'bool': false", {'bool': False}],
["'int': 0", {'int': 0}],
["'float': 1.0", {'float': 1.0}],
["'string': 'abc'", {'string': 'abc'}],
["'obj': {}", {'obj': {}}],
["'list': []", {'list': []}],
],
)
def test_load_valid_json_or_yaml(inp, out):
assert utils.load_json_or_yaml(inp) == out
@@ -74,19 +82,13 @@ def test_load_invalid_json_or_yaml(inp):
@pytest.mark.parametrize('non_ascii', [True, False])
@pytest.mark.skipif(
sys.version_info < (3, 6),
reason='this is only intended to be used in py3, not the CLI'
)
@pytest.mark.skipif(sys.version_info < (3, 6), reason='this is only intended to be used in py3, not the CLI')
def test_random_titles_are_unicode(non_ascii):
assert isinstance(utils.random_title(non_ascii=non_ascii), str)
@pytest.mark.parametrize('non_ascii', [True, False])
@pytest.mark.skipif(
sys.version_info < (3, 6),
reason='this is only intended to be used in py3, not the CLI'
)
@pytest.mark.skipif(sys.version_info < (3, 6), reason='this is only intended to be used in py3, not the CLI')
def test_random_titles_generates_correct_characters(non_ascii):
title = utils.random_title(non_ascii=non_ascii)
if non_ascii:
@@ -98,34 +100,39 @@ def test_random_titles_generates_correct_characters(non_ascii):
title.encode('utf-8')
@pytest.mark.parametrize('inp, out',
[['ClassNameShouldChange', 'class_name_should_change'],
['classnameshouldntchange', 'classnameshouldntchange'],
['Classspacingshouldntchange', 'classspacingshouldntchange'],
['Class1Name2Should3Change', 'class_1_name_2_should_3_change'],
['Class123name234should345change456', 'class_123_name_234_should_345_change_456']])
@pytest.mark.parametrize(
'inp, out',
[
['ClassNameShouldChange', 'class_name_should_change'],
['classnameshouldntchange', 'classnameshouldntchange'],
['Classspacingshouldntchange', 'classspacingshouldntchange'],
['Class1Name2Should3Change', 'class_1_name_2_should_3_change'],
['Class123name234should345change456', 'class_123_name_234_should_345_change_456'],
],
)
def test_class_name_to_kw_arg(inp, out):
assert utils.class_name_to_kw_arg(inp) == out
@pytest.mark.parametrize('first, second, expected',
[['/api/v2/resources/', '/api/v2/resources/', True],
['/api/v2/resources/', '/api/v2/resources/?test=ignored', True],
['/api/v2/resources/?one=ignored', '/api/v2/resources/?two=ignored', True],
['http://one.com', 'http://one.com', True],
['http://one.com', 'http://www.one.com', True],
['http://one.com', 'http://one.com?test=ignored', True],
['http://one.com', 'http://www.one.com?test=ignored', True],
['http://one.com', 'https://one.com', False],
['http://one.com', 'https://one.com?test=ignored', False]])
@pytest.mark.parametrize(
'first, second, expected',
[
['/api/v2/resources/', '/api/v2/resources/', True],
['/api/v2/resources/', '/api/v2/resources/?test=ignored', True],
['/api/v2/resources/?one=ignored', '/api/v2/resources/?two=ignored', True],
['http://one.com', 'http://one.com', True],
['http://one.com', 'http://www.one.com', True],
['http://one.com', 'http://one.com?test=ignored', True],
['http://one.com', 'http://www.one.com?test=ignored', True],
['http://one.com', 'https://one.com', False],
['http://one.com', 'https://one.com?test=ignored', False],
],
)
def test_are_same_endpoint(first, second, expected):
assert utils.are_same_endpoint(first, second) == expected
@pytest.mark.parametrize('endpoint, expected',
[['/api/v2/resources/', 'v2'],
['/api/v2000/resources/', 'v2000'],
['/api/', 'common']])
@pytest.mark.parametrize('endpoint, expected', [['/api/v2/resources/', 'v2'], ['/api/v2000/resources/', 'v2000'], ['/api/', 'common']])
def test_version_from_endpoint(endpoint, expected):
assert utils.version_from_endpoint(endpoint) == expected
@@ -133,42 +140,51 @@ def test_version_from_endpoint(endpoint, expected):
class OneClass:
pass
class TwoClass:
pass
class ThreeClass:
pass
class FourClass(ThreeClass):
pass
def test_filter_by_class_with_subclass_class():
filtered = utils.filter_by_class((OneClass, OneClass), (FourClass, ThreeClass))
assert filtered == [OneClass, FourClass]
def test_filter_by_class_with_subclass_instance():
one = OneClass()
four = FourClass()
filtered = utils.filter_by_class((one, OneClass), (four, ThreeClass))
assert filtered == [one, four]
def test_filter_by_class_no_arg_tuples():
three = ThreeClass()
filtered = utils.filter_by_class((True, OneClass), (False, TwoClass), (three, ThreeClass))
assert filtered == [OneClass, None, three]
def test_filter_by_class_with_arg_tuples_containing_class():
one = OneClass()
three = (ThreeClass, dict(one=1, two=2))
filtered = utils.filter_by_class((one, OneClass), (False, TwoClass), (three, ThreeClass))
assert filtered == [one, None, three]
def test_filter_by_class_with_arg_tuples_containing_subclass():
one = OneClass()
three = (FourClass, dict(one=1, two=2))
filtered = utils.filter_by_class((one, OneClass), (False, TwoClass), (three, ThreeClass))
assert filtered == [one, None, three]
@pytest.mark.parametrize('truthy', (True, 123, 'yes'))
def test_filter_by_class_with_arg_tuples_containing_truthy(truthy):
one = OneClass()
@@ -177,18 +193,20 @@ def test_filter_by_class_with_arg_tuples_containing_truthy(truthy):
assert filtered == [one, None, (ThreeClass, dict(one=1, two=2))]
@pytest.mark.parametrize('date_string,now,expected', [
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 2, 750000), 1.25),
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 1, 500000), 0.00),
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 0, 500000), -1.00),
])
@pytest.mark.parametrize(
'date_string,now,expected',
[
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 2, 750000), 1.25),
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 1, 500000), 0.00),
('2017-12-20T00:00:01.5Z', datetime(2017, 12, 20, 0, 0, 0, 500000), -1.00),
],
)
def test_seconds_since_date_string(date_string, now, expected):
with mock.patch('awxkit.utils.utcnow', return_value=now):
assert utils.seconds_since_date_string(date_string) == expected
class RecordingCallback(object):
def __init__(self, value=True):
self.call_count = 0
self.value = value
@@ -225,7 +243,6 @@ def test_suppress():
class TestPollUntil(object):
@pytest.mark.parametrize('timeout', [0, 0.0, -0.5, -1, -9999999])
def test_callback_called_once_for_non_positive_timeout(self, timeout):
with mock.patch('awxkit.utils.logged_sleep') as sleep:
@@ -246,7 +263,6 @@ class TestPollUntil(object):
class TestPseudoNamespace(object):
def test_set_item_check_item(self):
pn = utils.PseudoNamespace()
pn['key'] = 'value'
@@ -319,10 +335,7 @@ class TestPseudoNamespace(object):
assert pn == dict(one=[dict(two=2), dict(three=3)])
def test_instantiation_via_nested_dict_with_lists(self):
pn = utils.PseudoNamespace(dict(one=[dict(two=2),
dict(three=dict(four=4,
five=[dict(six=6),
dict(seven=7)]))]))
pn = utils.PseudoNamespace(dict(one=[dict(two=2), dict(three=dict(four=4, five=[dict(six=6), dict(seven=7)]))]))
assert pn.one[1].three.five[1].seven == 7
def test_instantiation_via_nested_dict_with_tuple(self):
@@ -332,10 +345,7 @@ class TestPseudoNamespace(object):
assert pn == dict(one=(dict(two=2), dict(three=3)))
def test_instantiation_via_nested_dict_with_tuples(self):
pn = utils.PseudoNamespace(dict(one=(dict(two=2),
dict(three=dict(four=4,
five=(dict(six=6),
dict(seven=7)))))))
pn = utils.PseudoNamespace(dict(one=(dict(two=2), dict(three=dict(four=4, five=(dict(six=6), dict(seven=7)))))))
assert pn.one[1].three.five[1].seven == 7
def test_update_with_nested_dict(self):
@@ -348,23 +358,16 @@ class TestPseudoNamespace(object):
def test_update_with_nested_dict_with_lists(self):
pn = utils.PseudoNamespace()
pn.update(dict(one=[dict(two=2),
dict(three=dict(four=4,
five=[dict(six=6),
dict(seven=7)]))]))
pn.update(dict(one=[dict(two=2), dict(three=dict(four=4, five=[dict(six=6), dict(seven=7)]))]))
assert pn.one[1].three.five[1].seven == 7
def test_update_with_nested_dict_with_tuples(self):
pn = utils.PseudoNamespace()
pn.update(dict(one=(dict(two=2),
dict(three=dict(four=4,
five=(dict(six=6),
dict(seven=7)))))))
pn.update(dict(one=(dict(two=2), dict(three=dict(four=4, five=(dict(six=6), dict(seven=7)))))))
assert pn.one[1].three.five[1].seven == 7
class TestUpdatePayload(object):
def test_empty_payload(self):
fields = ('one', 'two', 'three', 'four')
kwargs = dict(two=2, four=4)

View File

@@ -8,6 +8,7 @@ from awxkit.ws import WSClient
ParseResult = namedtuple("ParseResult", ["port", "hostname", "secure"])
def test_explicit_hostname():
client = WSClient("token", "some-hostname", 556, False)
assert client.port == 556
@@ -16,12 +17,15 @@ def test_explicit_hostname():
assert client.token == "token"
@pytest.mark.parametrize('url, result',
[['https://somename:123', ParseResult(123, "somename", True)],
['http://othername:456', ParseResult(456, "othername", False)],
['http://othername', ParseResult(80, "othername", False)],
['https://othername', ParseResult(443, "othername", True)],
])
@pytest.mark.parametrize(
'url, result',
[
['https://somename:123', ParseResult(123, "somename", True)],
['http://othername:456', ParseResult(456, "othername", False)],
['http://othername', ParseResult(80, "othername", False)],
['https://othername', ParseResult(443, "othername", True)],
],
)
def test_urlparsing(url, result):
with patch("awxkit.ws.config") as mock_config:
mock_config.base_url = url