mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Merge remote-tracking branch 'upstream/2015.5' into merge-forward-2015.8
Conflicts: salt/fileclient.py salt/modules/glance.py
This commit is contained in:
commit
785f1575e8
8 changed files with 248 additions and 83 deletions
|
@ -60,6 +60,7 @@ def beacon(config):
|
|||
except OSError:
|
||||
# Ensure a valid mount point
|
||||
log.error('{0} is not a valid mount point, skipping.'.format(mount))
|
||||
continue
|
||||
|
||||
current_usage = _current_usage.percent
|
||||
monitor_usage = diskusage[mount]
|
||||
|
|
|
@ -6,30 +6,27 @@ Module for handling openstack glance calls.
|
|||
:configuration: This module is not usable until the following are specified
|
||||
either in a pillar or in the minion's config file::
|
||||
|
||||
keystone.user: admin
|
||||
keystone.password: verybadpass
|
||||
keystone.tenant: admin
|
||||
keystone.tenant_id: f80919baedab48ec8931f200c65a50df
|
||||
keystone.insecure: False #(optional)
|
||||
keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
|
||||
glance.user: admin
|
||||
glance.password: verybadpass
|
||||
glance.tenant: admin
|
||||
glance.insecure: False #(optional)
|
||||
glance.auth_url: 'http://127.0.0.1:5000/v2.0/'
|
||||
|
||||
If configuration for multiple openstack accounts is required, they can be
|
||||
set up as different configuration profiles:
|
||||
For example::
|
||||
|
||||
openstack1:
|
||||
keystone.user: admin
|
||||
keystone.password: verybadpass
|
||||
keystone.tenant: admin
|
||||
keystone.tenant_id: f80919baedab48ec8931f200c65a50df
|
||||
keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
|
||||
glance.user: admin
|
||||
glance.password: verybadpass
|
||||
glance.tenant: admin
|
||||
glance.auth_url: 'http://127.0.0.1:5000/v2.0/'
|
||||
|
||||
openstack2:
|
||||
keystone.user: admin
|
||||
keystone.password: verybadpass
|
||||
keystone.tenant: admin
|
||||
keystone.tenant_id: f80919baedab48ec8931f200c65a50df
|
||||
keystone.auth_url: 'http://127.0.0.2:5000/v2.0/'
|
||||
glance.user: admin
|
||||
glance.password: verybadpass
|
||||
glance.tenant: admin
|
||||
glance.auth_url: 'http://127.0.0.2:5000/v2.0/'
|
||||
|
||||
With this configuration in place, any of the keystone functions can make use
|
||||
of a configuration profile by declaring it explicitly.
|
||||
|
@ -42,13 +39,30 @@ Module for handling openstack glance calls.
|
|||
from __future__ import absolute_import
|
||||
|
||||
# Import third party libs
|
||||
import salt.ext.six as six
|
||||
#import salt.ext.six as six
|
||||
from salt.exceptions import (
|
||||
#CommandExecutionError,
|
||||
SaltInvocationError
|
||||
)
|
||||
# pylint: disable=import-error
|
||||
HAS_GLANCE = False
|
||||
try:
|
||||
from glanceclient import client
|
||||
import glanceclient.v1.images
|
||||
from glanceclient import exc
|
||||
HAS_GLANCE = True
|
||||
import logging
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
log = logging.getLogger(__name__)
|
||||
import pprint
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Workaround, as the Glance API v2 requires you to
|
||||
# already have a keystone token
|
||||
HAS_KEYSTONE = False
|
||||
try:
|
||||
from keystoneclient.v2_0 import client as kstone
|
||||
HAS_KEYSTONE = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
@ -65,21 +79,86 @@ def __virtual__():
|
|||
__opts__ = {}
|
||||
|
||||
|
||||
def _auth(profile=None):
|
||||
def _auth(profile=None, api_version=2, **connection_args):
|
||||
'''
|
||||
Set up keystone credentials
|
||||
Set up glance credentials, returns
|
||||
`glanceclient.client.Client`. Optional parameter
|
||||
"api_version" defaults to 2.
|
||||
|
||||
Only intended to be used within glance-enabled modules
|
||||
'''
|
||||
kstone = __salt__['keystone.auth'](profile)
|
||||
token = kstone.auth_token
|
||||
endpoint = kstone.service_catalog.url_for(
|
||||
service_type='image',
|
||||
endpoint_type='publicURL',
|
||||
)
|
||||
|
||||
return client.Client('1', endpoint, token=token)
|
||||
if profile:
|
||||
prefix = profile + ":glance."
|
||||
else:
|
||||
prefix = "glance."
|
||||
|
||||
# look in connection_args first, then default to config file
|
||||
def get(key, default=None):
|
||||
'''
|
||||
TODO: Add docstring.
|
||||
'''
|
||||
return connection_args.get('connection_' + key,
|
||||
__salt__['config.get'](prefix + key, default))
|
||||
|
||||
user = get('user', 'admin')
|
||||
password = get('password', 'ADMIN')
|
||||
tenant = get('tenant', 'admin')
|
||||
tenant_id = get('tenant_id')
|
||||
auth_url = get('auth_url', 'http://127.0.0.1:35357/v2.0/')
|
||||
insecure = get('insecure', False)
|
||||
token = get('token')
|
||||
region = get('region')
|
||||
endpoint = get('endpoint', 'http://127.0.0.1:9292/')
|
||||
|
||||
if token:
|
||||
kwargs = {'token': token,
|
||||
'username': user,
|
||||
'endpoint_url': endpoint,
|
||||
'auth_url': auth_url,
|
||||
'region_name': region,
|
||||
'tenant_name': tenant}
|
||||
else:
|
||||
kwargs = {'username': user,
|
||||
'password': password,
|
||||
'tenant_id': tenant_id,
|
||||
'auth_url': auth_url,
|
||||
'region_name': region,
|
||||
'tenant_name': tenant}
|
||||
# 'insecure' keyword not supported by all v2.0 keystone clients
|
||||
# this ensures it's only passed in when defined
|
||||
if insecure:
|
||||
kwargs['insecure'] = True
|
||||
|
||||
if token:
|
||||
log.debug('Calling glanceclient.client.Client(' +
|
||||
'{0}, {1}, **{2})'.format(api_version, endpoint, kwargs))
|
||||
try:
|
||||
return client.Client(api_version, endpoint, **kwargs)
|
||||
except exc.HTTPUnauthorized:
|
||||
kwargs.pop('token')
|
||||
kwargs['password'] = password
|
||||
log.warn('Supplied token is invalid, trying to ' +
|
||||
'get a new one using username and password.')
|
||||
|
||||
if HAS_KEYSTONE:
|
||||
# TODO: redact kwargs['password']
|
||||
log.debug('Calling keystoneclient.v2_0.client.Client(' +
|
||||
'{0}, **{1})'.format(endpoint, kwargs))
|
||||
keystone = kstone.Client(**kwargs)
|
||||
log.debug(help(keystone.get_token))
|
||||
kwargs['token'] = keystone.get_token(keystone.session)
|
||||
kwargs.pop('password')
|
||||
log.debug('Calling glanceclient.client.Client(' +
|
||||
'{0}, {1}, **{2})'.format(api_version, endpoint, kwargs))
|
||||
return client.Client(api_version, endpoint, **kwargs)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Can't retrieve a auth_token without keystone")
|
||||
|
||||
|
||||
def image_create(profile=None, **kwargs):
|
||||
def image_create(name, location, profile=None, visibility='public',
|
||||
container_format='bare', disk_format='raw'):
|
||||
'''
|
||||
Create an image (glance image-create)
|
||||
|
||||
|
@ -87,20 +166,35 @@ def image_create(profile=None, **kwargs):
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' glance.image_create name=f16-jeos is_public=true \\
|
||||
salt '*' glance.image_create name=f16-jeos visibility=public \\
|
||||
disk_format=qcow2 container_format=ovf \\
|
||||
copy_from=http://berrange.fedorapeople.org/images/2012-02-29/f16-x86_64-openstack-sda.qcow2
|
||||
copy_from=http://berrange.fedorapeople.org/\
|
||||
images/2012-02-29/f16-x86_64-openstack-sda.qcow2
|
||||
|
||||
For all possible values, run ``glance help image-create`` on the minion.
|
||||
'''
|
||||
nt_ks = _auth(profile)
|
||||
fields = dict(
|
||||
[(k, v) for (k, v) in six.iteritems(kwargs) if k in glanceclient.v1.images.CREATE_PARAMS]
|
||||
)
|
||||
|
||||
image = nt_ks.images.create(**fields)
|
||||
newimage = image_list(str(image.id), profile)
|
||||
return {newimage['name']: newimage}
|
||||
# valid options for "visibility":
|
||||
v_list = ['public', 'private']
|
||||
# valid options for "container_format":
|
||||
cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf']
|
||||
# valid options for "disk_format":
|
||||
df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
|
||||
'raw', 'qcow2', 'vdi', 'iso']
|
||||
if visibility not in v_list:
|
||||
raise SaltInvocationError('"visibility" needs to be one ' +
|
||||
'of the following: {0}'.format(', '.join(v_list)))
|
||||
if container_format not in cf_list:
|
||||
raise SaltInvocationError('"container_format" needs to be ' +
|
||||
'one of the following: {0}'.format(', '.join(cf_list)))
|
||||
if disk_format not in df_list:
|
||||
raise SaltInvocationError('"disk_format" needs to be one ' +
|
||||
'of the following: {0}'.format(', '.join(df_list)))
|
||||
# Icehouse's glanceclient doesn't have add_location() and
|
||||
# glanceclient.v2 doesn't implement Client.images.create()
|
||||
# in a usable fashion. Thus we have to use v1 for now.
|
||||
g_client = _auth(profile, api_version=1)
|
||||
image = g_client.images.create(name=name, copy_from=location)
|
||||
return image_show(image.id)
|
||||
|
||||
|
||||
def image_delete(id=None, name=None, profile=None): # pylint: disable=C0103
|
||||
|
@ -115,15 +209,15 @@ def image_delete(id=None, name=None, profile=None): # pylint: disable=C0103
|
|||
salt '*' glance.image_delete id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df
|
||||
salt '*' glance.image_delete name=f16-jeos
|
||||
'''
|
||||
nt_ks = _auth(profile)
|
||||
g_client = _auth(profile)
|
||||
if name:
|
||||
for image in nt_ks.images.list():
|
||||
for image in g_client.images.list():
|
||||
if image.name == name:
|
||||
id = image.id # pylint: disable=C0103
|
||||
continue
|
||||
if not id:
|
||||
return {'Error': 'Unable to resolve image id'}
|
||||
nt_ks.images.delete(id)
|
||||
g_client.images.delete(id)
|
||||
ret = 'Deleted image with ID {0}'.format(id)
|
||||
if name:
|
||||
ret += ' ({0})'.format(name)
|
||||
|
@ -138,35 +232,29 @@ def image_show(id=None, name=None, profile=None): # pylint: disable=C0103
|
|||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' glance.image_get
|
||||
salt '*' glance.image_show
|
||||
'''
|
||||
nt_ks = _auth(profile)
|
||||
g_client = _auth(profile)
|
||||
ret = {}
|
||||
if name:
|
||||
for image in nt_ks.images.list():
|
||||
for image in g_client.images.list():
|
||||
if image.name == name:
|
||||
id = image.id # pylint: disable=C0103
|
||||
continue
|
||||
if not id:
|
||||
return {'Error': 'Unable to resolve image id'}
|
||||
image = nt_ks.images.get(id)
|
||||
ret[image.name] = {
|
||||
'id': image.id,
|
||||
'name': image.name,
|
||||
'checksum': image.checksum,
|
||||
'container_format': image.container_format,
|
||||
'created_at': image.created_at,
|
||||
'deleted': image.deleted,
|
||||
'disk_format': image.disk_format,
|
||||
'is_public': image.is_public,
|
||||
'min_disk': image.min_disk,
|
||||
'min_ram': image.min_ram,
|
||||
'owner': image.owner,
|
||||
'protected': image.protected,
|
||||
'size': image.size,
|
||||
'status': image.status,
|
||||
'updated_at': image.updated_at,
|
||||
}
|
||||
image = g_client.images.get(id)
|
||||
pformat = pprint.PrettyPrinter(indent=4).pformat
|
||||
log.debug('Properties of image {0}:\n{1}'.format(
|
||||
image.name, pformat(image)))
|
||||
# TODO: Get rid of the wrapping dict, see #24568
|
||||
ret[image.name] = {}
|
||||
schema = image_schema(profile=profile)
|
||||
if len(schema.keys()) == 1:
|
||||
schema = schema['image']
|
||||
for key in schema.keys():
|
||||
if key in image:
|
||||
ret[image.name][key] = image[key]
|
||||
return ret
|
||||
|
||||
|
||||
|
@ -180,31 +268,59 @@ def image_list(id=None, profile=None): # pylint: disable=C0103
|
|||
|
||||
salt '*' glance.image_list
|
||||
'''
|
||||
nt_ks = _auth(profile)
|
||||
g_client = _auth(profile)
|
||||
ret = {}
|
||||
for image in nt_ks.images.list():
|
||||
# TODO: Get rid of the wrapping dict, see #24568
|
||||
for image in g_client.images.list():
|
||||
ret[image.name] = {
|
||||
'id': image.id,
|
||||
'name': image.name,
|
||||
'checksum': image.checksum,
|
||||
'container_format': image.container_format,
|
||||
'created_at': image.created_at,
|
||||
'deleted': image.deleted,
|
||||
'disk_format': image.disk_format,
|
||||
'is_public': image.is_public,
|
||||
'file': image.file,
|
||||
'min_disk': image.min_disk,
|
||||
'min_ram': image.min_ram,
|
||||
'owner': image.owner,
|
||||
'protected': image.protected,
|
||||
'size': image.size,
|
||||
'status': image.status,
|
||||
'tags': image.tags,
|
||||
'updated_at': image.updated_at,
|
||||
'visibility': image.visibility,
|
||||
}
|
||||
# Those cause AttributeErrors in Icehouse' glanceclient
|
||||
for attr in ['container_format', 'disk_format', 'size']:
|
||||
if attr in image:
|
||||
ret[image.name][attr] = image[attr]
|
||||
if id == image.id:
|
||||
return ret[image.name]
|
||||
return ret
|
||||
|
||||
|
||||
def image_schema(profile=None):
|
||||
'''
|
||||
Returns names and descriptions of the schema "image"'s
|
||||
properties for this profile's instance of glance
|
||||
'''
|
||||
return schema_get('image', profile)
|
||||
|
||||
|
||||
def schema_get(name, profile=None):
|
||||
'''
|
||||
Known valid names of schemas are:
|
||||
- image
|
||||
- images
|
||||
- member
|
||||
- members
|
||||
'''
|
||||
g_client = _auth(profile)
|
||||
pformat = pprint.PrettyPrinter(indent=4).pformat
|
||||
schema_props = {}
|
||||
for prop in g_client.schemas.get(name).properties:
|
||||
schema_props[prop.name] = prop.description
|
||||
log.debug('Properties of schema {0}:\n{1}'.format(
|
||||
name, pformat(schema_props)))
|
||||
return {name: schema_props}
|
||||
|
||||
|
||||
def _item_list(profile=None):
|
||||
'''
|
||||
Template for writing list functions
|
||||
|
@ -216,17 +332,18 @@ def _item_list(profile=None):
|
|||
|
||||
salt '*' glance.item_list
|
||||
'''
|
||||
nt_ks = _auth(profile)
|
||||
g_client = _auth(profile)
|
||||
ret = []
|
||||
for item in nt_ks.items.list():
|
||||
for item in g_client.items.list():
|
||||
ret.append(item.__dict__)
|
||||
#ret[item.name] = {
|
||||
# 'name': item.name,
|
||||
# }
|
||||
return ret
|
||||
|
||||
# The following is a list of functions that need to be incorporated in the
|
||||
# glance module. This list should be updated as functions are added.
|
||||
|
||||
#The following is a list of functions that need to be incorporated in the
|
||||
#glance module. This list should be updated as functions are added.
|
||||
|
||||
# image-download Download a specific image.
|
||||
# image-update Update a specific image.
|
||||
|
|
|
@ -121,6 +121,26 @@ def list_(show_all=False, where=None, return_yaml=True):
|
|||
return {'schedule': {}}
|
||||
|
||||
|
||||
def is_enabled(name):
|
||||
'''
|
||||
List a Job only if its enabled
|
||||
|
||||
.. versionadded:: 2015.5.3
|
||||
|
||||
CLI Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
salt '*' schedule.is_enabled name=job_name
|
||||
'''
|
||||
|
||||
current_schedule = __salt__['schedule.list'](show_all=False, return_yaml=False)
|
||||
if name in current_schedule:
|
||||
return current_schedule[name]
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def purge(**kwargs):
|
||||
'''
|
||||
Purge all the jobs currently scheduled on the minion
|
||||
|
|
|
@ -350,6 +350,8 @@ def available(name):
|
|||
salt '*' service.available sshd
|
||||
'''
|
||||
name = _canonical_template_unit_name(name)
|
||||
if name.endswith('.service'):
|
||||
name = name[:-8] # len('.service') is 8
|
||||
units = get_all()
|
||||
if name in units:
|
||||
return True
|
||||
|
|
|
@ -50,6 +50,12 @@ def start():
|
|||
if 'num_processes' not in mod_opts:
|
||||
mod_opts['num_processes'] = 1
|
||||
|
||||
if mod_opts['num_processes'] > 1 and mod_opts.get('debug', False) is True:
|
||||
raise Exception((
|
||||
'Tornado\'s debug implementation is not compatible with multiprocess. '
|
||||
'Either disable debug, or set num_processes to 1.'
|
||||
))
|
||||
|
||||
paths = [
|
||||
(r"/", saltnado.SaltAPIHandler),
|
||||
(r"/login", saltnado.SaltAuthHandler),
|
||||
|
@ -85,7 +91,6 @@ def start():
|
|||
application.opts = __opts__
|
||||
application.mod_opts = mod_opts
|
||||
application.auth = salt.auth.LoadAuth(__opts__)
|
||||
application.event_listener = saltnado.EventListener(mod_opts, __opts__)
|
||||
|
||||
# the kwargs for the HTTPServer
|
||||
kwargs = {}
|
||||
|
@ -111,7 +116,7 @@ def start():
|
|||
)
|
||||
http_server.start(mod_opts['num_processes'])
|
||||
except:
|
||||
print('Rest_tornado unable to bind to port {0}'.format(mod_opts['port']))
|
||||
logger.error('Rest_tornado unable to bind to port {0}'.format(mod_opts['port']), exc_info=True)
|
||||
raise SystemExit(1)
|
||||
|
||||
try:
|
||||
|
|
|
@ -258,7 +258,8 @@ class EventListener(object):
|
|||
'master',
|
||||
opts['sock_dir'],
|
||||
opts['transport'],
|
||||
opts=opts)
|
||||
opts=opts,
|
||||
)
|
||||
|
||||
self.event.subscribe() # start listening for events immediately
|
||||
|
||||
|
@ -271,8 +272,10 @@ class EventListener(object):
|
|||
# map of future -> timeout_callback
|
||||
self.timeout_map = {}
|
||||
|
||||
self.stream = zmqstream.ZMQStream(self.event.sub,
|
||||
io_loop=tornado.ioloop.IOLoop.current())
|
||||
self.stream = zmqstream.ZMQStream(
|
||||
self.event.sub,
|
||||
io_loop=tornado.ioloop.IOLoop.current(),
|
||||
)
|
||||
self.stream.on_recv(self._handle_event_socket_recv)
|
||||
|
||||
def clean_timeout_futures(self, request):
|
||||
|
@ -391,6 +394,17 @@ class BaseSaltAPIHandler(tornado.web.RequestHandler, SaltClientsMixIn): # pylin
|
|||
self.write("400 Invalid Client: Client not found in salt clients")
|
||||
self.finish()
|
||||
|
||||
def initialize(self):
|
||||
'''
|
||||
Initialize the handler before requests are called
|
||||
'''
|
||||
if not hasattr(self.application, 'event_listener'):
|
||||
logger.critical('init a listener')
|
||||
self.application.event_listener = EventListener(
|
||||
self.application.mod_opts,
|
||||
self.application.opts,
|
||||
)
|
||||
|
||||
@property
|
||||
def token(self):
|
||||
'''
|
||||
|
|
|
@ -464,6 +464,7 @@ def _interfaces_ip(out):
|
|||
based on the current set of cols
|
||||
'''
|
||||
brd = None
|
||||
scope = None
|
||||
if '/' in value: # we have a CIDR in this address
|
||||
ip, cidr = value.split('/') # pylint: disable=C0103
|
||||
else:
|
||||
|
@ -476,7 +477,8 @@ def _interfaces_ip(out):
|
|||
brd = cols[cols.index('brd') + 1]
|
||||
elif type_ == 'inet6':
|
||||
mask = cidr
|
||||
return (ip, mask, brd)
|
||||
scope = cols[cols.index('scope') + 1]
|
||||
return (ip, mask, brd, scope)
|
||||
|
||||
groups = re.compile('\r?\n\\d').split(out)
|
||||
for group in groups:
|
||||
|
@ -503,7 +505,7 @@ def _interfaces_ip(out):
|
|||
iflabel = cols[-1:][0]
|
||||
if type_ in ('inet', 'inet6'):
|
||||
if 'secondary' not in cols:
|
||||
ipaddr, netmask, broadcast = parse_network(value, cols)
|
||||
ipaddr, netmask, broadcast, scope = parse_network(value, cols)
|
||||
if type_ == 'inet':
|
||||
if 'inet' not in data:
|
||||
data['inet'] = list()
|
||||
|
@ -519,11 +521,12 @@ def _interfaces_ip(out):
|
|||
addr_obj = dict()
|
||||
addr_obj['address'] = ipaddr
|
||||
addr_obj['prefixlen'] = netmask
|
||||
addr_obj['scope'] = scope
|
||||
data['inet6'].append(addr_obj)
|
||||
else:
|
||||
if 'secondary' not in data:
|
||||
data['secondary'] = list()
|
||||
ip_, mask, brd = parse_network(value, cols)
|
||||
ip_, mask, brd, scp = parse_network(value, cols)
|
||||
data['secondary'].append({
|
||||
'type': type_,
|
||||
'address': ip_,
|
||||
|
@ -531,7 +534,7 @@ def _interfaces_ip(out):
|
|||
'broadcast': brd,
|
||||
'label': iflabel,
|
||||
})
|
||||
del ip_, mask, brd
|
||||
del ip_, mask, brd, scp
|
||||
elif type_.startswith('link'):
|
||||
data['hwaddr'] = value
|
||||
if iface:
|
||||
|
|
3
setup.py
3
setup.py
|
@ -411,6 +411,8 @@ class Build(build):
|
|||
|
||||
class Install(install):
|
||||
user_options = install.user_options + [
|
||||
('salt-transport=', None, 'The transport to prepare salt for. Choices are \'zeromq\' '
|
||||
'\'raet\' or \'both\'. Defaults to \'zeromq\'', 'zeromq'),
|
||||
('salt-root-dir=', None,
|
||||
'Salt\'s pre-configured root directory'),
|
||||
('salt-config-dir=', None,
|
||||
|
@ -453,6 +455,7 @@ class Install(install):
|
|||
self.salt_base_master_roots_dir = None
|
||||
self.salt_logs_dir = None
|
||||
self.salt_pidfile_dir = None
|
||||
self.salt_transport = None
|
||||
|
||||
def finalize_options(self):
|
||||
install.finalize_options(self)
|
||||
|
|
Loading…
Add table
Reference in a new issue