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) class InventoryCopy(base.Base): pass page.register_page(resources.inventory_copy, InventoryCopy)