mirror of
https://github.com/ansible/awx.git
synced 2026-05-20 23:37:39 -02:30
import awxkit
Co-authored-by: Christopher Wang <cwang@ansible.com> Co-authored-by: Jake McDermott <jmcdermott@ansible.com> Co-authored-by: Jim Ladd <jladd@redhat.com> Co-authored-by: Elijah DeLee <kdelee@redhat.com> Co-authored-by: Alan Rominger <arominge@redhat.com> Co-authored-by: Yanis Guenane <yanis@guenane.org>
This commit is contained in:
39
awxkit/awxkit/api/pages/__init__.py
Normal file
39
awxkit/awxkit/api/pages/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# Order matters
|
||||
from .page import * # NOQA
|
||||
from .base import * # NOQA
|
||||
from .access_list import * # NOQA
|
||||
from .api import * # NOQA
|
||||
from .authtoken import * # NOQA
|
||||
from .roles import * # NOQA
|
||||
from .organizations import * # NOQA
|
||||
from .notifications import * # NOQA
|
||||
from .notification_templates import * # NOQA
|
||||
from .users import * # NOQA
|
||||
from .applications import * # NOQA
|
||||
from .teams import * # NOQA
|
||||
from .credentials import * # NOQA
|
||||
from .unified_jobs import * # NOQA
|
||||
from .unified_job_templates import * # NOQA
|
||||
from .projects import * # NOQA
|
||||
from .inventory import * # NOQA
|
||||
from .system_job_templates import * # NOQA
|
||||
from .job_templates import * # NOQA
|
||||
from .jobs import * # NOQA
|
||||
from .survey_spec import * # NOQA
|
||||
from .system_jobs import * # NOQA
|
||||
from .config import * # NOQA
|
||||
from .ping import * # NOQA
|
||||
from .dashboard import * # NOQA
|
||||
from .activity_stream import * # NOQA
|
||||
from .schedules import * # NOQA
|
||||
from .ad_hoc_commands import * # NOQA
|
||||
from .labels import * # NOQA
|
||||
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 .settings import * # NOQA
|
||||
from .instances import * # NOQA
|
||||
from .instance_groups import * # NOQA
|
||||
from .credential_input_sources import * # NOQA
|
||||
from .metrics import * # NOQA
|
||||
18
awxkit/awxkit/api/pages/access_list.py
Normal file
18
awxkit/awxkit/api/pages/access_list.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import users
|
||||
from . import page
|
||||
|
||||
|
||||
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)
|
||||
20
awxkit/awxkit/api/pages/activity_stream.py
Normal file
20
awxkit/awxkit/api/pages/activity_stream.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class ActivityStream(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.activity, ActivityStream)
|
||||
|
||||
|
||||
class ActivityStreams(page.PageList, ActivityStream):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.activity_stream,
|
||||
resources.object_activity_stream], ActivityStreams)
|
||||
66
awxkit/awxkit/api/pages/ad_hoc_commands.py
Normal file
66
awxkit/awxkit/api/pages/ad_hoc_commands.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from awxkit.utils import update_payload, PseudoNamespace
|
||||
from awxkit.api.pages import Inventory, Credential
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.utils import not_provided as np
|
||||
from awxkit.api.resources import resources
|
||||
|
||||
from .jobs import UnifiedJob
|
||||
from . import page
|
||||
|
||||
|
||||
class AdHocCommand(HasCreate, UnifiedJob):
|
||||
|
||||
dependencies = [Inventory, Credential]
|
||||
|
||||
def relaunch(self, payload={}):
|
||||
"""Relaunch the command using the related->relaunch endpoint"""
|
||||
# navigate to relaunch_pg
|
||||
relaunch_pg = self.get_related('relaunch')
|
||||
|
||||
# relaunch the command
|
||||
result = relaunch_pg.post(payload)
|
||||
|
||||
# return the corresponding command_pg
|
||||
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)
|
||||
|
||||
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):
|
||||
|
||||
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.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):
|
||||
|
||||
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))
|
||||
|
||||
|
||||
page.register_page([resources.ad_hoc_command], AdHocCommand)
|
||||
|
||||
|
||||
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)
|
||||
19
awxkit/awxkit/api/pages/api.py
Normal file
19
awxkit/awxkit/api/pages/api.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Api(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.api, Api)
|
||||
|
||||
|
||||
class ApiV2(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.v2, ApiV2)
|
||||
84
awxkit/awxkit/api/pages/applications.py
Normal file
84
awxkit/awxkit/api/pages/applications.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from awxkit.utils import random_title, update_payload, filter_by_class, PseudoNamespace
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.api.pages import Organization
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
|
||||
from . import page
|
||||
from . import base
|
||||
|
||||
|
||||
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'))
|
||||
if kwargs.get('organization'):
|
||||
payload.organization = kwargs['organization'].id
|
||||
|
||||
optional_fields = ('redirect_uris', 'skip_authorization')
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
return payload
|
||||
|
||||
def create_payload(self, organization=Organization, **kwargs):
|
||||
self.create_and_update_dependencies(*filter_by_class((organization, Organization)))
|
||||
organization = self.ds.organization if organization else None
|
||||
payload = self.payload(organization=organization, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, organization=Organization, **kwargs):
|
||||
payload = self.create_payload(organization=organization, **kwargs)
|
||||
return self.update_identity(OAuth2Applications(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page((resources.application,
|
||||
(resources.applications, 'post')), OAuth2Application)
|
||||
|
||||
|
||||
class OAuth2Applications(page.PageList, OAuth2Application):
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.applications, OAuth2Applications)
|
||||
|
||||
|
||||
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'))
|
||||
|
||||
if kwargs.get('oauth_2_application'):
|
||||
payload.application = kwargs['oauth_2_application'].id
|
||||
|
||||
optional_fields = ('expires',)
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
return payload
|
||||
|
||||
def create_payload(self, oauth_2_application=None, **kwargs):
|
||||
self.create_and_update_dependencies(*filter_by_class((oauth_2_application, OAuth2Application)))
|
||||
oauth_2_application = self.ds.oauth_2_application if oauth_2_application else None
|
||||
payload = self.payload(oauth_2_application=oauth_2_application, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, oauth_2_application=None, **kwargs):
|
||||
payload = self.create_payload(oauth_2_application=oauth_2_application, **kwargs)
|
||||
return self.update_identity(OAuth2AccessTokens(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page((resources.token,
|
||||
(resources.tokens, 'post')), OAuth2AccessToken)
|
||||
|
||||
|
||||
class OAuth2AccessTokens(page.PageList, OAuth2AccessToken):
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.tokens, OAuth2AccessTokens)
|
||||
11
awxkit/awxkit/api/pages/authtoken.py
Normal file
11
awxkit/awxkit/api/pages/authtoken.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class AuthToken(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.authtoken, AuthToken)
|
||||
234
awxkit/awxkit/api/pages/base.py
Normal file
234
awxkit/awxkit/api/pages/base.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import collections
|
||||
import logging
|
||||
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
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
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Base(Page):
|
||||
|
||||
def silent_delete(self):
|
||||
"""Delete the object. If it's already deleted, ignore the error"""
|
||||
try:
|
||||
if not config.prevent_teardown:
|
||||
return self.delete()
|
||||
except (exc.NoContent, exc.NotFound, exc.Forbidden):
|
||||
pass
|
||||
|
||||
def get_object_role(self, role, by_name=False):
|
||||
"""Lookup and return a related object role by its role field or name.
|
||||
|
||||
Args:
|
||||
----
|
||||
role (str): The role's `role_field` or name
|
||||
by_name (bool): Whether to retrieve the role by its name field (default: False)
|
||||
|
||||
Examples:
|
||||
--------
|
||||
>>> # get the description of the Use role for an inventory
|
||||
>>> inventory = v2.inventory.create()
|
||||
>>> use_role_1 = inventory.get_object_role('use_role')
|
||||
>>> use_role_2 = inventory.get_object_role('use', True)
|
||||
>>> use_role_1.description
|
||||
u'Can use the inventory in a job template'
|
||||
>>> use_role_1.json == use_role_2.json
|
||||
True
|
||||
|
||||
"""
|
||||
if by_name:
|
||||
for obj_role in self.related.object_roles.get().results:
|
||||
if obj_role.name.lower() == role.lower():
|
||||
return obj_role
|
||||
|
||||
raise Exception("Role '{0}' not found for {1.endpoint}".format(role, self))
|
||||
|
||||
object_roles = self.get_related('object_roles', role_field=role)
|
||||
if not object_roles.count == 1:
|
||||
raise Exception("No role with role_field '{0}' found.".format(role))
|
||||
return object_roles.results[0]
|
||||
|
||||
def set_object_roles(self, agent, *role_names, **kw):
|
||||
"""Associate related object roles to a User or Team by role names
|
||||
|
||||
Args:
|
||||
----
|
||||
agent (User or Team): The agent the role is to be (dis)associated with.
|
||||
*role_names (str): an arbitrary number of role names ('Admin', 'Execute', 'Read', etc.)
|
||||
**kw:
|
||||
endpoint (str): The endpoint to use when making the object role association
|
||||
- 'related_users': use the related users endpoint of the role (default)
|
||||
- 'related_roles': use the related roles endpoint of the user
|
||||
disassociate (bool): Indicates whether to disassociate the role with the user (default: False)
|
||||
|
||||
Examples:
|
||||
--------
|
||||
# create a user that is an organization admin with use and
|
||||
# update roles on an inventory
|
||||
>>> organization = v2.organization.create()
|
||||
>>> inventory = v2.inventory.create()
|
||||
>>> user = v2.user.create()
|
||||
>>> organization.set_object_roles(user, 'admin')
|
||||
>>> inventory.set_object_roles(user, 'use', 'update')
|
||||
|
||||
"""
|
||||
from awxkit.api.pages import User, Team
|
||||
|
||||
endpoint = kw.get('endpoint', 'related_users')
|
||||
disassociate = kw.get('disassociate', False)
|
||||
|
||||
if not any([isinstance(agent, agent_type) for agent_type in (User, Team)]):
|
||||
raise ValueError('Invalid agent type {0.__class__.__name__}'.format(agent))
|
||||
|
||||
if endpoint not in ('related_users', 'related_roles'):
|
||||
raise ValueError('Invalid role association endpoint: {0}'.format(endpoint))
|
||||
|
||||
object_roles = [self.get_object_role(name, by_name=True) for name in role_names]
|
||||
payload = {}
|
||||
for role in object_roles:
|
||||
if endpoint == 'related_users':
|
||||
payload['id'] = agent.id
|
||||
if isinstance(agent, User):
|
||||
endpoint_model = role.related.users
|
||||
elif isinstance(agent, Team):
|
||||
endpoint_model = role.related.teams
|
||||
else:
|
||||
raise RuntimeError("Unhandled type for agent: {0.__class__.__name__}.".format(agent))
|
||||
elif endpoint == 'related_roles':
|
||||
payload['id'] = role.id
|
||||
endpoint_model = agent.related.roles
|
||||
else:
|
||||
raise RuntimeError('Invalid role association endpoint')
|
||||
|
||||
if disassociate:
|
||||
payload['disassociate'] = True
|
||||
|
||||
try:
|
||||
endpoint_model.post(payload)
|
||||
except exc.NoContent: # desired exception on successful (dis)association
|
||||
pass
|
||||
return True
|
||||
|
||||
@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)
|
||||
auth_url = resources.authtoken
|
||||
return get_registered_page(auth_url)(self.connection, endpoint=auth_url).post(payload).token
|
||||
|
||||
def load_authtoken(self, username='', password=''):
|
||||
self.connection.login(token=self.get_authtoken(username, password))
|
||||
return self
|
||||
|
||||
load_default_authtoken = load_authtoken
|
||||
|
||||
def get_oauth2_token(self, username='', password='', client_id=None,
|
||||
client_secret=None, scope='write'):
|
||||
default_cred = config.credentials.default
|
||||
username = username or default_cred.username
|
||||
password = password or default_cred.password
|
||||
req = collections.namedtuple('req', 'headers')({})
|
||||
if client_id and client_secret:
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
else:
|
||||
HTTPBasicAuth(username, password)(req)
|
||||
resp = self.connection.post(
|
||||
'/api/v2/users/{}/personal_tokens/'.format(username),
|
||||
json={
|
||||
"description": "Tower CLI",
|
||||
"application": None,
|
||||
"scope": scope
|
||||
},
|
||||
headers=req.headers
|
||||
)
|
||||
if resp.ok:
|
||||
result = resp.json()
|
||||
if client_id:
|
||||
return result.pop('access_token', None)
|
||||
else:
|
||||
return result.pop('token', None)
|
||||
else:
|
||||
raise exception_from_status_code(resp.status_code)
|
||||
|
||||
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())
|
||||
return self
|
||||
|
||||
def cleanup(self):
|
||||
log.debug('{0.endpoint} cleaning up.'.format(self))
|
||||
return self._cleanup(self.delete)
|
||||
|
||||
def silent_cleanup(self):
|
||||
log.debug('{0.endpoint} silently cleaning up.'.format(self))
|
||||
return self._cleanup(self.silent_delete)
|
||||
|
||||
def _cleanup(self, delete_method):
|
||||
try:
|
||||
delete_method()
|
||||
except exc.Forbidden as e:
|
||||
if e.msg == {'detail': 'Cannot delete running job resource.'}:
|
||||
self.cancel()
|
||||
self.wait_until_completed(interval=1, timeout=30, since_job_created=False)
|
||||
delete_method()
|
||||
else:
|
||||
raise
|
||||
except exc.Conflict as e:
|
||||
conflict = e.msg.get('conflict', e.msg.get('error', ''))
|
||||
if "running jobs" in conflict:
|
||||
active_jobs = e.msg.get('active_jobs', []) # [{type: id},], not page containing
|
||||
jobs = []
|
||||
for active_job in active_jobs:
|
||||
job_type = active_job['type']
|
||||
endpoint = '/api/v2/{}s/{}/'.format(job_type, active_job['id'])
|
||||
job = self.walk(endpoint)
|
||||
jobs.append(job)
|
||||
job.cancel()
|
||||
for job in jobs:
|
||||
job.wait_until_completed(interval=1, timeout=30, since_job_created=False)
|
||||
delete_method()
|
||||
else:
|
||||
raise
|
||||
55
awxkit/awxkit/api/pages/config.py
Normal file
55
awxkit/awxkit/api/pages/config.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
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
|
||||
|
||||
@property
|
||||
def is_demo_license(self):
|
||||
return self.license_info.get('demo', False) or \
|
||||
self.license_info.get('key_present', False)
|
||||
|
||||
@property
|
||||
def is_valid_license(self):
|
||||
return self.license_info.get('valid_key', False) and \
|
||||
'license_key' in self.license_info 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)
|
||||
|
||||
@property
|
||||
def is_awx_license(self):
|
||||
return self.license_info.get('license_type', None) == 'open'
|
||||
|
||||
@property
|
||||
def is_legacy_license(self):
|
||||
return self.is_valid_license and \
|
||||
self.license_info.get('license_type', None) == 'legacy'
|
||||
|
||||
@property
|
||||
def is_basic_license(self):
|
||||
return self.is_valid_license and \
|
||||
self.license_info.get('license_type', None) == 'basic'
|
||||
|
||||
@property
|
||||
def is_enterprise_license(self):
|
||||
return self.is_valid_license and \
|
||||
self.license_info.get('license_type', None) == 'enterprise'
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
"""returns a list of enabled license features"""
|
||||
return [k for k, v in self.license_info.get('features', {}).items() if v]
|
||||
|
||||
|
||||
page.register_page(resources.config, Config)
|
||||
20
awxkit/awxkit/api/pages/credential_input_sources.py
Normal file
20
awxkit/awxkit/api/pages/credential_input_sources.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class CredentialInputSource(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.credential_input_source, CredentialInputSource)
|
||||
|
||||
|
||||
class CredentialInputSources(page.PageList, CredentialInputSource):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.credential_input_sources,
|
||||
resources.related_input_sources], CredentialInputSources)
|
||||
336
awxkit/awxkit/api/pages/credentials.py
Normal file
336
awxkit/awxkit/api/pages/credentials.py
Normal file
@@ -0,0 +1,336 @@
|
||||
import logging
|
||||
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
from awxkit.utils import (
|
||||
cloud_types,
|
||||
filter_by_class,
|
||||
not_provided,
|
||||
random_title,
|
||||
update_payload,
|
||||
PseudoNamespace)
|
||||
from awxkit.api.pages import Organization, User, Team
|
||||
from awxkit.api.mixins import HasCreate, HasCopy, DSAdapter
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.config import config
|
||||
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
credential_input_fields = (
|
||||
'authorize_password',
|
||||
'become_method',
|
||||
'become_password',
|
||||
'become_username',
|
||||
'client',
|
||||
'cloud_environment',
|
||||
'domain',
|
||||
'host',
|
||||
'password',
|
||||
'project_id',
|
||||
'project_name',
|
||||
'secret',
|
||||
'ssh_key_data',
|
||||
'ssh_key_unlock',
|
||||
'subscription',
|
||||
'tenant',
|
||||
'username',
|
||||
'vault_password',
|
||||
'vault_id')
|
||||
|
||||
|
||||
def generate_private_key():
|
||||
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()
|
||||
).decode('utf-8')
|
||||
|
||||
|
||||
def config_cred_from_kind(kind):
|
||||
try:
|
||||
if kind == 'net':
|
||||
config_cred = config.credentials.network
|
||||
elif kind in cloud_types:
|
||||
if kind == 'azure_rm':
|
||||
config_cred = config.credentials.cloud.azure
|
||||
else:
|
||||
config_cred = config.credentials.cloud[kind]
|
||||
else:
|
||||
config_cred = config.credentials[kind]
|
||||
return config_cred
|
||||
except (KeyError, AttributeError):
|
||||
return PseudoNamespace()
|
||||
|
||||
|
||||
credential_type_name_to_config_kind_map = {
|
||||
'amazon web services': 'aws',
|
||||
'ansible tower': 'tower',
|
||||
'google compute engine': 'gce',
|
||||
'insights': 'insights',
|
||||
'microsoft azure classic (deprecated)': 'azure_classic',
|
||||
'microsoft azure resource manager': 'azure_rm',
|
||||
'network': 'net',
|
||||
'openstack': 'OpenStack',
|
||||
'red hat virtualization': 'rhv',
|
||||
'red hat cloudforms': 'cloudforms',
|
||||
'red hat satellite 6': 'satellite6',
|
||||
'source control': 'scm',
|
||||
'machine': 'ssh',
|
||||
'vault': 'vault',
|
||||
'vmware vcenter': 'vmware'}
|
||||
|
||||
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):
|
||||
kind = ''
|
||||
|
||||
if not credential_type.managed_by_tower:
|
||||
return kind, PseudoNamespace()
|
||||
try:
|
||||
if credential_type.kind == 'net':
|
||||
config_cred = config.credentials.network
|
||||
kind = 'net'
|
||||
elif credential_type.kind == 'cloud':
|
||||
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:
|
||||
kind = credential_type.kind.lower()
|
||||
config_cred = config.credentials[kind]
|
||||
return kind, config_cred
|
||||
except (KeyError, AttributeError):
|
||||
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
|
||||
config_field = 'project'
|
||||
elif field == 'subscription' and 'azure' in kind:
|
||||
config_field = 'subscription_id'
|
||||
elif field == 'username' and kind == 'azure_ad':
|
||||
config_field = 'ad_user'
|
||||
elif field == 'client':
|
||||
config_field = 'client_id'
|
||||
elif field == 'authorize_password':
|
||||
config_field = 'authorize'
|
||||
else:
|
||||
config_field = field
|
||||
value = kwargs.get(field, config_cred.get(config_field, not_provided))
|
||||
if field in ('project_id', 'project_name'):
|
||||
field = 'project'
|
||||
return field, value
|
||||
|
||||
|
||||
class CredentialType(HasCreate, base.Base):
|
||||
|
||||
def silent_delete(self):
|
||||
if not self.managed_by_tower:
|
||||
return super(CredentialType, self).silent_delete()
|
||||
|
||||
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)
|
||||
fields = ('inputs', 'injectors')
|
||||
update_payload(payload, fields, kwargs)
|
||||
return payload
|
||||
|
||||
def create_payload(self, kind='cloud', **kwargs):
|
||||
payload = self.payload(kind=kind, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, kind='cloud', **kwargs):
|
||||
payload = self.create_payload(kind=kind, **kwargs)
|
||||
return self.update_identity(
|
||||
CredentialTypes(
|
||||
self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.credential_type,
|
||||
(resources.credential_types, 'post')], CredentialType)
|
||||
|
||||
|
||||
class CredentialTypes(page.PageList, CredentialType):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.credential_types, CredentialTypes)
|
||||
|
||||
|
||||
class Credential(HasCopy, HasCreate, base.Base):
|
||||
|
||||
dependencies = [CredentialType]
|
||||
optional_dependencies = [Organization, User, Team]
|
||||
|
||||
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))
|
||||
|
||||
if inputs is None:
|
||||
inputs = {}
|
||||
|
||||
payload = PseudoNamespace(
|
||||
name=kwargs.get('name') or 'Credential - {}'.format(
|
||||
random_title()),
|
||||
description=kwargs.get('description') or random_title(10),
|
||||
credential_type=credential_type.id,
|
||||
inputs=inputs)
|
||||
if user:
|
||||
payload.user = user.id
|
||||
if team:
|
||||
payload.team = team.id
|
||||
if organization:
|
||||
payload.organization = organization.id
|
||||
|
||||
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)
|
||||
if value != not_provided:
|
||||
payload.inputs[field] = value
|
||||
|
||||
if kind == 'net':
|
||||
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())
|
||||
|
||||
return payload
|
||||
|
||||
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()
|
||||
|
||||
if credential_type == CredentialType:
|
||||
kind = kwargs.pop('kind', 'ssh')
|
||||
if kind in ('openstack', 'openstack_v3'):
|
||||
credential_type_name = 'OpenStack'
|
||||
if inputs is None:
|
||||
if kind == 'openstack_v3':
|
||||
inputs = config.credentials.cloud['openstack_v3']
|
||||
else:
|
||||
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, 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)
|
||||
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.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(
|
||||
self,
|
||||
credential_type=CredentialType,
|
||||
user=None,
|
||||
team=None,
|
||||
organization=Organization,
|
||||
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)
|
||||
|
||||
@property
|
||||
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'):
|
||||
if getattr(self.inputs, field, None) == 'ASK':
|
||||
if field == 'password':
|
||||
passwords.append('ssh_password')
|
||||
else:
|
||||
passwords.append(field)
|
||||
return passwords
|
||||
|
||||
|
||||
page.register_page([resources.credential,
|
||||
(resources.credentials, 'post'),
|
||||
(resources.credential_copy, 'post')], Credential)
|
||||
|
||||
|
||||
class Credentials(page.PageList, Credential):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.credentials,
|
||||
resources.related_credentials,
|
||||
resources.job_extra_credentials,
|
||||
resources.job_template_extra_credentials],
|
||||
Credentials)
|
||||
11
awxkit/awxkit/api/pages/dashboard.py
Normal file
11
awxkit/awxkit/api/pages/dashboard.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Dashboard(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.dashboard, Dashboard)
|
||||
45
awxkit/awxkit/api/pages/instance_groups.py
Normal file
45
awxkit/awxkit/api/pages/instance_groups.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from awxkit.utils import PseudoNamespace, random_title, suppress, update_payload
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.api.mixins import HasCreate
|
||||
import awxkit.exceptions as exc
|
||||
from . import base
|
||||
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))
|
||||
|
||||
def remove_instance(self, instance):
|
||||
with suppress(exc.NoContent):
|
||||
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()))
|
||||
fields = ('policy_instance_percentage', 'policy_instance_minimum', 'policy_instance_list')
|
||||
update_payload(payload, fields, kwargs)
|
||||
return payload
|
||||
|
||||
def create_payload(self, name='', **kwargs):
|
||||
payload = self.payload(name=name, **kwargs)
|
||||
return payload
|
||||
|
||||
def create(self, name='', **kwargs):
|
||||
payload = self.create_payload(name=name, **kwargs)
|
||||
return self.update_identity(InstanceGroups(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.instance_group,
|
||||
(resources.instance_groups, 'post')], InstanceGroup)
|
||||
|
||||
|
||||
class InstanceGroups(page.PageList, InstanceGroup):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.instance_groups,
|
||||
resources.related_instance_groups], InstanceGroups)
|
||||
20
awxkit/awxkit/api/pages/instances.py
Normal file
20
awxkit/awxkit/api/pages/instances.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Instance(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.instance, Instance)
|
||||
|
||||
|
||||
class Instances(page.PageList, Instance):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.instances,
|
||||
resources.related_instances], Instances)
|
||||
684
awxkit/awxkit/api/pages/inventory.py
Normal file
684
awxkit/awxkit/api/pages/inventory.py
Normal file
@@ -0,0 +1,684 @@
|
||||
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.mixins import DSAdapter, HasCreate, HasInstanceGroups, HasNotifications, HasVariables, HasCopy
|
||||
from awxkit.api.resources import resources
|
||||
import awxkit.exceptions as exc
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Inventory(HasCopy, HasCreate, HasInstanceGroups, HasVariables, base.Base):
|
||||
|
||||
dependencies = [Organization]
|
||||
|
||||
def print_ini(self):
|
||||
"""Print an ini version of the inventory"""
|
||||
output = list()
|
||||
inv_dict = self.related.script.get(hostvars=1).json
|
||||
|
||||
for group in inv_dict.keys():
|
||||
if group == '_meta':
|
||||
continue
|
||||
|
||||
# output host groups
|
||||
output.append('[%s]' % group)
|
||||
for host in inv_dict[group].get('hosts', []):
|
||||
# FIXME ... include hostvars
|
||||
output.append(host)
|
||||
output.append('') # newline
|
||||
|
||||
# output child groups
|
||||
if inv_dict[group].get('children', []):
|
||||
output.append('[%s:children]' % group)
|
||||
for child in inv_dict[group].get('children', []):
|
||||
output.append(child)
|
||||
output.append('') # newline
|
||||
|
||||
# output group vars
|
||||
if inv_dict[group].get('vars', {}).items():
|
||||
output.append('[%s:vars]' % group)
|
||||
for k, v in inv_dict[group].get('vars', {}).items():
|
||||
output.append('%s=%s' % (k, v))
|
||||
output.append('') # newline
|
||||
|
||||
print('\n'.join(output))
|
||||
|
||||
def payload(self, organization, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
name=kwargs.get('name') or 'Inventory - {}'.format(
|
||||
random_title()),
|
||||
description=kwargs.get('description') or random_title(10),
|
||||
organization=organization.id)
|
||||
|
||||
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):
|
||||
payload.insights_credential = payload.insights_credential.id
|
||||
|
||||
return payload
|
||||
|
||||
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.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 add_host(self, host=None):
|
||||
if host is None:
|
||||
return self.related.hosts.create(inventory=self)
|
||||
|
||||
if isinstance(host, base.Base):
|
||||
host = host.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.hosts.post(host)
|
||||
return host
|
||||
|
||||
def wait_until_deleted(self):
|
||||
def _wait():
|
||||
try:
|
||||
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']
|
||||
|
||||
inv_updates = []
|
||||
for source_id in source_ids:
|
||||
inv_source = self.related.inventory_sources.get(
|
||||
id=source_id).results.pop()
|
||||
inv_updates.append(inv_source.related.current_job.get())
|
||||
|
||||
if wait:
|
||||
for update in inv_updates:
|
||||
update.wait_until_completed()
|
||||
return inv_updates
|
||||
|
||||
|
||||
page.register_page([resources.inventory,
|
||||
(resources.inventories, 'post'),
|
||||
(resources.inventory_copy, 'post')], Inventory)
|
||||
|
||||
|
||||
class Inventories(page.PageList, Inventory):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.inventories,
|
||||
resources.related_inventories], Inventories)
|
||||
|
||||
|
||||
class InventoryScript(HasCopy, HasCreate, base.Base):
|
||||
|
||||
dependencies = [Organization]
|
||||
|
||||
def payload(self, organization, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
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())
|
||||
return payload
|
||||
|
||||
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.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 _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))'
|
||||
])
|
||||
group_name = re.sub(r"[\']", "", "group-{}".format(random_utf8()))
|
||||
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)
|
||||
|
||||
|
||||
class InventoryScripts(page.PageList, InventoryScript):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.inventory_scripts], InventoryScripts)
|
||||
|
||||
|
||||
class Group(HasCreate, HasVariables, base.Base):
|
||||
|
||||
dependencies = [Inventory]
|
||||
optional_dependencies = [Credential, InventoryScript]
|
||||
|
||||
@property
|
||||
def is_root_group(self):
|
||||
"""Returns whether the current group is a top-level root group in the inventory"""
|
||||
return self.related.inventory.get().related.root_groups.get(id=self.id).count == 1
|
||||
|
||||
def get_parents(self):
|
||||
"""Inspects the API and returns all groups that include the current group as a child."""
|
||||
return Groups(self.connection).get(children=self.id).results
|
||||
|
||||
def payload(self, inventory, credential=None, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
name=kwargs.get('name') or 'Group{}'.format(
|
||||
random_title(
|
||||
non_ascii=False)),
|
||||
description=kwargs.get('description') or random_title(10),
|
||||
inventory=inventory.id)
|
||||
|
||||
if credential:
|
||||
payload.credential = credential.id
|
||||
|
||||
update_payload(payload, ('variables',), kwargs)
|
||||
|
||||
if 'variables' in payload and isinstance(payload.variables, dict):
|
||||
payload.variables = json.dumps(payload.variables)
|
||||
|
||||
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)
|
||||
credential = self.ds.credential if credential else None
|
||||
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)
|
||||
|
||||
parent = kwargs.get('parent', None) # parent must be a Group instance
|
||||
resource = parent.related.children if parent else Groups(
|
||||
self.connection)
|
||||
return self.update_identity(resource.post(payload))
|
||||
|
||||
def add_host(self, host=None):
|
||||
if host is None:
|
||||
host = self.related.hosts.create(inventory=self.ds.inventory)
|
||||
with suppress(exc.NoContent):
|
||||
host.related.groups.post(dict(id=self.id))
|
||||
return host
|
||||
|
||||
if isinstance(host, base.Base):
|
||||
host = host.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.hosts.post(host)
|
||||
return host
|
||||
|
||||
def add_group(self, group):
|
||||
if isinstance(group, page.Page):
|
||||
group = group.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.children.post(group)
|
||||
|
||||
def remove_group(self, group):
|
||||
if isinstance(group, page.Page):
|
||||
group = group.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.children.post(dict(id=group.id, disassociate=True))
|
||||
|
||||
|
||||
page.register_page([resources.group,
|
||||
(resources.groups, 'post')], Group)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class Host(HasCreate, HasVariables, base.Base):
|
||||
|
||||
dependencies = [Inventory]
|
||||
|
||||
def payload(self, inventory, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
name=kwargs.get('name') or 'Host{}'.format(
|
||||
random_title(
|
||||
non_ascii=False)),
|
||||
description=kwargs.get('description') or random_title(10),
|
||||
inventory=inventory.id)
|
||||
|
||||
optional_fields = ('enabled', 'instance_id')
|
||||
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
|
||||
variables = kwargs.get('variables', not_provided)
|
||||
|
||||
if variables is None:
|
||||
variables = dict(
|
||||
ansible_host='127.0.0.1',
|
||||
ansible_connection='local')
|
||||
|
||||
if variables != not_provided:
|
||||
if isinstance(variables, dict):
|
||||
variables = json.dumps(variables)
|
||||
payload.variables = variables
|
||||
|
||||
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)
|
||||
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)
|
||||
return self.update_identity(Hosts(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.host,
|
||||
(resources.hosts, 'post')], Host)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class FactVersion(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.host_related_fact_version, FactVersion)
|
||||
|
||||
|
||||
class FactVersions(page.PageList, FactVersion):
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return len(self.results)
|
||||
|
||||
|
||||
page.register_page(resources.host_related_fact_versions, FactVersions)
|
||||
|
||||
|
||||
class FactView(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.fact_view, FactView)
|
||||
|
||||
|
||||
class InventorySource(HasCreate, HasNotifications, UnifiedJobTemplate):
|
||||
|
||||
optional_schedule_fields = tuple()
|
||||
dependencies = [Inventory]
|
||||
optional_dependencies = [Credential, InventoryScript, Project]
|
||||
|
||||
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()),
|
||||
description=kwargs.get('description') or random_title(10),
|
||||
inventory=inventory.id,
|
||||
source=source)
|
||||
|
||||
if credential:
|
||||
payload.credential = credential.id
|
||||
if source_script:
|
||||
payload.source_script = source_script.id
|
||||
if project:
|
||||
payload.source_project = project.id
|
||||
|
||||
optional_fields = (
|
||||
'group_by',
|
||||
'instance_filters',
|
||||
'source_path',
|
||||
'source_regions',
|
||||
'source_vars',
|
||||
'timeout',
|
||||
'overwrite',
|
||||
'overwrite_vars',
|
||||
'update_cache_timeout',
|
||||
'update_on_launch',
|
||||
'update_on_project_update',
|
||||
'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):
|
||||
if source != 'custom' and source_script == InventoryScript:
|
||||
source_script = None
|
||||
if source == 'scm':
|
||||
kwargs.setdefault('overwrite_vars', True)
|
||||
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)
|
||||
|
||||
if credential:
|
||||
credential = self.ds.credential
|
||||
if source_script:
|
||||
source_script = self.ds.inventory_script
|
||||
if project:
|
||||
project = self.ds.project
|
||||
|
||||
payload = self.payload(
|
||||
inventory=self.ds.inventory,
|
||||
source=source,
|
||||
credential=credential,
|
||||
source_script=source_script,
|
||||
project=project,
|
||||
name=name,
|
||||
description=description,
|
||||
**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):
|
||||
payload = self.create_payload(
|
||||
name=name,
|
||||
description=description,
|
||||
source=source,
|
||||
inventory=inventory,
|
||||
credential=credential,
|
||||
source_script=source_script,
|
||||
project=project,
|
||||
**kwargs)
|
||||
return self.update_identity(
|
||||
InventorySources(
|
||||
self.connection).post(payload))
|
||||
|
||||
def update(self):
|
||||
"""Update the inventory_source using related->update endpoint"""
|
||||
# get related->launch
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
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
|
||||
|
||||
def add_credential(self, credential):
|
||||
with suppress(exc.NoContent):
|
||||
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))
|
||||
|
||||
|
||||
page.register_page([resources.inventory_source,
|
||||
(resources.inventory_sources, 'post')], InventorySource)
|
||||
|
||||
|
||||
class InventorySources(page.PageList, InventorySource):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.inventory_sources,
|
||||
resources.related_inventory_sources],
|
||||
InventorySources)
|
||||
|
||||
|
||||
class InventorySourceGroups(page.PageList, Group):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(
|
||||
resources.inventory_sources_related_groups,
|
||||
InventorySourceGroups)
|
||||
|
||||
|
||||
class InventorySourceUpdate(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.inventory_sources_related_update,
|
||||
resources.inventory_related_update_inventory_sources],
|
||||
InventorySourceUpdate)
|
||||
|
||||
|
||||
class InventoryUpdate(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.inventory_update, InventoryUpdate)
|
||||
|
||||
|
||||
class InventoryUpdates(page.PageList, InventoryUpdate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.inventory_updates,
|
||||
resources.inventory_source_updates,
|
||||
resources.project_update_scm_inventory_updates],
|
||||
InventoryUpdates)
|
||||
|
||||
|
||||
class InventoryUpdateCancel(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.inventory_update_cancel, InventoryUpdateCancel)
|
||||
235
awxkit/awxkit/api/pages/job_templates.py
Normal file
235
awxkit/awxkit/api/pages/job_templates.py
Normal file
@@ -0,0 +1,235 @@
|
||||
import json
|
||||
|
||||
from awxkit.utils import (
|
||||
filter_by_class,
|
||||
not_provided,
|
||||
random_title,
|
||||
suppress,
|
||||
update_payload,
|
||||
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
|
||||
import awxkit.exceptions as exc
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class JobTemplate(
|
||||
HasCopy,
|
||||
HasCreate,
|
||||
HasInstanceGroups,
|
||||
HasNotifications,
|
||||
HasSurvey,
|
||||
UnifiedJobTemplate):
|
||||
|
||||
optional_dependencies = [Inventory, Credential, Project]
|
||||
|
||||
def launch(self, payload={}):
|
||||
"""Launch the job_template using related->launch endpoint."""
|
||||
# get related->launch
|
||||
launch_pg = self.get_related('launch')
|
||||
|
||||
# launch the job_template
|
||||
result = launch_pg.post(payload)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
)
|
||||
return slice_workflow_jobs.results[0]
|
||||
else:
|
||||
raise RuntimeError('Unexpected type of job template spawned job.')
|
||||
|
||||
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)
|
||||
|
||||
optional_fields = (
|
||||
'ask_scm_branch_on_launch',
|
||||
'ask_credential_on_launch',
|
||||
'ask_diff_mode_on_launch',
|
||||
'ask_inventory_on_launch',
|
||||
'ask_job_type_on_launch',
|
||||
'ask_limit_on_launch',
|
||||
'ask_skip_tags_on_launch',
|
||||
'ask_tags_on_launch',
|
||||
'ask_variables_on_launch',
|
||||
'ask_verbosity_on_launch',
|
||||
'allow_simultaneous',
|
||||
'become_enabled',
|
||||
'diff_mode',
|
||||
'force_handlers',
|
||||
'forks',
|
||||
'host_config_key',
|
||||
'job_tags',
|
||||
'limit',
|
||||
'skip_tags',
|
||||
'start_at_task',
|
||||
'survey_enabled',
|
||||
'timeout',
|
||||
'use_fact_cache',
|
||||
'vault_credential',
|
||||
'verbosity',
|
||||
'job_slice_count',
|
||||
'scm_branch')
|
||||
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
|
||||
extra_vars = kwargs.get('extra_vars', not_provided)
|
||||
if extra_vars != not_provided:
|
||||
if isinstance(extra_vars, dict):
|
||||
extra_vars = json.dumps(extra_vars)
|
||||
payload.update(extra_vars=extra_vars)
|
||||
|
||||
if kwargs.get('project'):
|
||||
payload.update(project=kwargs.get('project').id, playbook=playbook)
|
||||
if kwargs.get('inventory'):
|
||||
payload.update(inventory=kwargs.get('inventory').id)
|
||||
if kwargs.get('credential'):
|
||||
payload.update(credential=kwargs.get('credential').id)
|
||||
return payload
|
||||
|
||||
def add_label(self, label):
|
||||
if isinstance(label, page.Page):
|
||||
label = label.json
|
||||
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):
|
||||
if not project and job_type != 'scan':
|
||||
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)))
|
||||
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)
|
||||
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))
|
||||
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']))
|
||||
return ret
|
||||
|
||||
def add_extra_credential(self, credential):
|
||||
with suppress(exc.NoContent):
|
||||
self.related.extra_credentials.post(
|
||||
dict(id=credential.id, associate=True))
|
||||
|
||||
def remove_extra_credential(self, credential):
|
||||
with suppress(exc.NoContent):
|
||||
self.related.extra_credentials.post(
|
||||
dict(id=credential.id, disassociate=True))
|
||||
|
||||
def add_credential(self, credential):
|
||||
with suppress(exc.NoContent):
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
|
||||
page.register_page([resources.job_template,
|
||||
(resources.job_templates, 'post'),
|
||||
(resources.job_template_copy, 'post')], JobTemplate)
|
||||
|
||||
|
||||
class JobTemplates(page.PageList, JobTemplate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.job_templates,
|
||||
resources.related_job_templates], JobTemplates)
|
||||
|
||||
|
||||
class JobTemplateCallback(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_template_callback, JobTemplateCallback)
|
||||
|
||||
|
||||
class JobTemplateLaunch(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_template_launch, JobTemplateLaunch)
|
||||
117
awxkit/awxkit/api/pages/jobs.py
Normal file
117
awxkit/awxkit/api/pages/jobs.py
Normal file
@@ -0,0 +1,117 @@
|
||||
from awxkit.api.pages import UnifiedJob
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Job(UnifiedJob):
|
||||
|
||||
def relaunch(self, payload={}):
|
||||
result = self.related.relaunch.post(payload)
|
||||
return self.walk(result.endpoint)
|
||||
|
||||
|
||||
page.register_page(resources.job, Job)
|
||||
|
||||
|
||||
class Jobs(page.PageList, Job):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.jobs,
|
||||
resources.job_template_jobs,
|
||||
resources.system_job_template_jobs], Jobs)
|
||||
|
||||
|
||||
class JobCancel(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_cancel, JobCancel)
|
||||
|
||||
|
||||
class JobEvent(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.job_event,
|
||||
resources.job_job_event], JobEvent)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class JobPlay(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_play, JobPlay)
|
||||
|
||||
|
||||
class JobPlays(page.PageList, JobPlay):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_plays, JobPlays)
|
||||
|
||||
|
||||
class JobTask(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_task, JobTask)
|
||||
|
||||
|
||||
class JobTasks(page.PageList, JobTask):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_tasks, JobTasks)
|
||||
|
||||
|
||||
class JobHostSummary(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_host_summary, JobHostSummary)
|
||||
|
||||
|
||||
class JobHostSummaries(page.PageList, JobHostSummary):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.job_host_summaries,
|
||||
resources.group_related_job_host_summaries], JobHostSummaries)
|
||||
|
||||
|
||||
class JobRelaunch(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.job_relaunch, JobRelaunch)
|
||||
|
||||
|
||||
class JobStdout(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.related_stdout, JobStdout)
|
||||
67
awxkit/awxkit/api/pages/labels.py
Normal file
67
awxkit/awxkit/api/pages/labels.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from awxkit.utils import random_title, PseudoNamespace
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.api.pages import Organization
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Label(HasCreate, base.Base):
|
||||
|
||||
dependencies = [Organization]
|
||||
|
||||
def silent_delete(self):
|
||||
"""Label pages do not support DELETE requests. Here, we override the base page object
|
||||
silent_delete method to account for this.
|
||||
"""
|
||||
pass
|
||||
|
||||
def payload(self, organization, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
name=kwargs.get('name') or 'Label - {}'.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):
|
||||
self.create_and_update_dependencies(organization)
|
||||
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)
|
||||
return self.update_identity(Labels(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.label,
|
||||
(resources.labels, 'post')], Label)
|
||||
|
||||
|
||||
class Labels(page.PageList, Label):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.labels,
|
||||
resources.job_labels,
|
||||
resources.job_template_labels], Labels)
|
||||
23
awxkit/awxkit/api/pages/metrics.py
Normal file
23
awxkit/awxkit/api/pages/metrics.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from prometheus_client.parser import text_string_to_metric_families
|
||||
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Metrics(base.Base):
|
||||
|
||||
def get(self, **query_parameters):
|
||||
request = self.connection.get(self.endpoint, query_parameters)
|
||||
self.page_identity(request, ignore_json_errors=True)
|
||||
parsed_metrics = text_string_to_metric_families(request.text)
|
||||
data = {}
|
||||
for family in parsed_metrics:
|
||||
for sample in family.samples:
|
||||
data[sample[0]] = {"labels": sample[1], "value": sample[2]}
|
||||
request.json = lambda: data
|
||||
return self.page_identity(request)
|
||||
|
||||
|
||||
page.register_page([resources.metrics,
|
||||
(resources.metrics, 'get')], Metrics)
|
||||
219
awxkit/awxkit/api/pages/notification_templates.py
Normal file
219
awxkit/awxkit/api/pages/notification_templates.py
Normal file
@@ -0,0 +1,219 @@
|
||||
from awxkit.api.mixins import HasCreate, HasCopy, DSAdapter
|
||||
from awxkit.api.pages import Organization
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.config import config
|
||||
import awxkit.exceptions as exc
|
||||
from awxkit.utils import not_provided, random_title, suppress, PseudoNamespace
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
job_results = ('any', 'error', 'success')
|
||||
notification_types = (
|
||||
'email',
|
||||
'irc',
|
||||
'pagerduty',
|
||||
'slack',
|
||||
'twilio',
|
||||
'webhook',
|
||||
'mattermost')
|
||||
|
||||
|
||||
class NotificationTemplate(HasCopy, HasCreate, base.Base):
|
||||
|
||||
dependencies = [Organization]
|
||||
|
||||
def test(self):
|
||||
"""Create test notification"""
|
||||
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)
|
||||
return notifications_pg.results[0]
|
||||
|
||||
def silent_delete(self):
|
||||
"""Delete the Notification Template, ignoring the exception that is raised
|
||||
if there are notifications pending.
|
||||
"""
|
||||
try:
|
||||
super(NotificationTemplate, self).silent_delete()
|
||||
except (exc.MethodNotAllowed):
|
||||
pass
|
||||
|
||||
def payload(self, organization, notification_type='slack', **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
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_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')
|
||||
cred = services.email
|
||||
elif notification_type == 'irc':
|
||||
fields = (
|
||||
'server',
|
||||
'port',
|
||||
'use_ssl',
|
||||
'password',
|
||||
'nickname',
|
||||
'targets')
|
||||
cred = services.irc
|
||||
elif notification_type == 'pagerduty':
|
||||
fields = ('client_name', 'service_key', 'subdomain', 'token')
|
||||
cred = services.pagerduty
|
||||
elif notification_type == 'slack':
|
||||
fields = ('channels', 'token')
|
||||
cred = services.slack
|
||||
elif notification_type == 'twilio':
|
||||
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')
|
||||
cred = services.mattermost
|
||||
else:
|
||||
raise ValueError(
|
||||
'Unknown notification_type {0}'.format(notification_type))
|
||||
|
||||
for field in fields:
|
||||
if field == 'bot_token':
|
||||
payload_field = 'token'
|
||||
else:
|
||||
payload_field = field
|
||||
value = kwargs.get(field, cred.get(field, not_provided))
|
||||
if value != not_provided:
|
||||
payload.notification_configuration[payload_field] = value
|
||||
|
||||
return payload
|
||||
|
||||
def create_payload(
|
||||
self,
|
||||
name='',
|
||||
description='',
|
||||
notification_type='slack',
|
||||
organization=Organization,
|
||||
**kwargs):
|
||||
if notification_type not in 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,
|
||||
**kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(
|
||||
self,
|
||||
name='',
|
||||
description='',
|
||||
notification_type='slack',
|
||||
organization=Organization,
|
||||
**kwargs):
|
||||
payload = self.create_payload(
|
||||
name=name,
|
||||
description=description,
|
||||
notification_type=notification_type,
|
||||
organization=organization,
|
||||
**kwargs)
|
||||
return self.update_identity(
|
||||
NotificationTemplates(
|
||||
self.connection).post(payload))
|
||||
|
||||
def associate(self, resource, job_result='any'):
|
||||
"""Associates a NotificationTemplate with the provided resource"""
|
||||
return self._associate(resource, job_result)
|
||||
|
||||
def disassociate(self, resource, job_result='any'):
|
||||
"""Disassociates a NotificationTemplate with the provided resource"""
|
||||
return self._associate(resource, job_result, disassociate=True)
|
||||
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
payload = dict(id=self.id)
|
||||
if disassociate:
|
||||
payload['disassociate'] = True
|
||||
|
||||
with suppress(exc.NoContent):
|
||||
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_error,
|
||||
resources.notification_template_success], NotificationTemplate)
|
||||
|
||||
|
||||
class NotificationTemplates(page.PageList, NotificationTemplate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.notification_templates,
|
||||
resources.notification_templates_any,
|
||||
resources.notification_templates_error,
|
||||
resources.notification_templates_success],
|
||||
NotificationTemplates)
|
||||
|
||||
|
||||
class NotificationTemplateTest(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(
|
||||
resources.notification_template_test,
|
||||
NotificationTemplateTest)
|
||||
52
awxkit/awxkit/api/pages/notifications.py
Normal file
52
awxkit/awxkit/api/pages/notifications.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from awxkit.api.mixins import HasStatus
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.utils import poll_until, seconds_since_date_string
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Notification(HasStatus, base.Base):
|
||||
|
||||
def __str__(self):
|
||||
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)))
|
||||
output = '<{0.__class__.__name__} {1}>'.format(self, ', '.join(info))
|
||||
return output.replace('%', '%%')
|
||||
|
||||
@property
|
||||
def is_successful(self):
|
||||
"""Return whether the notification was created successfully. This means that:
|
||||
* self.status == 'successful'
|
||||
* self.error == False
|
||||
"""
|
||||
return super(Notification, self).is_successful and not self.error
|
||||
|
||||
def wait_until_status(self, status, interval=5, timeout=30, **kwargs):
|
||||
adjusted_timeout = timeout - seconds_since_date_string(self.created)
|
||||
return super(Notification, self).wait_until_status(status, interval, adjusted_timeout, **kwargs)
|
||||
|
||||
def wait_until_completed(self, interval=5, timeout=240):
|
||||
"""Notifications need a longer timeout, since the backend often has
|
||||
to wait for the request (sending the notification) to timeout itself
|
||||
"""
|
||||
adjusted_timeout = timeout - seconds_since_date_string(self.created)
|
||||
return super(Notification, self).wait_until_completed(interval, adjusted_timeout)
|
||||
|
||||
|
||||
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)
|
||||
return self
|
||||
|
||||
|
||||
page.register_page([resources.notifications,
|
||||
resources.related_notifications], Notifications)
|
||||
49
awxkit/awxkit/api/pages/organizations.py
Normal file
49
awxkit/awxkit/api/pages/organizations.py
Normal file
@@ -0,0 +1,49 @@
|
||||
from awxkit.api.mixins import HasCreate, HasInstanceGroups, HasNotifications, DSAdapter
|
||||
from awxkit.utils import random_title, suppress, PseudoNamespace
|
||||
from awxkit.api.resources import resources
|
||||
import awxkit.exceptions as exc
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Organization(HasCreate, HasInstanceGroups, HasNotifications, base.Base):
|
||||
|
||||
def add_admin(self, user):
|
||||
if isinstance(user, page.Page):
|
||||
user = user.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.admins.post(user)
|
||||
|
||||
def add_user(self, user):
|
||||
if isinstance(user, page.Page):
|
||||
user = user.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.users.post(user)
|
||||
|
||||
def payload(self, **kwargs):
|
||||
payload = PseudoNamespace(name=kwargs.get('name') or 'Organization - {}'.format(random_title()),
|
||||
description=kwargs.get('description') or random_title(10))
|
||||
return payload
|
||||
|
||||
def create_payload(self, name='', description='', **kwargs):
|
||||
payload = self.payload(name=name, description=description, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, name='', description='', **kwargs):
|
||||
payload = self.create_payload(name=name, description=description, **kwargs)
|
||||
return self.update_identity(Organizations(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.organization,
|
||||
(resources.organizations, 'post')], Organization)
|
||||
|
||||
|
||||
class Organizations(page.PageList, Organization):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.organizations,
|
||||
resources.user_organizations,
|
||||
resources.project_organizations], Organizations)
|
||||
500
awxkit/awxkit/api/pages/page.py
Normal file
500
awxkit/awxkit/api/pages/page.py
Normal file
@@ -0,0 +1,500 @@
|
||||
import http.client
|
||||
import inspect
|
||||
import logging
|
||||
import json
|
||||
import re
|
||||
|
||||
from requests import Response
|
||||
|
||||
from awxkit.utils import (
|
||||
PseudoNamespace,
|
||||
is_relative_endpoint,
|
||||
are_same_endpoint,
|
||||
super_dir_set,
|
||||
suppress,
|
||||
is_list_or_tuple
|
||||
)
|
||||
from awxkit.api.client import Connection
|
||||
from awxkit.api.registry import URLRegistry
|
||||
from awxkit.config import config
|
||||
import awxkit.exceptions as exc
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_page_registry = URLRegistry()
|
||||
get_registered_page = _page_registry.get
|
||||
|
||||
|
||||
def is_license_invalid(response):
|
||||
if re.match(r".*Invalid license.*", response.text):
|
||||
return True
|
||||
if re.match(r".*Missing 'eula_accepted' property.*", response.text):
|
||||
return True
|
||||
if re.match(r".*'eula_accepted' must be True.*", response.text):
|
||||
return True
|
||||
if re.match(r".*Invalid license data.*", response.text):
|
||||
return True
|
||||
|
||||
|
||||
def is_license_exceeded(response):
|
||||
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):
|
||||
return True
|
||||
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
|
||||
if re.match(r".*License is missing.*", response.text):
|
||||
return True
|
||||
|
||||
|
||||
def is_duplicate_error(response):
|
||||
if re.match(r".*already exists.*", response.text):
|
||||
return True
|
||||
|
||||
|
||||
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):
|
||||
urls = [urls]
|
||||
# Register every methodless page with wildcard method
|
||||
# until more granular page objects exist (options, head, etc.)
|
||||
updated_urls = []
|
||||
for url_method_pair in urls:
|
||||
if isinstance(url_method_pair, str):
|
||||
url = url_method_pair
|
||||
method = '.*'
|
||||
else:
|
||||
url, method = url_method_pair
|
||||
updated_urls.append((url, method))
|
||||
|
||||
page_cls.endpoint = updated_urls[0][0]
|
||||
return _page_registry.register(updated_urls, page_cls)
|
||||
|
||||
|
||||
def objectify_response_json(response):
|
||||
"""return a PseudoNamespace() from requests.Response.json()."""
|
||||
try:
|
||||
json = response.json()
|
||||
except ValueError:
|
||||
json = dict()
|
||||
|
||||
# PseudoNamespace arg must be a dict, and json can be an array.
|
||||
# TODO: Assess if list elements should be PseudoNamespace
|
||||
if isinstance(json, dict):
|
||||
return PseudoNamespace(json)
|
||||
return json
|
||||
|
||||
|
||||
class Page(object):
|
||||
|
||||
endpoint = ''
|
||||
|
||||
def __init__(self, connection=None, *a, **kw):
|
||||
if 'endpoint' in kw:
|
||||
self.endpoint = kw['endpoint']
|
||||
|
||||
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.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):
|
||||
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):
|
||||
value[key] = TentativePage(item, self.connection)
|
||||
return value
|
||||
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:
|
||||
# Update field only. For new field use explicit patch
|
||||
self.patch(**{name: value})
|
||||
else:
|
||||
self.__dict__[name] = value
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self, 'json'):
|
||||
return json.dumps(self.json, indent=4)
|
||||
return str(super(Page, self).__repr__())
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __dir__(self):
|
||||
attrs = super_dir_set(self.__class__)
|
||||
if 'json' in self.__dict__ and hasattr(self.json, 'keys'):
|
||||
attrs.update(self.json.keys())
|
||||
return sorted(attrs)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.json)
|
||||
|
||||
@property
|
||||
def __item_class__(self):
|
||||
"""Returns the class representing a single 'Page' item"""
|
||||
return self.__class__
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, raw):
|
||||
resp = Response()
|
||||
resp._content = bytes(json.dumps(raw), 'utf-8')
|
||||
resp.encoding = 'utf-8'
|
||||
resp.status_code = 200
|
||||
return cls(r=resp)
|
||||
|
||||
def page_identity(self, response, request_json=None, ignore_json_errors=False):
|
||||
"""Takes a `requests.Response` and
|
||||
returns a new __item_class__ instance if the request method is not a get, or returns
|
||||
a __class__ instance if the request path is different than the caller's `endpoint`.
|
||||
"""
|
||||
request_path = response.request.path_url
|
||||
request_method = response.request.method.lower()
|
||||
|
||||
self.last_elapsed = response.elapsed
|
||||
|
||||
if isinstance(request_json, dict) and 'ds' in request_json:
|
||||
ds = request_json.ds
|
||||
else:
|
||||
ds = None
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError as e: # If there was no json to parse
|
||||
data = dict()
|
||||
if (response.text and not ignore_json_errors) or response.status_code not in (200, 202, 204):
|
||||
text = response.text
|
||||
if len(text) > 1024:
|
||||
text = text[:1024] + '... <<< Truncated >>> ...'
|
||||
log.warning(
|
||||
"Unable to parse JSON response ({0.status_code}): {1} - '{2}'".format(response, e, text))
|
||||
|
||||
exc_str = "%s (%s) received" % (
|
||||
http.client.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.client.OK,
|
||||
http.client.CREATED,
|
||||
http.client.ACCEPTED):
|
||||
|
||||
# Not all JSON responses include a URL. Grab it from the request
|
||||
# object, if needed.
|
||||
if 'url' in data:
|
||||
endpoint = data['url']
|
||||
else:
|
||||
endpoint = request_path
|
||||
|
||||
data = objectify_response_json(response)
|
||||
|
||||
if request_method in ('get', 'patch', 'put'):
|
||||
# Update existing resource and return it
|
||||
if are_same_endpoint(self.endpoint, request_path):
|
||||
self.json = data
|
||||
self.r = response
|
||||
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)
|
||||
|
||||
elif response.status_code == http.client.FORBIDDEN:
|
||||
if is_license_invalid(response):
|
||||
raise exc.LicenseInvalid(exc_str, data)
|
||||
elif is_license_exceeded(response):
|
||||
raise exc.LicenseExceeded(exc_str, data)
|
||||
else:
|
||||
raise exc.Forbidden(exc_str, data)
|
||||
|
||||
elif response.status_code == http.client.BAD_REQUEST:
|
||||
if is_license_invalid(response):
|
||||
raise exc.LicenseInvalid(exc_str, data)
|
||||
if is_duplicate_error(response):
|
||||
raise exc.Duplicate(exc_str, data)
|
||||
else:
|
||||
raise exc.BadRequest(exc_str, data)
|
||||
else:
|
||||
raise exc.Unknown(exc_str, data)
|
||||
|
||||
def update_identity(self, obj):
|
||||
"""Takes a `Page` and updates attributes to reflect its content"""
|
||||
self.endpoint = obj.endpoint
|
||||
self.json = obj.json
|
||||
self.last_elapsed = obj.last_elapsed
|
||||
self.r = obj.r
|
||||
return self
|
||||
|
||||
def delete(self):
|
||||
r = self.connection.delete(self.endpoint)
|
||||
with suppress(exc.NoContent):
|
||||
return self.page_identity(r)
|
||||
|
||||
def get(self, all_pages=False, **query_parameters):
|
||||
r = self.connection.get(self.endpoint, query_parameters)
|
||||
page = self.page_identity(r)
|
||||
if all_pages and page.next:
|
||||
paged_results = [r.json()['results']]
|
||||
while page.next:
|
||||
r = self.connection.get(self.next, query_parameters)
|
||||
page = self.page_identity(r)
|
||||
paged_results.append(r.json()['results'])
|
||||
json = r.json()
|
||||
json['results'] = []
|
||||
for page in paged_results:
|
||||
json['results'].extend(page)
|
||||
page = self.__class__.from_json(json)
|
||||
return page
|
||||
|
||||
def head(self):
|
||||
r = self.connection.head(self.endpoint)
|
||||
return self.page_identity(r)
|
||||
|
||||
def options(self):
|
||||
r = self.connection.options(self.endpoint)
|
||||
return self.page_identity(r)
|
||||
|
||||
def patch(self, **json):
|
||||
r = self.connection.patch(self.endpoint, json)
|
||||
return self.page_identity(r, request_json=json)
|
||||
|
||||
def post(self, json={}):
|
||||
r = self.connection.post(self.endpoint, json)
|
||||
return self.page_identity(r, request_json=json)
|
||||
|
||||
def put(self, json=None):
|
||||
"""If a payload is supplied, PUT the payload. If not, submit our existing page JSON as our payload."""
|
||||
json = self.json if json is None else json
|
||||
r = self.connection.put(self.endpoint, json=json)
|
||||
return self.page_identity(r, request_json=json)
|
||||
|
||||
def get_related(self, related_name, **kwargs):
|
||||
assert related_name in self.json.get('related', [])
|
||||
endpoint = self.json['related'][related_name]
|
||||
return self.walk(endpoint, **kwargs)
|
||||
|
||||
def walk(self, endpoint, **kw):
|
||||
page_cls = get_registered_page(endpoint)
|
||||
return page_cls(self.connection, endpoint=endpoint).get(**kw)
|
||||
|
||||
|
||||
_exception_map = {http.client.NO_CONTENT: exc.NoContent,
|
||||
http.client.NOT_FOUND: exc.NotFound,
|
||||
http.client.INTERNAL_SERVER_ERROR: exc.InternalServerError,
|
||||
http.client.BAD_GATEWAY: exc.BadGateway,
|
||||
http.client.METHOD_NOT_ALLOWED: exc.MethodNotAllowed,
|
||||
http.client.UNAUTHORIZED: exc.Unauthorized,
|
||||
http.client.PAYMENT_REQUIRED: exc.PaymentRequired,
|
||||
http.client.CONFLICT: exc.Conflict}
|
||||
|
||||
|
||||
def exception_from_status_code(status_code):
|
||||
return _exception_map.get(status_code, None)
|
||||
|
||||
|
||||
class PageList(object):
|
||||
|
||||
@property
|
||||
def __item_class__(self):
|
||||
"""Returns the class representing a single 'Page' item
|
||||
With an inheritence of OrgListSubClass -> OrgList -> PageList -> Org -> Base -> Page, the following
|
||||
will return the parent class of the current object (e.g. 'Org').
|
||||
|
||||
Obtaining a page type by registered endpoint is highly recommended over using this method.
|
||||
"""
|
||||
mro = inspect.getmro(self.__class__)
|
||||
bl_index = mro.index(PageList)
|
||||
return mro[bl_index + 1]
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
items = []
|
||||
for item in self.json['results']:
|
||||
endpoint = item.get('url')
|
||||
if endpoint is None:
|
||||
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))
|
||||
return items
|
||||
|
||||
def go_to_next(self):
|
||||
if self.next:
|
||||
next_page = self.__class__(self.connection, endpoint=self.next)
|
||||
return next_page.get()
|
||||
|
||||
def go_to_previous(self):
|
||||
if self.previous:
|
||||
prev_page = self.__class__(self.connection, endpoint=self.previous)
|
||||
return prev_page.get()
|
||||
|
||||
def create(self, *a, **kw):
|
||||
return self.__item_class__(self.connection).create(*a, **kw)
|
||||
|
||||
|
||||
class TentativePage(str):
|
||||
|
||||
def __new__(cls, endpoint, connection):
|
||||
return super(TentativePage, cls).__new__(cls, endpoint)
|
||||
|
||||
def __init__(self, endpoint, connection):
|
||||
self.endpoint = endpoint
|
||||
self.connection = connection
|
||||
|
||||
def _create(self):
|
||||
return get_registered_page(
|
||||
self.endpoint)(
|
||||
self.connection,
|
||||
endpoint=self.endpoint)
|
||||
|
||||
def get(self, **params):
|
||||
return self._create().get(**params)
|
||||
|
||||
def create_or_replace(self, **query_parameters):
|
||||
"""Create an object, and if any other item shares the name, delete that one first.
|
||||
|
||||
Generally, requires 'name' of object.
|
||||
|
||||
Exceptions:
|
||||
- Users are looked up by username
|
||||
- Teams need to be looked up by name + organization
|
||||
"""
|
||||
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'
|
||||
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'
|
||||
if query_parameters.get('organization'):
|
||||
if isinstance(query_parameters.get('organization'), int):
|
||||
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)
|
||||
else:
|
||||
page = self.get(name=query_parameters['name'])
|
||||
if page and page.results:
|
||||
for item in page.results:
|
||||
# We found a duplicate item, we will delete it
|
||||
# Some things, like inventory scripts, allow multiple scripts
|
||||
# by same name as long as they have different organization
|
||||
item.delete()
|
||||
# Now that we know that there is no duplicate, we create a new object
|
||||
return self.create(**query_parameters)
|
||||
|
||||
def get_or_create(self, **query_parameters):
|
||||
"""Get an object by this name or id if it exists, otherwise create it.
|
||||
|
||||
Exceptions:
|
||||
- Users are looked up by username
|
||||
- Teams need to be looked up by name + organization
|
||||
"""
|
||||
page = None
|
||||
# look up users by username not name
|
||||
if query_parameters.get('username') and 'users' in self:
|
||||
page = self.get(username=query_parameters['username'])
|
||||
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'))
|
||||
else:
|
||||
page = self.get(
|
||||
name=query_parameters['name'],
|
||||
organization=query_parameters.get('organization').id)
|
||||
else:
|
||||
page = self.get(name=query_parameters['name'])
|
||||
|
||||
elif query_parameters.get('id'):
|
||||
page = self.get(id=query_parameters['id'])
|
||||
if page and page.results:
|
||||
item = page.results.pop()
|
||||
return item.url.get()
|
||||
else:
|
||||
# We did not find it given these params, we will create it instead
|
||||
return self.create(**query_parameters)
|
||||
|
||||
def post(self, payload={}):
|
||||
return self._create().post(payload)
|
||||
|
||||
def put(self):
|
||||
return self._create().put()
|
||||
|
||||
def patch(self, **payload):
|
||||
return self._create().patch(**payload)
|
||||
|
||||
def delete(self):
|
||||
return self._create().delete()
|
||||
|
||||
def options(self):
|
||||
return self._create().options()
|
||||
|
||||
def create(self, *a, **kw):
|
||||
return self._create().create(*a, **kw)
|
||||
|
||||
def payload(self, *a, **kw):
|
||||
return self._create().payload(*a, **kw)
|
||||
|
||||
def create_payload(self, *a, **kw):
|
||||
return self._create().create_payload(*a, **kw)
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self, 'endpoint'):
|
||||
return self.endpoint
|
||||
return super(TentativePage, self).__str__()
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.endpoint == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return self.endpoint != other
|
||||
11
awxkit/awxkit/api/pages/ping.py
Normal file
11
awxkit/awxkit/api/pages/ping.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Ping(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.ping, Ping)
|
||||
209
awxkit/awxkit/api/pages/projects.py
Normal file
209
awxkit/awxkit/api/pages/projects.py
Normal file
@@ -0,0 +1,209 @@
|
||||
import json
|
||||
|
||||
from awxkit.api.pages import Credential, Organization, UnifiedJob, UnifiedJobTemplate
|
||||
from awxkit.utils import filter_by_class, random_title, update_payload, PseudoNamespace
|
||||
from awxkit.api.mixins import HasCreate, HasNotifications, HasCopy, DSAdapter
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.config import config
|
||||
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Project(HasCopy, HasCreate, HasNotifications, UnifiedJobTemplate):
|
||||
|
||||
optional_dependencies = [Credential, Organization]
|
||||
optional_schedule_fields = tuple()
|
||||
|
||||
def payload(self, organization, scm_type='git', **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
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,
|
||||
''))
|
||||
|
||||
if organization is not None:
|
||||
payload.organization = organization.id
|
||||
|
||||
if kwargs.get('credential'):
|
||||
payload.credential = kwargs.get('credential').id
|
||||
|
||||
fields = (
|
||||
'scm_branch',
|
||||
'local_path',
|
||||
'scm_clean',
|
||||
'scm_delete_on_update',
|
||||
'scm_update_cache_timeout',
|
||||
'scm_update_on_launch',
|
||||
'scm_refspec',
|
||||
'allow_override')
|
||||
update_payload(payload, fields, kwargs)
|
||||
|
||||
return payload
|
||||
|
||||
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'):
|
||||
credential = None # ignore incompatible credential from HasCreate dependency injection
|
||||
elif credential in (Credential,):
|
||||
credential = (
|
||||
Credential, dict(
|
||||
credential_type=(
|
||||
True, dict(
|
||||
kind='scm'))))
|
||||
elif credential is True:
|
||||
credential = (
|
||||
Credential, dict(
|
||||
credential_type=(
|
||||
True, dict(
|
||||
kind='scm'))))
|
||||
|
||||
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
|
||||
|
||||
payload = self.payload(
|
||||
organization=organization,
|
||||
scm_type=scm_type,
|
||||
name=name,
|
||||
description=description,
|
||||
scm_url=scm_url,
|
||||
scm_branch=scm_branch,
|
||||
credential=credential,
|
||||
**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):
|
||||
payload = self.create_payload(
|
||||
name=name,
|
||||
description=description,
|
||||
scm_type=scm_type,
|
||||
scm_url=scm_url,
|
||||
scm_branch=scm_branch,
|
||||
organization=organization,
|
||||
credential=credential,
|
||||
**kwargs)
|
||||
self.update_identity(Projects(self.connection).post(payload))
|
||||
|
||||
if kwargs.get('wait', True):
|
||||
self.related.current_update.get().wait_until_completed()
|
||||
return self.get()
|
||||
|
||||
return self
|
||||
|
||||
def update(self):
|
||||
"""Update the project using related->update endpoint."""
|
||||
# get related->launch
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
return jobs_pg.results[0]
|
||||
|
||||
@property
|
||||
def is_successful(self):
|
||||
"""An project is considered successful when:
|
||||
0) scm_type != ""
|
||||
1) unified_job_template.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)
|
||||
|
||||
|
||||
class Projects(page.PageList, Project):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.projects,
|
||||
resources.related_projects], Projects)
|
||||
|
||||
|
||||
class ProjectUpdate(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.project_update, ProjectUpdate)
|
||||
|
||||
|
||||
class ProjectUpdates(page.PageList, ProjectUpdate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.project_updates,
|
||||
resources.project_project_updates], ProjectUpdates)
|
||||
|
||||
|
||||
class ProjectUpdateLaunch(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.project_related_update, ProjectUpdateLaunch)
|
||||
|
||||
|
||||
class ProjectUpdateCancel(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.project_update_cancel, ProjectUpdateCancel)
|
||||
|
||||
|
||||
class Playbooks(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.project_playbooks, Playbooks)
|
||||
21
awxkit/awxkit/api/pages/roles.py
Normal file
21
awxkit/awxkit/api/pages/roles.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Role(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.role, Role)
|
||||
|
||||
|
||||
class Roles(page.PageList, Role):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.roles,
|
||||
resources.related_roles,
|
||||
resources.related_object_roles], Roles)
|
||||
54
awxkit/awxkit/api/pages/schedules.py
Normal file
54
awxkit/awxkit/api/pages/schedules.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from awxkit.api.pages import UnifiedJob
|
||||
from awxkit.api.resources import resources
|
||||
import awxkit.exceptions as exc
|
||||
from awxkit.utils import suppress
|
||||
from . import page
|
||||
from . import base
|
||||
|
||||
|
||||
class Schedule(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.schedule,
|
||||
resources.related_schedule], Schedule)
|
||||
|
||||
|
||||
class Schedules(page.PageList, Schedule):
|
||||
|
||||
def get_zoneinfo(self):
|
||||
return SchedulesZoneInfo(self.connection).get()
|
||||
|
||||
def preview(self, rrule=''):
|
||||
payload = dict(rrule=rrule)
|
||||
return SchedulesPreview(self.connection).post(payload)
|
||||
|
||||
def add_credential(self, cred):
|
||||
with suppress(exc.NoContent):
|
||||
self.related.credentials.post(dict(id=cred.id))
|
||||
|
||||
def remove_credential(self, cred):
|
||||
with suppress(exc.NoContent):
|
||||
self.related.credentials.post(dict(id=cred.id, disassociate=True))
|
||||
|
||||
|
||||
page.register_page([resources.schedules,
|
||||
resources.related_schedules], Schedules)
|
||||
|
||||
|
||||
class SchedulesPreview(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(((resources.schedules_preview, 'post'),), SchedulesPreview)
|
||||
|
||||
|
||||
class SchedulesZoneInfo(base.Base):
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.json[idx]
|
||||
|
||||
|
||||
page.register_page(((resources.schedules_zoneinfo, 'get'),), SchedulesZoneInfo)
|
||||
42
awxkit/awxkit/api/pages/settings.py
Normal file
42
awxkit/awxkit/api/pages/settings.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from awxkit.api.resources import resources
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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')
|
||||
"""
|
||||
base_url = '{0}{1}/'.format(self.endpoint, endpoint)
|
||||
return self.walk(base_url)
|
||||
|
||||
get_setting = get_endpoint
|
||||
|
||||
|
||||
page.register_page(resources.settings, Settings)
|
||||
30
awxkit/awxkit/api/pages/survey_spec.py
Normal file
30
awxkit/awxkit/api/pages/survey_spec.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
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:
|
||||
return item.get('default')
|
||||
|
||||
def get_default_vars(self):
|
||||
default_vars = dict()
|
||||
for item in self.spec:
|
||||
if item.get("default", None):
|
||||
default_vars[item.variable] = item.default
|
||||
return default_vars
|
||||
|
||||
def get_required_vars(self):
|
||||
required_vars = []
|
||||
for item in self.spec:
|
||||
if item.get("required", None):
|
||||
required_vars.append(item.variable)
|
||||
return required_vars
|
||||
|
||||
|
||||
page.register_page([resources.job_template_survey_spec,
|
||||
resources.workflow_job_template_survey_spec], SurveySpec)
|
||||
29
awxkit/awxkit/api/pages/system_job_templates.py
Normal file
29
awxkit/awxkit/api/pages/system_job_templates.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from awxkit.api.mixins import HasNotifications
|
||||
from awxkit.api.pages import UnifiedJobTemplate
|
||||
from awxkit.api.resources import resources
|
||||
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)
|
||||
return jobs_pg.results[0]
|
||||
|
||||
|
||||
page.register_page(resources.system_job_template, SystemJobTemplate)
|
||||
|
||||
|
||||
class SystemJobTemplates(page.PageList, SystemJobTemplate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.system_job_templates, SystemJobTemplates)
|
||||
27
awxkit/awxkit/api/pages/system_jobs.py
Normal file
27
awxkit/awxkit/api/pages/system_jobs.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from awxkit.api.pages import UnifiedJob
|
||||
from awxkit.api.resources import resources
|
||||
from . import page
|
||||
|
||||
|
||||
class SystemJob(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.system_job, SystemJob)
|
||||
|
||||
|
||||
class SystemJobs(page.PageList, SystemJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.system_jobs, SystemJobs)
|
||||
|
||||
|
||||
class SystemJobCancel(UnifiedJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.system_job_cancel, SystemJobCancel)
|
||||
48
awxkit/awxkit/api/pages/teams.py
Normal file
48
awxkit/awxkit/api/pages/teams.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.utils import suppress, random_title, PseudoNamespace
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.api.pages import Organization
|
||||
from awxkit.exceptions import NoContent
|
||||
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class Team(HasCreate, base.Base):
|
||||
|
||||
dependencies = [Organization]
|
||||
|
||||
def add_user(self, user):
|
||||
if isinstance(user, page.Page):
|
||||
user = user.json
|
||||
with suppress(NoContent):
|
||||
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)
|
||||
return payload
|
||||
|
||||
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.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(Teams(self.connection).post(payload))
|
||||
|
||||
|
||||
page.register_page([resources.team,
|
||||
(resources.teams, 'post')], Team)
|
||||
|
||||
|
||||
class Teams(page.PageList, Team):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.teams,
|
||||
resources.related_teams], Teams)
|
||||
86
awxkit/awxkit/api/pages/unified_job_templates.py
Normal file
86
awxkit/awxkit/api/pages/unified_job_templates.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.utils import random_title, update_payload
|
||||
from awxkit.api.mixins import HasStatus
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class UnifiedJobTemplate(HasStatus, base.Base):
|
||||
"""Base class for unified job template pages (e.g. project, inventory_source,
|
||||
and job_template).
|
||||
"""
|
||||
|
||||
optional_schedule_fields = (
|
||||
'extra_data',
|
||||
'diff_mode',
|
||||
'limit',
|
||||
'job_tags',
|
||||
'skip_tags',
|
||||
'job_type',
|
||||
'verbosity',
|
||||
'inventory',
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
# 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',
|
||||
'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):
|
||||
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))
|
||||
|
||||
update_payload(payload, self.optional_schedule_fields, kwargs)
|
||||
|
||||
return self.related.schedules.post(payload)
|
||||
|
||||
@property
|
||||
def is_successful(self):
|
||||
"""An unified_job_template is considered successful when:
|
||||
1) status == 'successful'
|
||||
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
|
||||
|
||||
|
||||
page.register_page(resources.unified_job_template, UnifiedJobTemplate)
|
||||
|
||||
|
||||
class UnifiedJobTemplates(page.PageList, UnifiedJobTemplate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.unified_job_templates, UnifiedJobTemplates)
|
||||
150
awxkit/awxkit/api/pages/unified_jobs.py
Normal file
150
awxkit/awxkit/api/pages/unified_jobs.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from pprint import pformat
|
||||
|
||||
import yaml.parser
|
||||
import yaml.scanner
|
||||
import yaml
|
||||
|
||||
from awxkit.utils import args_string_to_list, seconds_since_date_string
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.api.mixins import HasStatus
|
||||
import awxkit.exceptions as exc
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class UnifiedJob(HasStatus, base.Base):
|
||||
"""Base class for unified job pages (e.g. project_updates, inventory_updates
|
||||
and jobs).
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
# 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']
|
||||
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('%', '%%')
|
||||
|
||||
@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()
|
||||
if str(self.json.get('result_stdout')) == 'stdout capture is missing' and 'stdout' in self.related:
|
||||
ping = self.walk(resources.ping)
|
||||
if self.execution_node != ping.active_node:
|
||||
self.connection.get(self.related.stdout, query_parameters=dict(format='txt_download'))
|
||||
self.get()
|
||||
return self.json.result_stdout.decode()
|
||||
|
||||
def assert_text_in_stdout(self, expected_text, replace_spaces=None, replace_newlines=' '):
|
||||
"""Assert text is found in stdout, and if not raise exception with entire stdout.
|
||||
|
||||
Default behavior is to replace newline characters with a space, but this can be modified, including replacement
|
||||
with ''. Pass replace_newlines=None to disable.
|
||||
|
||||
Additionally, you may replace any ' ' with another character (including ''). This is applied after the newline
|
||||
replacement. Default behavior is to not replace spaces.
|
||||
"""
|
||||
stdout = self.result_stdout
|
||||
if replace_newlines is not None:
|
||||
stdout = stdout.replace('\n', replace_newlines)
|
||||
if replace_spaces is not None:
|
||||
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)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_successful(self):
|
||||
"""Return whether the current has completed successfully.
|
||||
|
||||
This means that:
|
||||
* self.status == 'successful'
|
||||
* self.has_traceback == False
|
||||
* self.failed == False
|
||||
"""
|
||||
return super(UnifiedJob, self).is_successful and not (self.has_traceback or self.failed)
|
||||
|
||||
def wait_until_status(self, status, interval=1, timeout=60, since_job_created=True, **kwargs):
|
||||
if since_job_created:
|
||||
timeout = timeout - seconds_since_date_string(self.created)
|
||||
return super(UnifiedJob, self).wait_until_status(status, interval, timeout, **kwargs)
|
||||
|
||||
def wait_until_completed(self, interval=5, timeout=60 * 8, since_job_created=True, **kwargs):
|
||||
if since_job_created:
|
||||
timeout = timeout - seconds_since_date_string(self.created)
|
||||
return super(UnifiedJob, self).wait_until_completed(interval, timeout, **kwargs)
|
||||
|
||||
@property
|
||||
def has_traceback(self):
|
||||
"""Return whether a traceback has been detected in result_traceback"""
|
||||
try:
|
||||
tb = str(self.result_traceback)
|
||||
except AttributeError:
|
||||
# If record obtained from list view, then traceback isn't given
|
||||
# and result_stdout is only given for some types
|
||||
# we must suppress AttributeError or else it will be mis-interpreted
|
||||
# by __getattr__
|
||||
tb = ''
|
||||
return 'Traceback' in tb
|
||||
|
||||
def cancel(self):
|
||||
cancel = self.get_related('cancel')
|
||||
if not cancel.can_cancel:
|
||||
return
|
||||
try:
|
||||
cancel.post()
|
||||
except exc.MethodNotAllowed as e:
|
||||
# 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)
|
||||
return self.get()
|
||||
|
||||
@property
|
||||
def job_args(self):
|
||||
"""Helper property to return flattened cmdline arg tokens in a list.
|
||||
Flattens arg strings for rough inclusion checks:
|
||||
```assert "thing" in unified_job.job_args```
|
||||
```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.load(arg, Loader=yaml.FullLoader)
|
||||
except (yaml.parser.ParserError, yaml.scanner.ScannerError):
|
||||
return str(arg)
|
||||
|
||||
args = []
|
||||
if not self.json.job_args:
|
||||
return ""
|
||||
for arg in yaml.load(self.json.job_args, Loader=yaml.FullLoader):
|
||||
try:
|
||||
args.append(yaml.load(arg, Loader=yaml.FullLoader))
|
||||
except (yaml.parser.ParserError, yaml.scanner.ScannerError):
|
||||
if arg[0] == '@': # extra var file reference
|
||||
args.append(attempt_yaml_load(arg))
|
||||
elif args[-1] == '-c': # this arg is likely sh arg string
|
||||
args.extend([attempt_yaml_load(item) for item in args_string_to_list(arg)])
|
||||
else:
|
||||
raise
|
||||
return args
|
||||
|
||||
|
||||
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)
|
||||
74
awxkit/awxkit/api/pages/users.py
Normal file
74
awxkit/awxkit/api/pages/users.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.utils import random_title, PseudoNamespace
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.config import config
|
||||
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class User(HasCreate, base.Base):
|
||||
|
||||
def payload(self, **kwargs):
|
||||
payload = PseudoNamespace(
|
||||
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)))
|
||||
)
|
||||
return payload
|
||||
|
||||
def create_payload(self, username='', password='', **kwargs):
|
||||
payload = self.payload(username=username, password=password, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, username='', password='', organization=None, **kwargs):
|
||||
payload = self.create_payload(
|
||||
username=username, password=password, **kwargs)
|
||||
self.password = payload.password
|
||||
|
||||
self.update_identity(Users(self.connection).post(payload))
|
||||
|
||||
if organization:
|
||||
organization.add_user(self)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
page.register_page([resources.user,
|
||||
(resources.users, 'post')], User)
|
||||
|
||||
|
||||
class Users(page.PageList, User):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.users,
|
||||
resources.organization_admins,
|
||||
resources.related_users,
|
||||
resources.user_admin_organizations], Users)
|
||||
|
||||
|
||||
class Me(Users):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.me, Me)
|
||||
39
awxkit/awxkit/api/pages/workflow_job_nodes.py
Normal file
39
awxkit/awxkit/api/pages/workflow_job_nodes.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from awxkit.api.pages import base
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.utils import poll_until, seconds_since_date_string, suppress
|
||||
from awxkit.exceptions import WaitUntilTimeout
|
||||
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)
|
||||
|
||||
with suppress(WaitUntilTimeout):
|
||||
poll_until(self.job_exists, interval=interval, timeout=adjusted_timeout, **kw)
|
||||
|
||||
return self
|
||||
|
||||
def job_exists(self):
|
||||
self.get()
|
||||
try:
|
||||
return self.job
|
||||
except AttributeError:
|
||||
return False
|
||||
|
||||
|
||||
page.register_page(resources.workflow_job_node, WorkflowJobNode)
|
||||
|
||||
|
||||
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)
|
||||
85
awxkit/awxkit/api/pages/workflow_job_template_nodes.py
Normal file
85
awxkit/awxkit/api/pages/workflow_job_template_nodes.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import awxkit.exceptions as exc
|
||||
|
||||
from awxkit.api.pages import base, WorkflowJobTemplate, UnifiedJobTemplate, JobTemplate
|
||||
from awxkit.api.mixins import HasCreate, DSAdapter
|
||||
from awxkit.api.resources import resources
|
||||
from awxkit.utils import update_payload, PseudoNamespace, suppress
|
||||
from . import page
|
||||
|
||||
|
||||
class WorkflowJobTemplateNode(HasCreate, base.Base):
|
||||
|
||||
dependencies = [WorkflowJobTemplate, UnifiedJobTemplate]
|
||||
|
||||
def payload(self, workflow_job_template, unified_job_template, **kwargs):
|
||||
payload = PseudoNamespace(workflow_job_template=workflow_job_template.id,
|
||||
unified_job_template=unified_job_template.id)
|
||||
|
||||
optional_fields = ('diff_mode', 'extra_data', 'limit', 'job_tags', 'job_type', 'skip_tags', 'verbosity',
|
||||
'extra_data')
|
||||
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
|
||||
for resource in ('credential', 'inventory'):
|
||||
if resource in kwargs:
|
||||
payload[resource] = kwargs[resource].id
|
||||
|
||||
return payload
|
||||
|
||||
def create_payload(self, workflow_job_template=WorkflowJobTemplate, unified_job_template=JobTemplate, **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 _add_node(self, endpoint, unified_job_template):
|
||||
node = endpoint.post(dict(unified_job_template=unified_job_template.id))
|
||||
node.create_and_update_dependencies(self.ds.workflow_job_template, unified_job_template)
|
||||
return node
|
||||
|
||||
def add_always_node(self, unified_job_template):
|
||||
return self._add_node(self.related.always_nodes, unified_job_template)
|
||||
|
||||
def add_failure_node(self, unified_job_template):
|
||||
return self._add_node(self.related.failure_nodes, unified_job_template)
|
||||
|
||||
def add_success_node(self, unified_job_template):
|
||||
return self._add_node(self.related.success_nodes, unified_job_template)
|
||||
|
||||
def add_credential(self, credential):
|
||||
with suppress(exc.NoContent):
|
||||
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))
|
||||
|
||||
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))
|
||||
|
||||
|
||||
page.register_page([resources.workflow_job_template_node,
|
||||
(resources.workflow_job_template_nodes, 'post')], WorkflowJobTemplateNode)
|
||||
|
||||
|
||||
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)
|
||||
100
awxkit/awxkit/api/pages/workflow_job_templates.py
Normal file
100
awxkit/awxkit/api/pages/workflow_job_templates.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import json
|
||||
|
||||
from awxkit.api.mixins import HasCreate, HasNotifications, HasSurvey, HasCopy, DSAdapter
|
||||
from awxkit.api.pages import Organization, UnifiedJobTemplate
|
||||
from awxkit.utils import filter_by_class, not_provided, update_payload, random_title, suppress, PseudoNamespace
|
||||
from awxkit.api.resources import resources
|
||||
import awxkit.exceptions as exc
|
||||
|
||||
from . import base
|
||||
from . import page
|
||||
|
||||
|
||||
class WorkflowJobTemplate(HasCopy, HasCreate, HasNotifications, HasSurvey, UnifiedJobTemplate):
|
||||
|
||||
optional_dependencies = [Organization]
|
||||
|
||||
def launch(self, payload={}):
|
||||
"""Launch using related->launch endpoint."""
|
||||
# get related->launch
|
||||
launch_pg = self.get_related('launch')
|
||||
|
||||
# launch the workflow_job_template
|
||||
result = launch_pg.post(payload)
|
||||
|
||||
# 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/" % \
|
||||
(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))
|
||||
|
||||
optional_fields = ("allow_simultaneous", "ask_variables_on_launch", "survey_enabled")
|
||||
update_payload(payload, optional_fields, kwargs)
|
||||
|
||||
extra_vars = kwargs.get('extra_vars', not_provided)
|
||||
if extra_vars != not_provided:
|
||||
if isinstance(extra_vars, dict):
|
||||
extra_vars = json.dumps(extra_vars)
|
||||
payload.update(extra_vars=extra_vars)
|
||||
|
||||
if kwargs.get('organization'):
|
||||
payload.organization = kwargs.get('organization').id
|
||||
|
||||
if kwargs.get('inventory'):
|
||||
payload.inventory = kwargs.get('inventory').id
|
||||
if kwargs.get('ask_inventory_on_launch'):
|
||||
payload.ask_inventory_on_launch = kwargs.get('ask_inventory_on_launch')
|
||||
|
||||
return payload
|
||||
|
||||
def create_payload(self, name='', description='', organization=None, **kwargs):
|
||||
self.create_and_update_dependencies(*filter_by_class((organization, Organization)))
|
||||
organization = self.ds.organization if organization else None
|
||||
payload = self.payload(name=name, description=description, organization=organization, **kwargs)
|
||||
payload.ds = DSAdapter(self.__class__.__name__, self._dependency_store)
|
||||
return payload
|
||||
|
||||
def create(self, name='', description='', organization=None, **kwargs):
|
||||
payload = self.create_payload(name=name, description=description, organization=organization, **kwargs)
|
||||
return self.update_identity(WorkflowJobTemplates(self.connection).post(payload))
|
||||
|
||||
def add_label(self, label):
|
||||
if isinstance(label, page.Page):
|
||||
label = label.json
|
||||
with suppress(exc.NoContent):
|
||||
self.related.labels.post(label)
|
||||
|
||||
|
||||
page.register_page([resources.workflow_job_template,
|
||||
(resources.workflow_job_templates, 'post'),
|
||||
(resources.workflow_job_template_copy, 'post')], WorkflowJobTemplate)
|
||||
|
||||
|
||||
class WorkflowJobTemplates(page.PageList, WorkflowJobTemplate):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.workflow_job_templates], WorkflowJobTemplates)
|
||||
|
||||
|
||||
class WorkflowJobTemplateLaunch(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page(resources.workflow_job_template_launch, WorkflowJobTemplateLaunch)
|
||||
|
||||
|
||||
class WorkflowJobTemplateCopy(base.Base):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.workflow_job_template_copy], WorkflowJobTemplateCopy)
|
||||
38
awxkit/awxkit/api/pages/workflow_jobs.py
Normal file
38
awxkit/awxkit/api/pages/workflow_jobs.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from awxkit.api.pages import UnifiedJob
|
||||
from awxkit.api.resources import resources
|
||||
from . import page
|
||||
|
||||
|
||||
class WorkflowJob(UnifiedJob):
|
||||
|
||||
def __str__(self):
|
||||
# TODO: Update after endpoint's fields are finished filling out
|
||||
return super(UnifiedJob, self).__str__()
|
||||
|
||||
def relaunch(self, payload={}):
|
||||
result = self.related.relaunch.post(payload)
|
||||
return self.walk(result.url)
|
||||
|
||||
@property
|
||||
def result_stdout(self):
|
||||
# workflow jobs do not have result_stdout
|
||||
# which is problematic for the UnifiedJob.is_successful reliance on
|
||||
# related stdout endpoint.
|
||||
if 'result_stdout' not in self.json:
|
||||
return 'Unprovided AWX field.'
|
||||
else:
|
||||
return super(WorkflowJob, self).result_stdout
|
||||
|
||||
|
||||
page.register_page(resources.workflow_job, WorkflowJob)
|
||||
|
||||
|
||||
class WorkflowJobs(page.PageList, WorkflowJob):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
page.register_page([resources.workflow_jobs,
|
||||
resources.workflow_job_template_jobs,
|
||||
resources.job_template_slice_workflow_jobs],
|
||||
WorkflowJobs)
|
||||
Reference in New Issue
Block a user