123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- '''
- A Python library for working with Salt's REST API
- (Specifically the rest_cherrypy netapi module.)
- '''
- import json
- import logging
- import re
- import ssl
- from pepper.exceptions import PepperException
- try:
- ssl._create_default_https_context = ssl._create_stdlib_context
- except Exception:
- pass
- try:
- from urllib.request import HTTPHandler, HTTPSHandler, Request, urlopen, \
- install_opener, build_opener
- from urllib.error import HTTPError, URLError
- import urllib.parse as urlparse
- except ImportError:
- from urllib2 import HTTPHandler, HTTPSHandler, Request, urlopen, install_opener, build_opener, \
- HTTPError, URLError
- import urlparse
- logger = logging.getLogger(__name__)
- class Pepper(object):
- '''
- A thin wrapper for making HTTP calls to the salt-api rest_cherrpy REST
- interface
- >>> api = Pepper('https://localhost:8000')
- >>> api.login('saltdev', 'saltdev', 'pam')
- {"return": [
- {
- "eauth": "pam",
- "expire": 1370434219.714091,
- "perms": [
- "test.*"
- ],
- "start": 1370391019.71409,
- "token": "c02a6f4397b5496ba06b70ae5fd1f2ab75de9237",
- "user": "saltdev"
- }
- ]
- }
- >>> api.low([{'client': 'local', 'tgt': '*', 'fun': 'test.ping'}])
- {u'return': [{u'ms-0': True,
- u'ms-1': True,
- u'ms-2': True,
- u'ms-3': True,
- u'ms-4': True}]}
- '''
- def __init__(self, api_url='https://localhost:8000', debug_http=False, ignore_ssl_errors=False):
- '''
- Initialize the class with the URL of the API
- :param api_url: Host or IP address of the salt-api URL;
- include the port number
- :param debug_http: Add a flag to urllib2 to output the HTTP exchange
- :param ignore_ssl_errors: Add a flag to urllib2 to ignore invalid SSL certificates
- :raises PepperException: if the api_url is misformed
- '''
- split = urlparse.urlsplit(api_url)
- if split.scheme not in ['http', 'https']:
- raise PepperException("salt-api URL missing HTTP(s) protocol: {0}"
- .format(api_url))
- self.api_url = api_url
- self.debug_http = int(debug_http)
- self._ssl_verify = not ignore_ssl_errors
- self.auth = {}
- self.salt_version = None
- def req_stream(self, path):
- '''
- A thin wrapper to get a response from saltstack api.
- The body of the response will not be downloaded immediately.
- Make sure to close the connection after use.
- api = Pepper('http://ipaddress/api/')
- print(api.login('salt','salt','pam'))
- response = api.req_stream('/events')
- :param path: The path to the salt api resource
- :return: :class:`Response <Response>` object
- :rtype: requests.Response
- '''
- import requests
- headers = {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- }
- if self.auth and 'token' in self.auth and self.auth['token']:
- headers.setdefault('X-Auth-Token', self.auth['token'])
- else:
- raise PepperException('Authentication required')
- return
- params = {'url': self._construct_url(path),
- 'headers': headers,
- 'verify': self._ssl_verify is True,
- 'stream': True
- }
- try:
- resp = requests.get(**params)
- if resp.status_code == 401:
- raise PepperException(str(resp.status_code) + ':Authentication denied')
- return
- if resp.status_code == 500:
- raise PepperException(str(resp.status_code) + ':Server error.')
- return
- if resp.status_code == 404:
- raise PepperException(str(resp.status_code) + ' :This request returns nothing.')
- return
- except PepperException as e:
- print(e)
- return
- return resp
- def req_get(self, path):
- '''
- A thin wrapper from get http method of saltstack api
- api = Pepper('http://ipaddress/api/')
- print(api.login('salt','salt','pam'))
- print(api.req_get('/keys'))
- '''
- import requests
- headers = {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- }
- if self.auth and 'token' in self.auth and self.auth['token']:
- headers.setdefault('X-Auth-Token', self.auth['token'])
- else:
- raise PepperException('Authentication required')
- return
- params = {'url': self._construct_url(path),
- 'headers': headers,
- 'verify': self._ssl_verify is True,
- }
- try:
- resp = requests.get(**params)
- if resp.status_code == 401:
- raise PepperException(str(resp.status_code) + ':Authentication denied')
- return
- if resp.status_code == 500:
- raise PepperException(str(resp.status_code) + ':Server error.')
- return
- if resp.status_code == 404:
- raise PepperException(str(resp.status_code) + ' :This request returns nothing.')
- return
- except PepperException as e:
- print(e)
- return
- return resp.json()
- def req(self, path, data=None):
- '''
- A thin wrapper around urllib2 to send requests and return the response
- If the current instance contains an authentication token it will be
- attached to the request as a custom header.
- :rtype: dictionary
- '''
- if ((hasattr(data, 'get') and data.get('eauth') == 'kerberos')
- or self.auth.get('eauth') == 'kerberos'):
- return self.req_requests(path, data)
- headers = {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- }
- opener = build_opener()
- for handler in opener.handlers:
- if isinstance(handler, HTTPHandler):
- handler.set_http_debuglevel(self.debug_http)
- if isinstance(handler, HTTPSHandler):
- handler.set_http_debuglevel(self.debug_http)
- install_opener(opener)
- # Build POST data
- if data is not None:
- postdata = json.dumps(data).encode()
- clen = len(postdata)
- else:
- postdata = None
- # Create request object
- url = self._construct_url(path)
- req = Request(url, postdata, headers)
- # Add POST data to request
- if data is not None:
- req.add_header('Content-Length', clen)
- # Add auth header to request
- if path != '/run' and self.auth and 'token' in self.auth and self.auth['token']:
- req.add_header('X-Auth-Token', self.auth['token'])
- # Send request
- try:
- if not (self._ssl_verify):
- con = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
- f = urlopen(req, context=con)
- else:
- f = urlopen(req)
- content = f.read().decode('utf-8')
- if (self.debug_http):
- logger.debug('Response: %s', content)
- ret = json.loads(content)
- if not self.salt_version and 'x-salt-version' in f.headers:
- self._parse_salt_version(f.headers['x-salt-version'])
- except (HTTPError, URLError) as exc:
- logger.debug('Error with request', exc_info=True)
- status = getattr(exc, 'code', None)
- if status == 401:
- raise PepperException('Authentication denied')
- if status == 500:
- raise PepperException('Server error.')
- logger.error('Error with request: {0}'.format(exc))
- raise
- except AttributeError:
- logger.debug('Error converting response from JSON', exc_info=True)
- raise PepperException('Unable to parse the server response.')
- return ret
- def req_requests(self, path, data=None):
- '''
- A thin wrapper around request and request_kerberos to send
- requests and return the response
- If the current instance contains an authentication token it will be
- attached to the request as a custom header.
- :rtype: dictionary
- '''
- import requests
- from requests_gssapi import HTTPSPNEGOAuth, OPTIONAL
- auth = HTTPSPNEGOAuth(mutual_authentication=OPTIONAL)
- headers = {
- 'Accept': 'application/json',
- 'Content-Type': 'application/json',
- 'X-Requested-With': 'XMLHttpRequest',
- }
- if self.auth and 'token' in self.auth and self.auth['token']:
- headers.setdefault('X-Auth-Token', self.auth['token'])
- # Optionally toggle SSL verification
- params = {'url': self._construct_url(path),
- 'headers': headers,
- 'verify': self._ssl_verify is True,
- 'auth': auth,
- 'data': json.dumps(data),
- }
- logger.debug('postdata {0}'.format(params))
- resp = requests.post(**params)
- if resp.status_code == 401:
- # TODO should be resp.raise_from_status
- raise PepperException('Authentication denied')
- if resp.status_code == 500:
- # TODO should be resp.raise_from_status
- raise PepperException('Server error.')
- if not self.salt_version and 'x-salt-version' in resp.headers:
- self._parse_salt_version(resp.headers['x-salt-version'])
- return resp.json()
- def low(self, lowstate, path='/'):
- '''
- Execute a command through salt-api and return the response
- :param string path: URL path to be joined with the API hostname
- :param list lowstate: a list of lowstate dictionaries
- '''
- return self.req(path, lowstate)
- def local(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
- timeout=None, ret=None):
- '''
- Run a single command using the ``local`` client
- Wraps :meth:`low`.
- '''
- low = {
- 'client': 'local',
- 'tgt': tgt,
- 'fun': fun,
- }
- if arg:
- low['arg'] = arg
- if kwarg:
- low['kwarg'] = kwarg
- if expr_form:
- low['expr_form'] = expr_form
- if timeout:
- low['timeout'] = timeout
- if ret:
- low['ret'] = ret
- return self.low([low])
- def local_async(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
- timeout=None, ret=None):
- '''
- Run a single command using the ``local_async`` client
- Wraps :meth:`low`.
- '''
- low = {
- 'client': 'local_async',
- 'tgt': tgt,
- 'fun': fun,
- }
- if arg:
- low['arg'] = arg
- if kwarg:
- low['kwarg'] = kwarg
- if expr_form:
- low['expr_form'] = expr_form
- if timeout:
- low['timeout'] = timeout
- if ret:
- low['ret'] = ret
- return self.low([low])
- def local_batch(self, tgt, fun, arg=None, kwarg=None, expr_form='glob',
- batch='50%', ret=None):
- '''
- Run a single command using the ``local_batch`` client
- Wraps :meth:`low`.
- '''
- low = {
- 'client': 'local_batch',
- 'tgt': tgt,
- 'fun': fun,
- }
- if arg:
- low['arg'] = arg
- if kwarg:
- low['kwarg'] = kwarg
- if expr_form:
- low['expr_form'] = expr_form
- if batch:
- low['batch'] = batch
- if ret:
- low['ret'] = ret
- return self.low([low])
- def lookup_jid(self, jid):
- '''
- Get job results
- Wraps :meth:`runner`.
- '''
- return self.runner('jobs.lookup_jid', jid='{0}'.format(jid))
- def runner(self, fun, arg=None, **kwargs):
- '''
- Run a single command using the ``runner`` client
- Usage::
- runner('jobs.lookup_jid', jid=12345)
- '''
- low = {
- 'client': 'runner',
- 'fun': fun,
- }
- if arg:
- low['arg'] = arg
- low.update(kwargs)
- return self.low([low])
- def wheel(self, fun, arg=None, kwarg=None, **kwargs):
- '''
- Run a single command using the ``wheel`` client
- Usage::
- wheel('key.accept', match='myminion')
- '''
- low = {
- 'client': 'wheel',
- 'fun': fun,
- }
- if arg:
- low['arg'] = arg
- if kwarg:
- low['kwarg'] = kwarg
- low.update(kwargs)
- return self.low([low])
- def _send_auth(self, path, **kwargs):
- return self.req(path, kwargs)
- def login(self, username=None, password=None, eauth=None, **kwargs):
- '''
- Authenticate with salt-api and return the user permissions and
- authentication token or an empty dict
- '''
- local = locals()
- kwargs.update(
- dict(
- (key, local[key]) for key in (
- 'username',
- 'password',
- 'eauth'
- ) if local.get(key, None) is not None
- )
- )
- self.auth = self._send_auth('/login', **kwargs).get('return', [{}])[0]
- return self.auth
- def token(self, **kwargs):
- '''
- Get an eauth token from Salt for use with the /run URL
- '''
- self.auth = self._send_auth('/token', **kwargs)[0]
- return self.auth
- def _construct_url(self, path):
- '''
- Construct the url to salt-api for the given path
- Args:
- path: the path to the salt-api resource
- >>> api = Pepper('https://localhost:8000/salt-api/')
- >>> api._construct_url('/login')
- 'https://localhost:8000/salt-api/login'
- '''
- relative_path = path.lstrip('/')
- return urlparse.urljoin(self.api_url, relative_path)
- def _parse_salt_version(self, version):
- # borrow from salt.version
- git_describe_regex = re.compile(
- r'(?:[^\d]+)?(?P<major>[\d]{1,4})'
- r'\.(?P<minor>[\d]{1,2})'
- r'(?:\.(?P<bugfix>[\d]{0,2}))?'
- r'(?:\.(?P<mbugfix>[\d]{0,2}))?'
- r'(?:(?P<pre_type>rc|a|b|alpha|beta|nb)(?P<pre_num>[\d]{1}))?'
- r'(?:(?:.*)-(?P<noc>(?:[\d]+|n/a))-(?P<sha>[a-z0-9]{8}))?'
- )
- match = git_describe_regex.match(version)
- if match:
- self.salt_version = match.groups()
|