kolla-cli/kollacli/common/inventory.py

915 lines
31 KiB
Python

# 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__)
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)
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
def get_vars(self):
return self.vars.copy()
def set_var(self, name, value):
self.vars[name] = value
def upgrade(self):
pass
class HostGroup(object):
class_version = 1
def __init__(self, name):
self.name = name
self.hostnames = []
self.vars = {}
self.version = self.__class__.class_version
def upgrade(self):
pass
def add_host(self, host):
if host.name not in self.hostnames:
self.hostnames.append(host.name)
def remove_host(self, host):
if host.name in self.hostnames:
self.hostnames.remove(host.name)
def get_hostnames(self):
return self.hostnames
def get_vars(self):
return self.vars.copy()
def set_var(self, name, value):
self.vars[name] = value
def clear_var(self, name):
if name in self.vars:
del self.vars[name]
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)
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
def upgrade(self):
pass
def add_groupname(self, groupname):
if groupname is not None and groupname not in self._groupnames:
self._groupnames.append(groupname)
def remove_groupname(self, groupname):
if groupname in self._groupnames:
self._groupnames.remove(groupname)
def get_groupnames(self):
return self._groupnames
def get_sub_servicenames(self):
return self._sub_servicenames
def add_sub_servicename(self, sub_servicename):
if sub_servicename not in self._sub_servicenames:
self._sub_servicenames.append(sub_servicename)
def get_vars(self):
return self._vars.copy()
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
def upgrade(self):
pass
def add_groupname(self, groupname):
if groupname not in self._groupnames:
self._groupnames.append(groupname)
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
def get_groupnames(self):
return self._groupnames
def set_parent_servicename(self, parent_svc_name):
self._parent_servicename = parent_svc_name
def get_parent_servicename(self):
return self._parent_servicename
def get_vars(self):
return self.vars.copy()
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()
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
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
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])
def get_hosts(self):
return self._hosts.values()
def get_hostnames(self):
return list(self._hosts.keys())
def get_host(self, hostname):
host = None
if hostname in self._hosts:
host = self._hosts[hostname]
return host
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
def remove_all_hosts(self):
"""remove all hosts."""
hostnamess = self.get_hostnames()
for hostname in hostnamess:
self.remove_host(hostname)
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
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.'))
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
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
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
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
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
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]
def get_group(self, groupname):
group = None
if groupname in self._groups:
group = self._groups[groupname]
return group
def get_groupnames(self):
return list(self._groups.keys())
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
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
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
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
def create_service(self, servicename):
if servicename not in self._services:
service = Service(servicename)
self._services[servicename] = service
return self._services[servicename]
def delete_service(self, servicename):
if servicename in self._services:
del self._services[servicename]
def get_services(self):
return self._services.values()
def get_service(self, servicename):
service = None
if servicename in self._services:
service = self._services[servicename]
return service
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)
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)
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]
def delete_sub_service(self, sub_servicename):
if sub_servicename in self._sub_services:
del self._sub_services[sub_servicename]
def get_sub_services(self):
return self._sub_services.values()
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
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
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)
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
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
def remove_json_gen_file(self, path):
remove_temp_inventory(path)
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)
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)
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)