Source code for kollacli.common.inventory

# Copyright(c) 2016, Oracle and/or its affiliates.  All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.
import json
import jsonpickle
import logging
import os
import tempfile
import traceback
import uuid

import kollacli.i18n as u

from kollacli.api.exceptions import FailedOperation
from kollacli.api.exceptions import HostError
from kollacli.api.exceptions import InvalidArgument
from kollacli.api.exceptions import InvalidConfiguration
from kollacli.api.exceptions import MissingArgument
from kollacli.api.exceptions import NotInInventory
from kollacli.common.sshutils import ssh_setup_host
from kollacli.common.utils import get_admin_user
from kollacli.common.utils import get_ansible_command
from kollacli.common.utils import get_group_vars_dir
from kollacli.common.utils import get_host_vars_dir
from kollacli.common.utils import get_kollacli_etc
from kollacli.common.utils import run_cmd
from kollacli.common.utils import sync_read_file
from kollacli.common.utils import sync_write_file

ANSIBLE_SSH_USER = 'ansible_ssh_user'
ANSIBLE_CONNECTION = 'ansible_connection'
ANSIBLE_BECOME = 'ansible_become'

INVENTORY_PATH = 'ansible/inventory.json'

COMPUTE_GRP_NAME = 'compute'
CONTROL_GRP_NAME = 'control'
NETWORK_GRP_NAME = 'network'
STORAGE_GRP_NAME = 'storage'
DATABASE_GRP_NAME = 'database'

DEPLOY_GROUPS = [
    COMPUTE_GRP_NAME,
    CONTROL_GRP_NAME,
    NETWORK_GRP_NAME,
    STORAGE_GRP_NAME,
    DATABASE_GRP_NAME,
    ]

SERVICES = {
    'ceilometer':   ['ceilometer-alarm-evaluator', 'ceilometer-alarm-notifier',
                     'ceilometer-api', 'ceilometer-central',
                     'ceilometer-collector', 'ceilometer-notification'],
    'cinder':       ['cinder-api', 'cinder-scheduler', 'cinder-backup',
                     'cinder-volume'],
    'glance':       ['glance-api', 'glance-registry'],
    'haproxy':      [],
    'heat':         ['heat-api', 'heat-api-cfn', 'heat-engine'],
    'horizon':      [],
    'keystone':     [],
    'memcached':    [],
    'murano':       ['murano-api', 'murano-engine'],
    'mysqlcluster': ['mysqlcluster-api', 'mysqlcluster-mgmt',
                     'mysqlcluster-ndb'],
    'neutron':      ['neutron-server', 'neutron-agents'],
    'nova':         ['nova-api', 'nova-conductor', 'nova-consoleauth',
                     'nova-novncproxy', 'nova-scheduler'],
    'rabbitmq':     [],
    'swift':        ['swift-proxy-server', 'swift-account-server',
                     'swift-container-server', 'swift-object-server'],
    }

DEFAULT_GROUPS = {
    'ceilometer':               CONTROL_GRP_NAME,
    'cinder':                   CONTROL_GRP_NAME,
    'glance':                   CONTROL_GRP_NAME,
    'haproxy':                  CONTROL_GRP_NAME,
    'heat':                     CONTROL_GRP_NAME,
    'horizon':                  CONTROL_GRP_NAME,
    'keystone':                 CONTROL_GRP_NAME,
    'memcached':                CONTROL_GRP_NAME,
    'murano':                   CONTROL_GRP_NAME,
    'mysqlcluster':             CONTROL_GRP_NAME,
    'neutron':                  NETWORK_GRP_NAME,
    'nova':                     CONTROL_GRP_NAME,
    'rabbitmq':                 CONTROL_GRP_NAME,
    'swift':                    CONTROL_GRP_NAME,
    }

DEFAULT_OVERRIDES = {
    'cinder-backup':            STORAGE_GRP_NAME,
    'cinder-volume':            STORAGE_GRP_NAME,
    'mysqlcluster-ndb':         DATABASE_GRP_NAME,
    'neutron-server':           CONTROL_GRP_NAME,
    'swift-account-server':     STORAGE_GRP_NAME,
    'swift-container-server':   STORAGE_GRP_NAME,
    'swift-object-server':      STORAGE_GRP_NAME,
    }


# these groups cannot be deleted, they are required by kolla
PROTECTED_GROUPS = [COMPUTE_GRP_NAME]

LOG = logging.getLogger(__name__)


[docs]def remove_temp_inventory(path): """remove temp inventory file and its parent directory""" if path: if os.path.exists(path): os.remove(path) dirpath = os.path.dirname(path) if os.path.exists(dirpath): os.rmdir(dirpath)
[docs]class Host(object): class_version = 1 def __init__(self, hostname): self.name = hostname self.alias = '' self.is_mgmt = False self.hypervisor = '' self.vars = {} self.version = self.__class__.class_version
[docs] def get_vars(self): return self.vars.copy()
[docs] def set_var(self, name, value): self.vars[name] = value
[docs] def upgrade(self): pass
[docs]class HostGroup(object): class_version = 1 def __init__(self, name): self.name = name self.hostnames = [] self.vars = {} self.version = self.__class__.class_version
[docs] def upgrade(self): pass
[docs] def add_host(self, host): if host.name not in self.hostnames: self.hostnames.append(host.name)
[docs] def remove_host(self, host): if host.name in self.hostnames: self.hostnames.remove(host.name)
[docs] def get_hostnames(self): return self.hostnames
[docs] def get_vars(self): return self.vars.copy()
[docs] def set_var(self, name, value): self.vars[name] = value
[docs] def clear_var(self, name): if name in self.vars: del self.vars[name]
[docs] def set_remote(self, remote_flag): self.set_var(ANSIBLE_BECOME, 'yes') if remote_flag: # set the ssh info for all the servers in the group self.set_var(ANSIBLE_SSH_USER, get_admin_user()) self.clear_var(ANSIBLE_CONNECTION) else: # remove ssh info, add local connection type self.set_var(ANSIBLE_CONNECTION, 'local') self.clear_var(ANSIBLE_SSH_USER)
[docs]class Service(object): class_version = 1 def __init__(self, name): self.name = name self._sub_servicenames = [] self._groupnames = [] self._vars = {} self.version = self.__class__.class_version
[docs] def upgrade(self): pass
[docs] def add_groupname(self, groupname): if groupname is not None and groupname not in self._groupnames: self._groupnames.append(groupname)
[docs] def remove_groupname(self, groupname): if groupname in self._groupnames: self._groupnames.remove(groupname)
[docs] def get_groupnames(self): return self._groupnames
[docs] def get_sub_servicenames(self): return self._sub_servicenames
[docs] def add_sub_servicename(self, sub_servicename): if sub_servicename not in self._sub_servicenames: self._sub_servicenames.append(sub_servicename)
[docs] def get_vars(self): return self._vars.copy()
[docs]class SubService(object): class_version = 1 def __init__(self, name): self.name = name # groups and parent services are mutually exclusive self._groupnames = [] self._parent_servicename = None self._vars = {} self.version = self.__class__.class_version
[docs] def upgrade(self): pass
[docs] def add_groupname(self, groupname): if groupname not in self._groupnames: self._groupnames.append(groupname)
[docs] def remove_groupname(self, groupname): if groupname in self._groupnames: self._groupnames.remove(groupname) if not self._groupnames: # no groups left, re-associate to the parent for servicename in SERVICES: if self.name in SERVICES[servicename]: self.set_parent_servicename(servicename) break
[docs] def get_groupnames(self): return self._groupnames
[docs] def set_parent_servicename(self, parent_svc_name): self._parent_servicename = parent_svc_name
[docs] def get_parent_servicename(self): return self._parent_servicename
[docs] def get_vars(self): return self.vars.copy()
[docs]class Inventory(object): class_version = 3 """class version history 1: initial release """ def __init__(self): self._groups = {} # kv = name:object self._hosts = {} # kv = name:object self._services = {} # kv = name:object self._sub_services = {} # kv = name:object self.vars = {} self.version = self.__class__.class_version self.remote_mode = True # initialize the inventory to its defaults self._create_default_inventory()
[docs] def upgrade(self): if self.version <= 1: # upgrade from inventory v1 # add ceilometer to inventory svc_name = 'ceilometer' svc = self.create_service(svc_name) # associate ceilometer with all groups that heat is in. clone_svc = self.get_service('heat') groups = clone_svc.get_groupnames() for group in groups: svc.add_groupname(group) # stitch sub-service to service and set override # groups for sub_svc_name in SERVICES[svc_name]: sub_svc = self.create_sub_service(sub_svc_name) sub_svc.set_parent_servicename(svc_name) svc.add_sub_servicename(sub_svc_name) if self.version <= 2: # upgrade from inventory v2 # some sub-services may be missing their parent associations. # they are now needed in v3. for svc in self.get_services(): for sub_svcname in svc.get_sub_servicenames(): sub_svc = self.get_sub_service(sub_svcname) if not sub_svc.get_parent_servicename(): sub_svc.set_parent_servicename(svc.name) # update the version and save upgraded inventory file self.version = self.__class__.class_version Inventory.save(self)
@staticmethod
[docs] def load(): """load the inventory from a pickle file""" inventory_path = os.path.join(get_kollacli_etc(), INVENTORY_PATH) data = '' try: if os.path.exists(inventory_path): data = sync_read_file(inventory_path) # The inventory path changed between v1 and v2. Need to change # path throughout the inventory. This has to be done before # the pickle decode. if 'kollacli.common.inventory' not in data: data = data.replace( '"py/object": "kollacli.ansible.inventory.', '"py/object": "kollacli.common.inventory.') if data.strip(): inventory = jsonpickle.decode(data) # upgrade version handling if inventory.version != inventory.class_version: inventory.upgrade() else: inventory = Inventory() except Exception: raise FailedOperation( u._('Loading inventory failed. : {error}') .format(error=traceback.format_exc())) return inventory
@staticmethod
[docs] def save(inventory): """Save the inventory in a pickle file""" inventory_path = os.path.join(get_kollacli_etc(), INVENTORY_PATH) try: # multiple trips thru json to render a readable inventory file data = jsonpickle.encode(inventory) data_str = json.loads(data) pretty_data = json.dumps(data_str, indent=4) sync_write_file(inventory_path, pretty_data) except Exception as e: raise FailedOperation( u._('Saving inventory failed. : {error}') .format(error=str(e)))
def _create_default_inventory(self): # create the default groups for groupname in DEPLOY_GROUPS: self.add_group(groupname) # create the default services/sub_services & their default groups for svcname in SERVICES: svc = self.create_service(svcname) default_grpname = DEFAULT_GROUPS[svcname] svc.add_groupname(default_grpname) sub_svcnames = SERVICES[svcname] if sub_svcnames: for sub_svcname in sub_svcnames: # create a subservice svc.add_sub_servicename(sub_svcname) sub_svc = self.create_sub_service(sub_svcname) sub_svc.set_parent_servicename(svc.name) if sub_svc.name in DEFAULT_OVERRIDES: sub_svc.add_groupname(DEFAULT_OVERRIDES[sub_svc.name])
[docs] def get_hosts(self): return self._hosts.values()
[docs] def get_hostnames(self): return list(self._hosts.keys())
[docs] def get_host(self, hostname): host = None if hostname in self._hosts: host = self._hosts[hostname] return host
[docs] def add_host(self, hostname, groupname=None): """add host if groupname is none, create a new host if group name is not none, add host to group """ if groupname and groupname not in self._groups: raise NotInInventory(u._('Group'), groupname) if groupname and hostname not in self._hosts: # if a groupname is specified, the host must already exist raise NotInInventory(u._('Host'), hostname) if not groupname and not self.remote_mode and len(self._hosts) >= 1: raise InvalidConfiguration( u._('Cannot have more than one host when in local deploy ' 'mode.')) changed = False # create new host if it doesn't exist host = Host(hostname) if hostname not in self.get_hostnames(): # a new host is being added to the inventory changed = True self._hosts[hostname] = host # a host is to be added to an existing group elif groupname: group = self._groups[groupname] if hostname not in group.get_hostnames(): changed = True group.add_host(host) return changed
[docs] def remove_all_hosts(self): """remove all hosts.""" hostnamess = self.get_hostnames() for hostname in hostnamess: self.remove_host(hostname)
[docs] def remove_host(self, hostname, groupname=None): """remove host if groupname is none, delete host if group name is not none, remove host from group """ changed = False if groupname and groupname not in self._groups: raise NotInInventory(u._('Group'), groupname) if hostname not in self._hosts: return changed changed = True host = self._hosts[hostname] groups = self.get_groups(host) for group in groups: if not groupname or groupname == group.name: group.remove_host(host) host_vars = os.path.join(get_host_vars_dir(), hostname) if os.path.exists(host_vars): os.remove(host_vars) if not groupname: del self._hosts[hostname] return changed
[docs] def setup_hosts(self, hosts_info): """setup multiple hosts hosts_info is a dict of format: {'hostname1': { 'password': password 'uname': user_name } } The uname entry is optional. """ failed_hosts = {} for hostname, host_info in hosts_info.items(): host = self.get_host(hostname) if not host: failed_hosts[hostname] = u._("Host doesn't exist.") continue if not host_info or 'password' not in host_info: failed_hosts[hostname] = u._('No password in yml file.') continue passwd = host_info['password'] uname = None if 'uname' in host_info: uname = host_info['uname'] try: self.setup_host(hostname, passwd, uname) except Exception as e: failed_hosts[hostname] = '%s' % e if failed_hosts: summary = '\n' for hostname, err in failed_hosts.items(): summary = summary + '- %s: %s\n' % (hostname, err) raise HostError( u._('Not all hosts were set up. : {reasons}') .format(reasons=summary)) else: LOG.info(u._LI('All hosts were successfully set up.'))
[docs] def setup_host(self, hostname, password, uname=None): try: LOG.info( u._LI('Starting setup of host ({host}).') .format(host=hostname)) ssh_setup_host(hostname, password, uname) check_ok, msg = self.ssh_check_host(hostname) if not check_ok: raise Exception(u._('Post-setup ssh check failed. {err}') .format(err=msg)) LOG.info(u._LI('Host ({host}) setup succeeded.') .format(host=hostname)) except Exception as e: raise HostError( u._('Host ({host}) setup failed : {error}') .format(host=hostname, error=str(e))) return True
[docs] def ssh_check_hosts(self, hostnames): """ssh check for hosts return {hostname: {'success': True|False, 'msg': message}} """ summary = {} for hostname in hostnames: is_ok, msg = self.ssh_check_host(hostname) summary[hostname] = {} summary[hostname]['success'] = is_ok summary[hostname]['msg'] = msg return summary
[docs] def ssh_check_host(self, hostname): err_msg, output = self.run_ansible_command('-m ping', hostname) is_ok = True if err_msg: is_ok = False msg = ( u._('Host ({host}) ssh check failed. : {error} {message}') .format(host=hostname, error=err_msg, message=output)) else: msg = (u._LI('Host ({host}) ssh check succeeded.') .format(host=hostname)) return is_ok, msg
[docs] def run_ansible_command(self, ansible_command, hostname): err_msg = None command_string = '/usr/bin/sudo -u %s %s -vvv' % \ (get_admin_user(), get_ansible_command()) gen_file_path = self.create_json_gen_file() cmd = '%s %s -i %s %s' % (command_string, hostname, gen_file_path, ansible_command) try: err_msg, output = run_cmd(cmd, False) except Exception as e: err_msg = str(e) finally: self.remove_json_gen_file(gen_file_path) return err_msg, output
[docs] def add_group(self, groupname): # Group names cannot overlap with service names: if groupname in self._services or groupname in self._sub_services: raise InvalidArgument( u._('Invalid group name. A service name ' 'cannot be used for a group name.')) if groupname not in self._groups: self._groups[groupname] = HostGroup(groupname) group = self._groups[groupname] group.set_remote(self.remote_mode) return group
[docs] def remove_group(self, groupname): if groupname in PROTECTED_GROUPS: raise InvalidArgument( u._('Cannot remove {group} group. It is required by kolla.') .format(group=groupname)) # remove group from services & subservices for service in self._services.values(): service.remove_groupname(groupname) for subservice in self._sub_services.values(): subservice.remove_groupname(groupname) group_vars = os.path.join(get_group_vars_dir(), groupname) if os.path.exists(group_vars) and groupname != '__GLOBAL__': os.remove(group_vars) if groupname in self._groups: del self._groups[groupname]
[docs] def get_group(self, groupname): group = None if groupname in self._groups: group = self._groups[groupname] return group
[docs] def get_groupnames(self): return list(self._groups.keys())
[docs] def get_groups(self, host=None): """return all groups containing host if hosts is none, return all groups in inventory """ groups = [] if not host: groups = self._groups.values() else: for group in self._groups.values(): if host.name in group.get_hostnames(): groups.append(group) return groups
[docs] def get_host_groups(self): """return { hostname : [groupnames] }""" host_groups = {} for host in self._hosts.values(): host_groups[host.name] = [] groups = self.get_groups(host) for group in groups: host_groups[host.name].append(group.name) return host_groups
[docs] def get_group_services(self): """get groups and their services return { groupname: [servicenames] } """ group_services = {} for group in self.get_groups(): group_services[group.name] = [] for svc in self.get_services(): for groupname in svc.get_groupnames(): group_services[groupname].append(svc.name) for sub_svc in self.get_sub_services(): for groupname in sub_svc.get_groupnames(): group_services[groupname].append(sub_svc.name) return group_services
[docs] def get_group_hosts(self): """return { groupname : [hostnames] }""" group_hosts = {} for group in self.get_groups(): group_hosts[group.name] = [] for hostname in group.get_hostnames(): group_hosts[group.name].append(hostname) return group_hosts
[docs] def create_service(self, servicename): if servicename not in self._services: service = Service(servicename) self._services[servicename] = service return self._services[servicename]
[docs] def delete_service(self, servicename): if servicename in self._services: del self._services[servicename]
[docs] def get_services(self): return self._services.values()
[docs] def get_service(self, servicename): service = None if servicename in self._services: service = self._services[servicename] return service
[docs] def add_group_to_service(self, groupname, servicename): if groupname not in self._groups: raise NotInInventory(u._('Group'), groupname) if servicename in self._services: service = self.get_service(servicename) service.add_groupname(groupname) elif servicename in self._sub_services: sub_service = self.get_sub_service(servicename) sub_service.add_groupname(groupname) else: raise NotInInventory(u._('Service'), servicename)
[docs] def remove_group_from_service(self, groupname, servicename): if groupname not in self._groups: raise NotInInventory(u._('Group'), groupname) if servicename in self._services: service = self.get_service(servicename) service.remove_groupname(groupname) elif servicename in self._sub_services: sub_service = self.get_sub_service(servicename) sub_service.remove_groupname(groupname) else: raise NotInInventory(u._('Service'), servicename)
[docs] def create_sub_service(self, sub_servicename): if sub_servicename not in self._sub_services: sub_service = SubService(sub_servicename) self._sub_services[sub_servicename] = sub_service return self._sub_services[sub_servicename]
[docs] def delete_sub_service(self, sub_servicename): if sub_servicename in self._sub_services: del self._sub_services[sub_servicename]
[docs] def get_sub_services(self): return self._sub_services.values()
[docs] def get_sub_service(self, sub_servicename): sub_service = None if sub_servicename in self._sub_services: sub_service = self._sub_services[sub_servicename] return sub_service
[docs] def get_service_sub_services(self): """get services and their sub_services return { servicename: [sub_servicenames] } """ svc_sub_svcs = {} for service in self.get_services(): svc_sub_svcs[service.name] = [] svc_sub_svcs[service.name].extend(service.get_sub_servicenames()) return svc_sub_svcs
[docs] def set_deploy_mode(self, remote_flag): if not remote_flag and len(self._hosts) > 1: raise InvalidConfiguration( u._('Cannot set local deploy mode when multiple hosts exist.')) self.remote_mode = remote_flag for group in self.get_groups(): group.set_remote(remote_flag)
[docs] def get_ansible_json(self, inventory_filter=None): """generate json inventory for ansible The hosts and groups added to the json output for ansible will be filtered by the hostnames and groupnames in the deploy filters. This allows a more targeted deploy to a specific set of hosts or groups. typical ansible json format: { 'group': { 'hosts': [ '192.168.28.71', '192.168.28.72' ], 'vars': { 'ansible_ssh_user': 'johndoe', 'ansible_ssh_private_key_file': '~/.ssh/mykey', 'example_variable': 'value' } 'children': [ 'marietta', '5points' ] }, '_meta': { 'hostvars': { '192.168.28.71': { 'host_specific_var': 'bar' }, '192.168.28.72': { 'host_specific_var': 'foo' } } } } """ jdict = {} # if no filter provided, use all groups, all hosts deploy_hostnames = self.get_hostnames() deploy_groupnames = self.get_groupnames() if inventory_filter: if 'deploy_hosts' in inventory_filter: deploy_hostnames = inventory_filter['deploy_hosts'] if 'deploy_groups' in inventory_filter: deploy_groupnames = inventory_filter['deploy_groups'] # add hostgroups for group in self.get_groups(): jdict[group.name] = {} jdict[group.name]['hosts'] = [] if group.name in deploy_groupnames: jdict[group.name]['hosts'] = \ self._filter_hosts(group.get_hostnames(), deploy_hostnames) jdict[group.name]['children'] = [] jdict[group.name]['vars'] = group.get_vars() # add top-level services and what groups they are in for service in self.get_services(): jdict[service.name] = {} jdict[service.name]['children'] = service.get_groupnames() # add sub-services and their groups for sub_svc in self.get_sub_services(): jdict[sub_svc.name] = {} groupnames = sub_svc.get_groupnames() if groupnames: # sub-service is associated with a group(s) jdict[sub_svc.name]['children'] = groupnames else: # sub-service is associated with parent service jdict[sub_svc.name]['children'] = \ [sub_svc.get_parent_servicename()] # temporarily create group containing all hosts. this is needed for # ansible commands that are performed on hosts not yet in groups. group = self.add_group('__GLOBAL__') jdict[group.name] = {} jdict[group.name]['hosts'] = deploy_hostnames jdict[group.name]['vars'] = group.get_vars() self.remove_group(group.name) # process hosts vars jdict['_meta'] = {} jdict['_meta']['hostvars'] = {} for hostname in deploy_hostnames: host = self.get_host(hostname) if host: jdict['_meta']['hostvars'][hostname] = host.get_vars() return json.dumps(jdict)
def _filter_hosts(self, initial_hostnames, deploy_hostnames): """filter out hosts not in deploy hosts Must preserve the ordering of hosts in the group. """ filtered_hostnames = [] for hostname in initial_hostnames: if hostname in deploy_hostnames: filtered_hostnames.append(hostname) return filtered_hostnames
[docs] def create_json_gen_file(self, inventory_filter=None): """create json inventory file using filter ({}) The inventory will be placed in a directory in /tmp, with the directory name of form kolla_uuid.py, where uuid is a unique deployment id. return path to filtered json generator file """ json_out = self.get_ansible_json(inventory_filter) deploy_id = str(uuid.uuid4()) dirname = 'kolla_%s' % deploy_id dirpath = os.path.join(tempfile.gettempdir(), dirname) os.mkdir(dirpath) json_gen_path = os.path.join(dirpath, 'temp_inventory.py') with open(json_gen_path, 'w') as json_gen_file: json_gen_file.write('#!/usr/bin/env python\n') # the quotes here are significant. The json_out has double quotes # embedded in it so single quotes are needed to wrap it. json_gen_file.write("print('%s')" % json_out) # set executable by group os.chmod(json_gen_path, 0o555) # nosec return json_gen_path
[docs] def remove_json_gen_file(self, path): remove_temp_inventory(path)
[docs] def validate_hostnames(self, hostnames): if not hostnames: raise MissingArgument(u._('Host name(s)')) invalid_hosts = [] for hostname in hostnames: if hostname not in self._hosts: invalid_hosts.append(hostname) if invalid_hosts: raise NotInInventory(u._('Host'), invalid_hosts)
[docs] def validate_groupnames(self, groupnames): if not groupnames: raise MissingArgument(u._('Group name(s)')) invalid_groups = [] for groupname in groupnames: if groupname not in self._groups: invalid_groups.append(groupname) if invalid_groups: raise NotInInventory(u._('Group'), invalid_groups)
[docs] def validate_servicenames(self, servicenames): if not servicenames: raise MissingArgument(u._('Service name(s)')) invalid_services = [] for servicename in servicenames: if (servicename not in self._services and servicename not in self._sub_services): invalid_services.append(servicename) if invalid_services: raise NotInInventory(u._('Service'), invalid_services)