<commit_before>#!/usr/bin/env python
#
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2013 YAMAMOTO Takashi <yamamoto at valinux co jp>
#
# 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.

# a simple command line msgpack-rpc client
#
# a usage example:
#     % PYTHONPATH=. ./bin/rpc-cli \
#      --peers=echo-server=localhost:9999,hoge=localhost:9998
#     (Cmd) request echo-server echo ["hoge"]
#     RESULT hoge
#     (Cmd) request echo-server notify ["notify-method", ["param1","param2"]]
#     RESULT notify-method
#     (Cmd)
#     NOTIFICATION from echo-server ['notify-method', ['param1', 'param2']]
#     (Cmd)

from __future__ import print_function

import cmd
import signal
import socket
import sys
import termios

from ryu import cfg
from ryu.lib import rpc

CONF = cfg.CONF
CONF.register_cli_opts([
    cfg.ListOpt('peers', default=[],
                help='List of peers, separated by commas. '
                     '(e.g., "hoge=localhost:9998,fuga=localhost:9999")'),
    cfg.StrOpt('command', short='c', default=None,
               help='Command to be executed as single command. '
                    'The default is None and opens interactive console.'),
])

class Peer(object):
    def __init__(self, name, addr):
        self._name = name
        self._addr = addr
        self.client = None
        try:
            self.connect()
        except:
            pass

    def connect(self):
        self.client = None
        s = socket.create_connection(self._addr)
        self.client = rpc.Client(s, notification_callback=self.notification)

    def try_to_connect(self, verbose=False):
        if self.client:
            return
        try:
            self.connect()
            assert self.client
        except Exception as e:
            if verbose:
                print("connection failure %s" % e)
            raise EOFError

    def notification(self, n):
        print("NOTIFICATION from %s %s" % (self._name, n))

    def call(self, method, params):
        return self._do(lambda: self.client.call(method, params))

    def send_notification(self, method, params):
        self._do(lambda: self.client.send_notification(method, params))

    def _do(self, f):
        def g():
            try:
                return f()
            except EOFError:
                self.client = None
                raise

        self.try_to_connect(verbose=True)
        try:
            return g()
        except EOFError:
            print("disconnected.  trying to connect...")
            self.try_to_connect(verbose=True)
            print("connected.  retrying the request...")
            return g()

peers = {}

def add_peer(name, host, port):
    peers[name] = Peer(name, (host, port))

class Cmd(cmd.Cmd):
    def __init__(self, *args, **kwargs):
        self._in_onecmd = False
        self._notification_check_interval = 1  # worth to be configurable?
        self._saved_termios = None
        cmd.Cmd.__init__(self, *args, **kwargs)

    def _request(self, line, f):
        args = line.split(None, 2)
        try:
            peer = args[0]
            method = args[1]
            params = eval(args[2])
        except:
            print("argument error")
            return
        try:
            p = peers[peer]
        except KeyError:
            print("unknown peer %s" % peer)
            return
        try:
            f(p, method, params)
        except rpc.RPCError as e:
            print("RPC ERROR %s" % e)
        except EOFError:
            print("disconnected")

    def _complete_peer(self, text, line, _begidx, _endidx):
        if len((line + 'x').split()) >= 3:
            return []
        return [name for name in peers if name.startswith(text)]

    def do_request(self, line):
        """request <peer> <method> <params>
        send a msgpack-rpc request and print a response.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            result = p.call(method, params)
            print("RESULT %s" % result)

        self._request(line, f)

    def do_notify(self, line):
        """notify <peer> <method> <params>
        send a msgpack-rpc notification.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            p.send_notification(method, params)

        self._request(line, f)

    def complete_request(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def complete_notify(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def do_EOF(self, _line):
        sys.exit(0)

    def emptyline(self):
        self._peek_notification()

    def postcmd(self, _stop, _line):
        self._peek_notification()

    def _peek_notification(self):
        for k, p in peers.items():
            if p.client:
                try:
                    p.client.peek_notification()
                except EOFError:
                    p.client = None
                    print("disconnected %s" % k)

    @staticmethod
    def _save_termios():
        return termios.tcgetattr(sys.stdin.fileno())

    @staticmethod
    def _restore_termios(t):
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, t)

    def preloop(self):
        self._saved_termios = self._save_termios()
        signal.signal(signal.SIGALRM, self._timeout)
        signal.alarm(1)

    def onecmd(self, string):
        self._in_onecmd = True
        try:
            return cmd.Cmd.onecmd(self, string)
        finally:
            self._in_onecmd = False

    def _timeout(self, _sig, _frame):
        if not self._in_onecmd:
            # restore terminal settings. (cooked/raw, ...)
            # required for pypy at least.
            # this doesn't seem to be needed for cpython readline
            # module but i'm not sure if it's by spec or luck.
            o = self._save_termios()
            self._restore_termios(self._saved_termios)
            self._peek_notification()
            self._restore_termios(o)
        signal.alarm(self._notification_check_interval)

def main(args=None, prog=None):
    CONF(args=args, prog=prog, project='rpc-cli', version='rpc-cli')

    for p_str in CONF.peers:
        name, addr = p_str.split('=')
        host, port = addr.rsplit(':', 1)
        add_peer(name, host, port)

    if CONF.command:
        command = Cmd()
        command.onecmd(CONF.command)
        command.do_EOF()

    Cmd().cmdloop()

if __name__ == "__main__":
    main()
<commit_msg>rpc_cli: Close peer connections when exiting prompt<commit_after>#!/usr/bin/env python
#
# Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
# Copyright (C) 2013 YAMAMOTO Takashi <yamamoto at valinux co jp>
#
# 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.

# a simple command line msgpack-rpc client
#
# a usage example:
#     % PYTHONPATH=. ./bin/rpc-cli \
#      --peers=echo-server=localhost:9999,hoge=localhost:9998
#     (Cmd) request echo-server echo ["hoge"]
#     RESULT hoge
#     (Cmd) request echo-server notify ["notify-method", ["param1","param2"]]
#     RESULT notify-method
#     (Cmd)
#     NOTIFICATION from echo-server ['notify-method', ['param1', 'param2']]
#     (Cmd)

from __future__ import print_function

import cmd
import signal
import socket
import sys
import termios

from ryu import cfg
from ryu.lib import rpc

CONF = cfg.CONF
CONF.register_cli_opts([
    cfg.ListOpt('peers', default=[],
                help='List of peers, separated by commas. '
                     '(e.g., "hoge=localhost:9998,fuga=localhost:9999")'),
    cfg.StrOpt('command', short='c', default=None,
               help='Command to be executed as single command. '
                    'The default is None and opens interactive console.'),
])

class Peer(object):
    def __init__(self, name, addr):
        self._name = name
        self._addr = addr
        self.socket = None
        self.client = None
        try:
            self.connect()
        except ConnectionError as e:
            print('Exception when connecting to peer "%s": %s' % (name, e))
            raise e

    def connect(self):
        self.socket = socket.create_connection(self._addr)
        self.client = rpc.Client(self.socket,
                                 notification_callback=self.notification)

    def try_to_connect(self, verbose=False):
        if self.client:
            return
        try:
            self.connect()
            assert self.client
        except Exception as e:
            if verbose:
                print("connection failure %s" % e)
            raise EOFError

    def notification(self, n):
        print("NOTIFICATION from %s %s" % (self._name, n))

    def call(self, method, params):
        return self._do(lambda: self.client.call(method, params))

    def send_notification(self, method, params):
        self._do(lambda: self.client.send_notification(method, params))

    def _do(self, f):
        def g():
            try:
                return f()
            except EOFError:
                self.client = None
                raise

        self.try_to_connect(verbose=True)
        try:
            return g()
        except EOFError:
            print("disconnected.  trying to connect...")
            self.try_to_connect(verbose=True)
            print("connected.  retrying the request...")
            return g()

    def close(self):
        self.socket.close()

peers = {}

def add_peer(name, host, port):
    try:
        peer = Peer(name, (host, port))
    except ConnectionError:
        return

    peers[name] = peer

def close_peers():
    for peer in peers.values():
        peer.socket.close()

class Cmd(cmd.Cmd):
    def __init__(self, *args, **kwargs):
        self._in_onecmd = False
        self._notification_check_interval = 1  # worth to be configurable?
        self._saved_termios = None
        cmd.Cmd.__init__(self, *args, **kwargs)

    def _request(self, line, f):
        args = line.split(None, 2)
        try:
            peer = args[0]
            method = args[1]
            params = eval(args[2])
        except:
            print("argument error")
            return
        try:
            p = peers[peer]
        except KeyError:
            print("unknown peer %s" % peer)
            return
        try:
            f(p, method, params)
        except rpc.RPCError as e:
            print("RPC ERROR %s" % e)
        except EOFError:
            print("disconnected")

    def _complete_peer(self, text, line, _begidx, _endidx):
        if len((line + 'x').split()) >= 3:
            return []
        return [name for name in peers if name.startswith(text)]

    def do_request(self, line):
        """request <peer> <method> <params>
        send a msgpack-rpc request and print a response.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            result = p.call(method, params)
            print("RESULT %s" % result)

        self._request(line, f)

    def do_notify(self, line):
        """notify <peer> <method> <params>
        send a msgpack-rpc notification.
        <params> is a python code snippet, it should be eval'ed to a list.
        """

        def f(p, method, params):
            p.send_notification(method, params)

        self._request(line, f)

    def complete_request(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def complete_notify(self, text, line, begidx, endidx):
        return self._complete_peer(text, line, begidx, endidx)

    def do_EOF(self, _line=None):
        close_peers()
        sys.exit(0)

    def emptyline(self):
        self._peek_notification()

    def postcmd(self, _stop, _line):
        self._peek_notification()

    def _peek_notification(self):
        for k, p in peers.items():
            if p.client:
                try:
                    p.client.peek_notification()
                except EOFError:
                    p.client = None
                    print("disconnected %s" % k)

    @staticmethod
    def _save_termios():
        return termios.tcgetattr(sys.stdin.fileno())

    @staticmethod
    def _restore_termios(t):
        termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, t)

    def preloop(self):
        self._saved_termios = self._save_termios()
        signal.signal(signal.SIGALRM, self._timeout)
        signal.alarm(1)

    def postloop(self):
        close_peers()

    def onecmd(self, string):
        self._in_onecmd = True
        try:
            return cmd.Cmd.onecmd(self, string)
        finally:
            self._in_onecmd = False

    def _timeout(self, _sig, _frame):
        if not self._in_onecmd:
            # restore terminal settings. (cooked/raw, ...)
            # required for pypy at least.
            # this doesn't seem to be needed for cpython readline
            # module but i'm not sure if it's by spec or luck.
            o = self._save_termios()
            self._restore_termios(self._saved_termios)
            self._peek_notification()
            self._restore_termios(o)
        signal.alarm(self._notification_check_interval)

def main(args=None, prog=None):
    CONF(args=args, prog=prog, project='rpc-cli', version='rpc-cli')

    for p_str in CONF.peers:
        name, addr = p_str.split('=')
        host, port = addr.rsplit(':', 1)
        add_peer(name, host, port)

    if CONF.command:
        command = Cmd()
        command.onecmd(CONF.command)
        command.do_EOF()

    Cmd().cmdloop()

if __name__ == "__main__":
    main()
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
'''
A salt interface to psutil, a system and process library.
See http://code.google.com/p/psutil.

:depends:   - psutil Python module, version 0.3.0 or later
            - python-utmp package (optional)
'''

# Import python libs
import time
import datetime

# Import salt libs
import salt.utils
from salt.exceptions import SaltInvocationError

# Import third party libs
try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

def __virtual__():
    if not HAS_PSUTIL:
        return False

    # Functions and attributes used in this execution module seem to have been
    # added as of psutil 0.3.0, from an inspection of the source code. Only
    # make this module available if the version of psutil is >= 0.3.0. Note
    # that this may need to be tweaked if we find post-0.3.0 versions which
    # also have problems running the functions in this execution module, but
    # most distributions have already moved to later versions (for example,
    # as of Dec. 2013 EPEL is on 0.6.1, Debian 7 is on 0.5.1, etc.).
    if psutil.version_info >= (0, 3, 0):
        return True
    return False

def top(num_processes=5, interval=3):
    '''
    Return a list of top CPU consuming processes during the interval.
    num_processes = return the top N CPU consuming processes
    interval = the number of seconds to sample CPU usage over

    CLI Examples:

    .. code-block:: bash

        salt '*' ps.top

        salt '*' ps.top 5 10
    '''
    result = []
    start_usage = {}
    for pid in psutil.get_pid_list():
        try:
            process = psutil.Process(pid)
            user, system = process.get_cpu_times()
        except psutil.NoSuchProcess:
            continue
        start_usage[process] = user + system
    time.sleep(interval)
    usage = set()
    for process, start in start_usage.items():
        try:
            user, system = process.get_cpu_times()
        except psutil.NoSuchProcess:
            continue
        now = user + system
        diff = now - start
        usage.add((diff, process))

    for idx, (diff, process) in enumerate(reversed(sorted(usage))):
        if num_processes and idx >= num_processes:
            break
        if len(process.cmdline) == 0:
            cmdline = [process.name]
        else:
            cmdline = process.cmdline
        info = {'cmd': cmdline,
                'user': process.username,
                'status': process.status,
                'pid': process.pid,
                'create_time': process.create_time,
                'cpu': {},
                'mem': {},
                }
        for key, value in process.get_cpu_times()._asdict().items():
            info['cpu'][key] = value
        for key, value in process.get_memory_info()._asdict().items():
            info['mem'][key] = value
        result.append(info)

    return result

def get_pid_list():
    '''
    Return a list of process ids (PIDs) for all running processes.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.get_pid_list
    '''
    return psutil.get_pid_list()

def kill_pid(pid, signal=15):
    '''
    Kill a process by PID.

    .. code-block:: bash

        salt 'minion' ps.kill_pid pid [signal=signal_number]

    pid
        PID of process to kill.

    signal
        Signal to send to the process. See manpage entry for kill
        for possible values. Default: 15 (SIGTERM).

    **Example:**

    Send SIGKILL to process with PID 2000:

    .. code-block:: bash

        salt 'minion' ps.kill_pid 2000 signal=9
    '''
    try:
        psutil.Process(pid).send_signal(signal)
        return True
    except psutil.NoSuchProcess:
        return False

def pkill(pattern, user=None, signal=15, full=False):
    '''
    Kill processes matching a pattern.

    .. code-block:: bash

        salt '*' ps.pkill pattern [user=username] [signal=signal_number] \\
                [full=(true|false)]

    pattern
        Pattern to search for in the process list.

    user
        Limit matches to the given username. Default: All users.

    signal
        Signal to send to the process(es). See manpage entry for kill
        for possible values. Default: 15 (SIGTERM).

    full
        A boolean value indicating whether only the name of the command or
        the full command line should be matched against the pattern.

    **Examples:**

    Send SIGHUP to all httpd processes on all 'www' minions:

    .. code-block:: bash

        salt 'www.*' ps.pkill httpd signal=1

    Send SIGKILL to all bash processes owned by user 'tom':

    .. code-block:: bash

        salt '*' ps.pkill bash signal=9 user=tom
    '''

    killed = []
    for proc in psutil.process_iter():
        name_match = pattern in ' '.join(proc.cmdline) if full \
            else pattern in proc.name
        user_match = True if user is None else user == proc.username
        if name_match and user_match:
            try:
                proc.send_signal(signal)
                killed.append(proc.pid)
            except psutil.NoSuchProcess:
                pass
    if not killed:
        return None
    else:
        return {'killed': killed}

def pgrep(pattern, user=None, full=False):
    '''
    Return the pids for processes matching a pattern.

    If full is true, the full command line is searched for a match,
    otherwise only the name of the command is searched.

    .. code-block:: bash

        salt '*' ps.pgrep pattern [user=username] [full=(true|false)]

    pattern
        Pattern to search for in the process list.

    user
        Limit matches to the given username. Default: All users.

    full
        A boolean value indicating whether only the name of the command or
        the full command line should be matched against the pattern.

    **Examples:**

    Find all httpd processes on all 'www' minions:

    .. code-block:: bash

        salt 'www.*' ps.pgrep httpd

    Find all bash processes owned by user 'tom':

    .. code-block:: bash

        salt '*' ps.pgrep bash user=tom
    '''

    procs = []
    for proc in psutil.process_iter():
        name_match = pattern in ' '.join(proc.cmdline) if full \
            else pattern in proc.name
        user_match = True if user is None else user == proc.username
        if name_match and user_match:
            procs.append(proc.pid)
    return procs or None

def cpu_percent(interval=0.1, per_cpu=False):
    '''
    Return the percent of time the CPU is busy.

    interval
        the number of seconds to sample CPU usage over
    per_cpu
        if True return an array of CPU percent busy for each CPU, otherwise
        aggregate all percents into one number

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cpu_percent
    '''
    if per_cpu:
        result = list(psutil.cpu_percent(interval, True))
    else:
        result = psutil.cpu_percent(interval)
    return result

def cpu_times(per_cpu=False):
    '''
    Return the percent of time the CPU spends in each state,
    e.g. user, system, idle, nice, iowait, irq, softirq.

    per_cpu
        if True return an array of percents for each CPU, otherwise aggregate
        all percents into one number

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cpu_times
    '''
    if per_cpu:
        result = [dict(times._asdict()) for times in psutil.cpu_times(True)]
    else:
        result = dict(psutil.cpu_times(per_cpu)._asdict())
    return result

def virtual_memory():
    '''
    .. versionadded:: Helium

    Return a dict that describes statistics about system memory usage.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.virtual_memory
    '''
    return dict(psutil.virtual_memory()._asdict())

def swap_memory():
    '''
    .. versionadded:: Helium

    Return a dict that describes swap memory statistics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.swap_memory
    '''
    return dict(psutil.swap_memory()._asdict())

def physical_memory_usage():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return a dict that describes free and available physical memory.

    CLI Examples:

    .. code-block:: bash

        salt '*' ps.physical_memory_usage
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.physical_memory_usage\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return dict(psutil.phymem_usage()._asdict())

def virtual_memory_usage():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return a dict that describes free and available memory, both physical
    and virtual.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.virtual_memory_usage
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.virtual_memory_usage\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return dict(psutil.virtmem_usage()._asdict())

def cached_physical_memory():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return the amount cached memory.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cached_physical_memory
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.cached_physical_memory\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return psutil.cached_phymem()

def physical_memory_buffers():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return the amount of physical memory buffers.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.physical_memory_buffers
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.physical_memory_buffers\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return psutil.phymem_buffers()

def disk_partitions(all=False):
    '''
    Return a list of disk partitions and their device, mount point, and
    filesystem type.

    all
        if set to False, only return local, physical partitions (hard disk,
        USB, CD/DVD partitions).  If True, return all filesystems.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_partitions
    '''
    result = [dict(partition._asdict()) for partition in
              psutil.disk_partitions(all)]
    return result

def disk_usage(path):
    '''
    Given a path, return a dict listing the total available space as well as
    the free space, and used space.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_usage /home
    '''
    return dict(psutil.disk_usage(path)._asdict())

def disk_partition_usage(all=False):
    '''
    Return a list of disk partitions plus the mount point, filesystem and usage
    statistics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_partition_usage
    '''
    result = disk_partitions(all)
    for partition in result:
        partition.update(disk_usage(partition['mountpoint']))
    return result

def total_physical_memory():
    '''
    Return the total number of bytes of physical memory.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.total_physical_memory
    '''
    try:
        return psutil.virtual_memory().total
    except AttributeError:
        # TOTAL_PHYMEM is deprecated but with older psutil versions this is
        # needed as a fallback.
        return psutil.TOTAL_PHYMEM

def num_cpus():
    '''
    Return the number of CPUs.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.num_cpus
    '''
    try:
        return psutil.cpu_count()
    except AttributeError:
        # NUM_CPUS is deprecated but with older psutil versions this is needed
        # as a fallback.
        return psutil.NUM_CPUS

def boot_time(time_format=None):
    '''
    Return the boot time in number of seconds since the epoch began.

    CLI Example:

    time_format
        Optionally specify a `strftime`_ format string. Use
        ``time_format='%c'`` to get a nicely-formatted locale specific date and
        time (i.e. ``Fri May  2 19:08:32 2014``).

        .. _strftime: https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior

        .. versionadded:: 2014.1.4

    .. code-block:: bash

        salt '*' ps.boot_time
    '''
    try:
        boot_time = int(psutil.boot_time())
    except AttributeError:
        # get_boot_time() has been removed in newer psutil versions, and has
        # been replaced by boot_time() which provides the same information.
        boot_time = int(psutil.get_boot_time())
    if time_format:
        # Load epoch timestamp as a datetime.datetime object
        boot_time = datetime.datetime.fromtimestamp(boot_time)
        try:
            return boot_time.strftime(time_format)
        except TypeError as exc:
            raise SaltInvocationError('Invalid format string: {0}'.format(exc))
    return boot_time

def network_io_counters(interface=None):
    '''
    Return network I/O statisitics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.network_io_counters

        salt '*' ps.network_io_counters interface=eth0
    '''
    if not interface:
        return dict(psutil.network_io_counters()._asdict())
    else:
        stats = psutil.network_io_counters(pernic=True)
        if interface in stats:
            return dict(stats[interface]._asdict())
        else:
            return False

def disk_io_counters(device=None):
    '''
    Return disk I/O statisitics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_io_counters

        salt '*' ps.disk_io_counters device=sda1
    '''
    if not device:
        return dict(psutil.disk_io_counters()._asdict())
    else:
        stats = psutil.disk_io_counters(perdisk=True)
        if device in stats:
            return dict(stats[device]._asdict())
        else:
            return False

def get_users():
    '''
    Return logged-in users.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.get_users
    '''
    try:
        recs = psutil.get_users()
        return [dict(x._asdict()) for x in recs]
    except AttributeError:
        # get_users is only present in psutil > v0.5.0
        # try utmp
        try:
            import utmp
            result = []
            while True:
                rec = utmp.utmpaccess.getutent()
                if rec is None:
                    return result
                elif rec[0] == 7:
                    started = rec[8]
                    if isinstance(started, tuple):
                        started = started[0]
                    result.append({'name': rec[4], 'terminal': rec[2],
                                   'started': started, 'host': rec[5]})
        except ImportError:
            return False
# This is a possible last ditch method
#        result = []
#        w = __salt__['cmd.run'](
#            'who', env='{"LC_ALL": "en_US.UTF-8"}').splitlines()
#        for u in w:
#            u = u.split()
#            started = __salt__['cmd.run'](
#                'date --d "{0} {1}" +%s'.format(u[2], u[3])).strip()
#            rec = {'name': u[0], 'terminal': u[1],
#                   'started': started, 'host': None}
#            if len(u) > 4:
#                rec['host'] = u[4][1:-1]
#            result.append(rec)
#        return result
<commit_msg>Don't shadow boot_time function<commit_after># -*- coding: utf-8 -*-
'''
A salt interface to psutil, a system and process library.
See http://code.google.com/p/psutil.

:depends:   - psutil Python module, version 0.3.0 or later
            - python-utmp package (optional)
'''

# Import python libs
import time
import datetime

# Import salt libs
import salt.utils
from salt.exceptions import SaltInvocationError

# Import third party libs
try:
    import psutil
    HAS_PSUTIL = True
except ImportError:
    HAS_PSUTIL = False

def __virtual__():
    if not HAS_PSUTIL:
        return False

    # Functions and attributes used in this execution module seem to have been
    # added as of psutil 0.3.0, from an inspection of the source code. Only
    # make this module available if the version of psutil is >= 0.3.0. Note
    # that this may need to be tweaked if we find post-0.3.0 versions which
    # also have problems running the functions in this execution module, but
    # most distributions have already moved to later versions (for example,
    # as of Dec. 2013 EPEL is on 0.6.1, Debian 7 is on 0.5.1, etc.).
    if psutil.version_info >= (0, 3, 0):
        return True
    return False

def top(num_processes=5, interval=3):
    '''
    Return a list of top CPU consuming processes during the interval.
    num_processes = return the top N CPU consuming processes
    interval = the number of seconds to sample CPU usage over

    CLI Examples:

    .. code-block:: bash

        salt '*' ps.top

        salt '*' ps.top 5 10
    '''
    result = []
    start_usage = {}
    for pid in psutil.get_pid_list():
        try:
            process = psutil.Process(pid)
            user, system = process.get_cpu_times()
        except psutil.NoSuchProcess:
            continue
        start_usage[process] = user + system
    time.sleep(interval)
    usage = set()
    for process, start in start_usage.items():
        try:
            user, system = process.get_cpu_times()
        except psutil.NoSuchProcess:
            continue
        now = user + system
        diff = now - start
        usage.add((diff, process))

    for idx, (diff, process) in enumerate(reversed(sorted(usage))):
        if num_processes and idx >= num_processes:
            break
        if len(process.cmdline) == 0:
            cmdline = [process.name]
        else:
            cmdline = process.cmdline
        info = {'cmd': cmdline,
                'user': process.username,
                'status': process.status,
                'pid': process.pid,
                'create_time': process.create_time,
                'cpu': {},
                'mem': {},
                }
        for key, value in process.get_cpu_times()._asdict().items():
            info['cpu'][key] = value
        for key, value in process.get_memory_info()._asdict().items():
            info['mem'][key] = value
        result.append(info)

    return result

def get_pid_list():
    '''
    Return a list of process ids (PIDs) for all running processes.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.get_pid_list
    '''
    return psutil.get_pid_list()

def kill_pid(pid, signal=15):
    '''
    Kill a process by PID.

    .. code-block:: bash

        salt 'minion' ps.kill_pid pid [signal=signal_number]

    pid
        PID of process to kill.

    signal
        Signal to send to the process. See manpage entry for kill
        for possible values. Default: 15 (SIGTERM).

    **Example:**

    Send SIGKILL to process with PID 2000:

    .. code-block:: bash

        salt 'minion' ps.kill_pid 2000 signal=9
    '''
    try:
        psutil.Process(pid).send_signal(signal)
        return True
    except psutil.NoSuchProcess:
        return False

def pkill(pattern, user=None, signal=15, full=False):
    '''
    Kill processes matching a pattern.

    .. code-block:: bash

        salt '*' ps.pkill pattern [user=username] [signal=signal_number] \\
                [full=(true|false)]

    pattern
        Pattern to search for in the process list.

    user
        Limit matches to the given username. Default: All users.

    signal
        Signal to send to the process(es). See manpage entry for kill
        for possible values. Default: 15 (SIGTERM).

    full
        A boolean value indicating whether only the name of the command or
        the full command line should be matched against the pattern.

    **Examples:**

    Send SIGHUP to all httpd processes on all 'www' minions:

    .. code-block:: bash

        salt 'www.*' ps.pkill httpd signal=1

    Send SIGKILL to all bash processes owned by user 'tom':

    .. code-block:: bash

        salt '*' ps.pkill bash signal=9 user=tom
    '''

    killed = []
    for proc in psutil.process_iter():
        name_match = pattern in ' '.join(proc.cmdline) if full \
            else pattern in proc.name
        user_match = True if user is None else user == proc.username
        if name_match and user_match:
            try:
                proc.send_signal(signal)
                killed.append(proc.pid)
            except psutil.NoSuchProcess:
                pass
    if not killed:
        return None
    else:
        return {'killed': killed}

def pgrep(pattern, user=None, full=False):
    '''
    Return the pids for processes matching a pattern.

    If full is true, the full command line is searched for a match,
    otherwise only the name of the command is searched.

    .. code-block:: bash

        salt '*' ps.pgrep pattern [user=username] [full=(true|false)]

    pattern
        Pattern to search for in the process list.

    user
        Limit matches to the given username. Default: All users.

    full
        A boolean value indicating whether only the name of the command or
        the full command line should be matched against the pattern.

    **Examples:**

    Find all httpd processes on all 'www' minions:

    .. code-block:: bash

        salt 'www.*' ps.pgrep httpd

    Find all bash processes owned by user 'tom':

    .. code-block:: bash

        salt '*' ps.pgrep bash user=tom
    '''

    procs = []
    for proc in psutil.process_iter():
        name_match = pattern in ' '.join(proc.cmdline) if full \
            else pattern in proc.name
        user_match = True if user is None else user == proc.username
        if name_match and user_match:
            procs.append(proc.pid)
    return procs or None

def cpu_percent(interval=0.1, per_cpu=False):
    '''
    Return the percent of time the CPU is busy.

    interval
        the number of seconds to sample CPU usage over
    per_cpu
        if True return an array of CPU percent busy for each CPU, otherwise
        aggregate all percents into one number

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cpu_percent
    '''
    if per_cpu:
        result = list(psutil.cpu_percent(interval, True))
    else:
        result = psutil.cpu_percent(interval)
    return result

def cpu_times(per_cpu=False):
    '''
    Return the percent of time the CPU spends in each state,
    e.g. user, system, idle, nice, iowait, irq, softirq.

    per_cpu
        if True return an array of percents for each CPU, otherwise aggregate
        all percents into one number

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cpu_times
    '''
    if per_cpu:
        result = [dict(times._asdict()) for times in psutil.cpu_times(True)]
    else:
        result = dict(psutil.cpu_times(per_cpu)._asdict())
    return result

def virtual_memory():
    '''
    .. versionadded:: Helium

    Return a dict that describes statistics about system memory usage.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.virtual_memory
    '''
    return dict(psutil.virtual_memory()._asdict())

def swap_memory():
    '''
    .. versionadded:: Helium

    Return a dict that describes swap memory statistics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.swap_memory
    '''
    return dict(psutil.swap_memory()._asdict())

def physical_memory_usage():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return a dict that describes free and available physical memory.

    CLI Examples:

    .. code-block:: bash

        salt '*' ps.physical_memory_usage
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.physical_memory_usage\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return dict(psutil.phymem_usage()._asdict())

def virtual_memory_usage():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return a dict that describes free and available memory, both physical
    and virtual.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.virtual_memory_usage
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.virtual_memory_usage\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return dict(psutil.virtmem_usage()._asdict())

def cached_physical_memory():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return the amount cached memory.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.cached_physical_memory
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.cached_physical_memory\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return psutil.cached_phymem()

def physical_memory_buffers():
    '''
    .. deprecated:: Helium
        Use :mod:`ps.virtual_memory <salt.modules.ps.virtual_memory>` instead.

    Return the amount of physical memory buffers.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.physical_memory_buffers
    '''
    salt.utils.warn_until(
        'Helium',
        '\'ps.physical_memory_buffers\' is deprecated.  Please use'
        '\'ps.virtual_memory\' instead.  This functionality will'
        'be removed in Salt {version}.'
    )
    return psutil.phymem_buffers()

def disk_partitions(all=False):
    '''
    Return a list of disk partitions and their device, mount point, and
    filesystem type.

    all
        if set to False, only return local, physical partitions (hard disk,
        USB, CD/DVD partitions).  If True, return all filesystems.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_partitions
    '''
    result = [dict(partition._asdict()) for partition in
              psutil.disk_partitions(all)]
    return result

def disk_usage(path):
    '''
    Given a path, return a dict listing the total available space as well as
    the free space, and used space.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_usage /home
    '''
    return dict(psutil.disk_usage(path)._asdict())

def disk_partition_usage(all=False):
    '''
    Return a list of disk partitions plus the mount point, filesystem and usage
    statistics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_partition_usage
    '''
    result = disk_partitions(all)
    for partition in result:
        partition.update(disk_usage(partition['mountpoint']))
    return result

def total_physical_memory():
    '''
    Return the total number of bytes of physical memory.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.total_physical_memory
    '''
    try:
        return psutil.virtual_memory().total
    except AttributeError:
        # TOTAL_PHYMEM is deprecated but with older psutil versions this is
        # needed as a fallback.
        return psutil.TOTAL_PHYMEM

def num_cpus():
    '''
    Return the number of CPUs.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.num_cpus
    '''
    try:
        return psutil.cpu_count()
    except AttributeError:
        # NUM_CPUS is deprecated but with older psutil versions this is needed
        # as a fallback.
        return psutil.NUM_CPUS

def boot_time(time_format=None):
    '''
    Return the boot time in number of seconds since the epoch began.

    CLI Example:

    time_format
        Optionally specify a `strftime`_ format string. Use
        ``time_format='%c'`` to get a nicely-formatted locale specific date and
        time (i.e. ``Fri May  2 19:08:32 2014``).

        .. _strftime: https://docs.python.org/2/library/datetime.html#strftime-strptime-behavior

        .. versionadded:: 2014.1.4

    .. code-block:: bash

        salt '*' ps.boot_time
    '''
    try:
        b_time = int(psutil.boot_time())
    except AttributeError:
        # get_boot_time() has been removed in newer psutil versions, and has
        # been replaced by boot_time() which provides the same information.
        b_time = int(psutil.get_boot_time())
    if time_format:
        # Load epoch timestamp as a datetime.datetime object
        b_time = datetime.datetime.fromtimestamp(b_time)
        try:
            return b_time.strftime(time_format)
        except TypeError as exc:
            raise SaltInvocationError('Invalid format string: {0}'.format(exc))
    return b_time

def network_io_counters(interface=None):
    '''
    Return network I/O statisitics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.network_io_counters

        salt '*' ps.network_io_counters interface=eth0
    '''
    if not interface:
        return dict(psutil.network_io_counters()._asdict())
    else:
        stats = psutil.network_io_counters(pernic=True)
        if interface in stats:
            return dict(stats[interface]._asdict())
        else:
            return False

def disk_io_counters(device=None):
    '''
    Return disk I/O statisitics.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.disk_io_counters

        salt '*' ps.disk_io_counters device=sda1
    '''
    if not device:
        return dict(psutil.disk_io_counters()._asdict())
    else:
        stats = psutil.disk_io_counters(perdisk=True)
        if device in stats:
            return dict(stats[device]._asdict())
        else:
            return False

def get_users():
    '''
    Return logged-in users.

    CLI Example:

    .. code-block:: bash

        salt '*' ps.get_users
    '''
    try:
        recs = psutil.get_users()
        return [dict(x._asdict()) for x in recs]
    except AttributeError:
        # get_users is only present in psutil > v0.5.0
        # try utmp
        try:
            import utmp
            result = []
            while True:
                rec = utmp.utmpaccess.getutent()
                if rec is None:
                    return result
                elif rec[0] == 7:
                    started = rec[8]
                    if isinstance(started, tuple):
                        started = started[0]
                    result.append({'name': rec[4], 'terminal': rec[2],
                                   'started': started, 'host': rec[5]})
        except ImportError:
            return False
# This is a possible last ditch method
#        result = []
#        w = __salt__['cmd.run'](
#            'who', env='{"LC_ALL": "en_US.UTF-8"}').splitlines()
#        for u in w:
#            u = u.split()
#            started = __salt__['cmd.run'](
#                'date --d "{0} {1}" +%s'.format(u[2], u[3])).strip()
#            rec = {'name': u[0], 'terminal': u[1],
#                   'started': started, 'host': None}
#            if len(u) > 4:
#                rec['host'] = u[4][1:-1]
#            result.append(rec)
#        return result
<|endoftext|>


<commit_before>'''
Interaction with Git repositories.
==================================

NOTE: This modules is under heavy development and the API is subject to change.
It may be replaced with a generic VCS module if this proves viable.

Important, before using git over ssh, make sure your remote host fingerprint
exists in "~/.ssh/known_hosts" file.

.. code-block:: yaml

    https://github.com/saltstack/salt.git:
      git.latest:
        - rev: develop
        - target: /tmp/salt
'''
import logging
import os
import shutil

log = logging.getLogger(__name__)

def __virtual__():
    '''
    Only load if git is available
    '''
    return 'git' if __salt__['cmd.has_exec']('git') else False

def latest(name,
           rev=None,
           target=None,
           runas=None,
           force=None,
           submodules=False,
        ):
    '''
    Make sure the repository is cloned to the given directory and is up to date

    name
        Address of the remote repository as passed to "git clone"
    rev
        The remote branch, tag, or revision ID to checkout after
        clone / before update
    target
        Name of the target directory where repository is about to be cloned
    runas
        Name of the user performing repository management operations
    force
        Force git to clone into pre-existing directories (deletes contents)
    submodules
        Update submodules on clone or branch change (Default: False)
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
    if not target:
        return _fail(ret, '"target" option is required')

    if os.path.isdir(target) and os.path.isdir('{0}/.git'.format(target)):
        # git pull is probably required
        log.debug(
                'target {0} is found, "git pull" is probably required'.format(
                    target)
                )
        try:
            current_rev = __salt__['git.revision'](target, user=runas)

            if __opts__['test']:
                return _neutral_test(
                        ret,
                        ('Repository {0} update is probably required (current '
                        'revision is {1})').format(target, current_rev))

            pull_out = __salt__['git.pull'](target, user=runas)

            if rev:
                __salt__['git.checkout'](target, rev, user=runas)

            if submodules:
                __salt__['git.submodule'](target, user=runas, opts='--recursive')

            new_rev = __salt__['git.revision'](cwd=target, user=runas)

        except Exception as exc:
            return _fail(
                    ret,
                    str(exc))

        if current_rev != new_rev:
            log.info('Repository {0} updated: {1} => {2}'.format(target,
                                                                 current_rev,
                                                                 new_rev))
            ret['comment'] = 'Repository {0} updated'.format(target)
            ret['changes']['revision'] = '{0} => {1}'.format(
                    current_rev, new_rev)
    else:
        if os.path.isdir(target):
            # git clone is required, target exists but force is turned on
            if force:
                log.debug(
                    'target {0} found, but not a git repository. Since force option'
                    ' is in use, deleting.'.format(target))
                shutil.rmtree(target)
            # git clone is required, but target exists and is non-empty
            elif os.listdir(target):
                return _fail(ret, 'Directory exists, is non-empty, and force '
                    'option not in use')

        # git clone is required
        log.debug(
                'target {0} is not found, "git clone" is required'.format(
                    target))

        if __opts__['test']:
            return _neutral_test(
                    ret,
                    'Repository {0} is about to be cloned to {1}'.format(
                        name, target))
        try:
            # make the clone
            result = __salt__['git.clone'](target, name, user=runas)

            if rev:
                __salt__['git.checkout'](target, rev, user=runas)

            if submodules:
                __salt__['git.submodule'](target, user=runas, opts='--recursive')

            new_rev = __salt__['git.revision'](cwd=target, user=runas)

        except Exception, exception:
            return _fail(
                    ret,
                    str(exception))

        message = 'Repository {0} cloned to {1}'.format(name, target)
        log.info(message)
        ret['comment'] = message

        ret['changes']['new'] = name
        ret['changes']['revision'] = new_rev
    return ret

def present(name, bare=True, runas=None, force=False):
    '''
    Make sure the repository is present in the given directory

    name
        Name of the directory where the repository is about to be created
    bare
        Create a bare repository (Default: True)
    runas
        Name of the user performing repository management operations
    force
        Force create a new repository into an pre-existing non-git directory (deletes contents)
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}

    # If the named directory is a git repo return True
    if os.path.isdir(name):
        if bare and os.path.isfile('{0}/HEAD'.format(name)):
            return ret
        elif not bare and os.path.isdir('{0}/.git'.format(name)):
            return ret
        # Directory exists and is not a git repo, if force is set destroy the
        # directory and recreate, otherwise throw an error
        elif not force and os.listdir(name):
            return _fail(ret,
                         'Directory which does not contain a git repo '
                         'is already present at {0}. To delete this '
                         'directory and create a fresh git repo set '
                         'force: True'.format(name))

    # Run test is set
    if __opts__['test']:
        ret['changes']['new repository'] = name
        return _neutral_test(ret, 'New git repo set for creation at {0}'.format(name))

    if force and os.path.isdir(name):
        shutil.rmtree(name)

    opts = '--bare' if bare else ''
    __salt__['git.init'](cwd=name, user=runas, opts=opts)

    message = 'Initialized repository {0}'.format(name)
    log.info(message)
    ret['changes']['new repository'] = name
    ret['comment'] = message

    return ret

def _fail(ret, comment):
    ret['result'] = False
    ret['comment'] = comment
    return ret

def _neutral_test(ret, comment):
    ret['result'] = None
    ret['comment'] = comment
    return ret
<commit_msg>Found another in the git state<commit_after>'''
Interaction with Git repositories.
==================================

NOTE: This modules is under heavy development and the API is subject to change.
It may be replaced with a generic VCS module if this proves viable.

Important, before using git over ssh, make sure your remote host fingerprint
exists in "~/.ssh/known_hosts" file.

.. code-block:: yaml

    https://github.com/saltstack/salt.git:
      git.latest:
        - rev: develop
        - target: /tmp/salt
'''
import logging
import os
import shutil

log = logging.getLogger(__name__)

def __virtual__():
    '''
    Only load if git is available
    '''
    return 'git' if __salt__['cmd.has_exec']('git') else False

def latest(name,
           rev=None,
           target=None,
           runas=None,
           force=None,
           submodules=False,
        ):
    '''
    Make sure the repository is cloned to the given directory and is up to date

    name
        Address of the remote repository as passed to "git clone"
    rev
        The remote branch, tag, or revision ID to checkout after
        clone / before update
    target
        Name of the target directory where repository is about to be cloned
    runas
        Name of the user performing repository management operations
    force
        Force git to clone into pre-existing directories (deletes contents)
    submodules
        Update submodules on clone or branch change (Default: False)
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}
    if not target:
        return _fail(ret, '"target" option is required')

    if os.path.isdir(target) and os.path.isdir('{0}/.git'.format(target)):
        # git pull is probably required
        log.debug(
                'target {0} is found, "git pull" is probably required'.format(
                    target)
                )
        try:
            current_rev = __salt__['git.revision'](target, user=runas)

            if __opts__['test']:
                return _neutral_test(
                        ret,
                        ('Repository {0} update is probably required (current '
                        'revision is {1})').format(target, current_rev))

            pull_out = __salt__['git.pull'](target, user=runas)

            if rev:
                __salt__['git.checkout'](target, rev, user=runas)

            if submodules:
                __salt__['git.submodule'](target, user=runas, opts='--recursive')

            new_rev = __salt__['git.revision'](cwd=target, user=runas)

        except Exception as exc:
            return _fail(
                    ret,
                    str(exc))

        if current_rev != new_rev:
            log.info('Repository {0} updated: {1} => {2}'.format(target,
                                                                 current_rev,
                                                                 new_rev))
            ret['comment'] = 'Repository {0} updated'.format(target)
            ret['changes']['revision'] = '{0} => {1}'.format(
                    current_rev, new_rev)
    else:
        if os.path.isdir(target):
            # git clone is required, target exists but force is turned on
            if force:
                log.debug(
                    'target {0} found, but not a git repository. Since force option'
                    ' is in use, deleting.'.format(target))
                shutil.rmtree(target)
            # git clone is required, but target exists and is non-empty
            elif os.listdir(target):
                return _fail(ret, 'Directory exists, is non-empty, and force '
                    'option not in use')

        # git clone is required
        log.debug(
                'target {0} is not found, "git clone" is required'.format(
                    target))

        if __opts__['test']:
            return _neutral_test(
                    ret,
                    'Repository {0} is about to be cloned to {1}'.format(
                        name, target))
        try:
            # make the clone
            result = __salt__['git.clone'](target, name, user=runas)

            if rev:
                __salt__['git.checkout'](target, rev, user=runas)

            if submodules:
                __salt__['git.submodule'](target, user=runas, opts='--recursive')

            new_rev = __salt__['git.revision'](cwd=target, user=runas)

        except Exception as exc:
            return _fail(
                    ret,
                    str(exc))

        message = 'Repository {0} cloned to {1}'.format(name, target)
        log.info(message)
        ret['comment'] = message

        ret['changes']['new'] = name
        ret['changes']['revision'] = new_rev
    return ret

def present(name, bare=True, runas=None, force=False):
    '''
    Make sure the repository is present in the given directory

    name
        Name of the directory where the repository is about to be created
    bare
        Create a bare repository (Default: True)
    runas
        Name of the user performing repository management operations
    force
        Force create a new repository into an pre-existing non-git directory (deletes contents)
    '''
    ret = {'name': name, 'result': True, 'comment': '', 'changes': {}}

    # If the named directory is a git repo return True
    if os.path.isdir(name):
        if bare and os.path.isfile('{0}/HEAD'.format(name)):
            return ret
        elif not bare and os.path.isdir('{0}/.git'.format(name)):
            return ret
        # Directory exists and is not a git repo, if force is set destroy the
        # directory and recreate, otherwise throw an error
        elif not force and os.listdir(name):
            return _fail(ret,
                         'Directory which does not contain a git repo '
                         'is already present at {0}. To delete this '
                         'directory and create a fresh git repo set '
                         'force: True'.format(name))

    # Run test is set
    if __opts__['test']:
        ret['changes']['new repository'] = name
        return _neutral_test(ret, 'New git repo set for creation at {0}'.format(name))

    if force and os.path.isdir(name):
        shutil.rmtree(name)

    opts = '--bare' if bare else ''
    __salt__['git.init'](cwd=name, user=runas, opts=opts)

    message = 'Initialized repository {0}'.format(name)
    log.info(message)
    ret['changes']['new repository'] = name
    ret['comment'] = message

    return ret

def _fail(ret, comment):
    ret['result'] = False
    ret['comment'] = comment
    return ret

def _neutral_test(ret, comment):
    ret['result'] = None
    ret['comment'] = comment
    return ret
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
'''
Installation of NPM Packages
============================

These states manage the installed packages for node.js using the Node Package
Manager (npm). Note that npm must be installed for these states to be
available, so npm states should include a requisite to a pkg.installed state
for the package which provides npm (simply ``npm`` in most cases). Example:

.. code-block:: yaml

    npm:
      pkg.installed

    yaml:
      npm.installed:
        - require:
          - pkg: npm
'''
from __future__ import absolute_import

# Import salt libs
from salt.exceptions import CommandExecutionError, CommandNotFoundError

def __virtual__():
    '''
    Only load if the npm module is available in __salt__
    '''
    return 'npm' if 'npm.list' in __salt__ else False, '\'npm\' binary not found on system'

def installed(name,
              pkgs=None,
              dir=None,
              user=None,
              force_reinstall=False,
              registry=None,
              env=None):
    '''
    Verify that the given package is installed and is at the correct version
    (if specified).

    .. code-block:: yaml

        coffee-script:
          npm.installed:
            - user: someuser

        coffee-script@1.0.1:
          npm.installed: []

    name
        The package to install

        .. versionchanged:: 2014.7.2
            This parameter is no longer lowercased by salt so that
            case-sensitive NPM package names will work.

    pkgs
        A list of packages to install with a single npm invocation; specifying
        this argument will ignore the ``name`` argument

        .. versionadded:: 2014.7.0

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0

    registry
        The NPM registry from which to install the package

        .. versionadded:: 2014.7.0

    env
        A list of environment variables to be set prior to execution. The
        format is the same as the :py:func:`cmd.run <salt.states.cmd.run>`.
        state function.

        .. versionadded:: 2014.7.0

    force_reinstall
        Install the package even if it is already installed
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if pkgs is not None:
        pkg_list = pkgs
    else:
        pkg_list = [name]

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir, runas=user, env=env)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error looking up {0!r}: {1}'.format(name, err)
        return ret
    else:
        installed_pkgs = dict((p, info)
                for p, info in installed_pkgs.items())

    pkgs_satisfied = []
    pkgs_to_install = []

    def _pkg_is_installed(pkg, installed_pkgs):
        '''
        Helper function to determine if a package is installed

        This performs more complex comparison than just checking
        keys, such as examining source repos to see if the package
        was installed by a different name from the same repo

        :pkg str: The package to compare
        :installed_pkgs: A dictionary produced by npm list --json
        '''
        if (pkg_name in installed_pkgs and
            'version' in installed_pkgs[pkg_name]):
            return True
        # Check to see if we are trying to install from a URI
        elif '://' in pkg_name:  # TODO Better way?
            for pkg_details in installed_pkgs.values():
                pkg_from = pkg_details.get('from', '').split('://')[1]
                if pkg_name.split('://')[1] == pkg_from:
                    return True
        return False

    for pkg in pkg_list:
        pkg_name, _, pkg_ver = pkg.partition('@')
        pkg_name = pkg_name.strip()

        if force_reinstall is True:
            pkgs_to_install.append(pkg)
            continue
        if not _pkg_is_installed(pkg, installed_pkgs):
            pkgs_to_install.append(pkg)
            continue

        installed_name_ver = '{0}@{1}'.format(pkg_name,
                installed_pkgs[pkg_name]['version'])

        # If given an explicit version check the installed version matches.
        if pkg_ver:
            if installed_pkgs[pkg_name].get('version') != pkg_ver:
                pkgs_to_install.append(pkg)
            else:
                pkgs_satisfied.append(installed_name_ver)

            continue
        else:
            pkgs_satisfied.append(installed_name_ver)
            continue

    if __opts__['test']:
        ret['result'] = None

        comment_msg = []
        if pkgs_to_install:
            comment_msg.append('NPM package(s) {0!r} are set to be installed'
                .format(', '.join(pkgs_to_install)))

            ret['changes'] = {'old': [], 'new': pkgs_to_install}

        if pkgs_satisfied:
            comment_msg.append('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
            ret['result'] = True

        ret['comment'] = '. '.join(comment_msg)
        return ret

    if not pkgs_to_install:
        ret['result'] = True
        ret['comment'] = ('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
        return ret

    try:
        cmd_args = {
            'dir': dir,
            'runas': user,
            'registry': registry,
            'env': env,
        }

        if pkgs is not None:
            cmd_args['pkgs'] = pkgs
        else:
            cmd_args['pkg'] = pkg_name

        call = __salt__['npm.install'](**cmd_args)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error installing {0!r}: {1}'.format(
                ', '.join(pkg_list), err)
        return ret

    if call and (isinstance(call, list) or isinstance(call, dict)):
        ret['result'] = True
        ret['changes'] = {'old': [], 'new': pkgs_to_install}
        ret['comment'] = 'Package(s) {0!r} successfully installed'.format(
                ', '.join(pkgs_to_install))
    else:
        ret['result'] = False
        ret['comment'] = 'Could not install package(s) {0!r}'.format(
                ', '.join(pkg_list))

    return ret

def removed(name,
            dir=None,
            user=None):
    '''
    Verify that the given package is not installed.

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir)
    except (CommandExecutionError, CommandNotFoundError) as err:
        ret['result'] = False
        ret['comment'] = 'Error uninstalling {0!r}: {1}'.format(name, err)
        return ret

    if name not in installed_pkgs:
        ret['result'] = True
        ret['comment'] = 'Package {0!r} is not installed'.format(name)
        return ret

    if __opts__['test']:
        ret['result'] = None
        ret['comment'] = 'Package {0!r} is set to be removed'.format(name)
        return ret

    if __salt__['npm.uninstall'](pkg=name, dir=dir, runas=user):
        ret['result'] = True
        ret['changes'][name] = 'Removed'
        ret['comment'] = 'Package {0!r} was successfully removed'.format(name)
    else:
        ret['result'] = False
        ret['comment'] = 'Error removing package {0!r}'.format(name)

    return ret

def bootstrap(name,
              user=None):
    '''
    Bootstraps a node.js application.

    Will execute 'npm install --json' on the specified directory.

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        call = __salt__['npm.install'](dir=name, runas=user, pkg=None)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
        return ret

    if not call:
        ret['result'] = True
        ret['comment'] = 'Directory is already bootstrapped'
        return ret

    # npm.install will return a string if it can't parse a JSON result
    if isinstance(call, str):
        ret['result'] = False
        ret['comment'] = 'Could not bootstrap directory'
    else:
        ret['result'] = True
        ret['changes'] = {name: 'Bootstrapped'}
        ret['comment'] = 'Directory was successfully bootstrapped'

    return ret
<commit_msg>Install correct package version, if provided, for npm state.<commit_after># -*- coding: utf-8 -*-
'''
Installation of NPM Packages
============================

These states manage the installed packages for node.js using the Node Package
Manager (npm). Note that npm must be installed for these states to be
available, so npm states should include a requisite to a pkg.installed state
for the package which provides npm (simply ``npm`` in most cases). Example:

.. code-block:: yaml

    npm:
      pkg.installed

    yaml:
      npm.installed:
        - require:
          - pkg: npm
'''
from __future__ import absolute_import

# Import salt libs
from salt.exceptions import CommandExecutionError, CommandNotFoundError

def __virtual__():
    '''
    Only load if the npm module is available in __salt__
    '''
    return 'npm' if 'npm.list' in __salt__ else False, '\'npm\' binary not found on system'

def installed(name,
              pkgs=None,
              dir=None,
              user=None,
              force_reinstall=False,
              registry=None,
              env=None):
    '''
    Verify that the given package is installed and is at the correct version
    (if specified).

    .. code-block:: yaml

        coffee-script:
          npm.installed:
            - user: someuser

        coffee-script@1.0.1:
          npm.installed: []

    name
        The package to install

        .. versionchanged:: 2014.7.2
            This parameter is no longer lowercased by salt so that
            case-sensitive NPM package names will work.

    pkgs
        A list of packages to install with a single npm invocation; specifying
        this argument will ignore the ``name`` argument

        .. versionadded:: 2014.7.0

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0

    registry
        The NPM registry from which to install the package

        .. versionadded:: 2014.7.0

    env
        A list of environment variables to be set prior to execution. The
        format is the same as the :py:func:`cmd.run <salt.states.cmd.run>`.
        state function.

        .. versionadded:: 2014.7.0

    force_reinstall
        Install the package even if it is already installed
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if pkgs is not None:
        pkg_list = pkgs
    else:
        pkg_list = [name]

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir, runas=user, env=env)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error looking up {0!r}: {1}'.format(name, err)
        return ret
    else:
        installed_pkgs = dict((p, info)
                for p, info in installed_pkgs.items())

    pkgs_satisfied = []
    pkgs_to_install = []

    def _pkg_is_installed(pkg, installed_pkgs):
        '''
        Helper function to determine if a package is installed

        This performs more complex comparison than just checking
        keys, such as examining source repos to see if the package
        was installed by a different name from the same repo

        :pkg str: The package to compare
        :installed_pkgs: A dictionary produced by npm list --json
        '''
        if (pkg_name in installed_pkgs and
            'version' in installed_pkgs[pkg_name]):
            return True
        # Check to see if we are trying to install from a URI
        elif '://' in pkg_name:  # TODO Better way?
            for pkg_details in installed_pkgs.values():
                pkg_from = pkg_details.get('from', '').split('://')[1]
                if pkg_name.split('://')[1] == pkg_from:
                    return True
        return False

    for pkg in pkg_list:
        pkg_name, _, pkg_ver = pkg.partition('@')
        pkg_name = pkg_name.strip()

        if force_reinstall is True:
            pkgs_to_install.append(pkg)
            continue
        if not _pkg_is_installed(pkg, installed_pkgs):
            pkgs_to_install.append(pkg)
            continue

        installed_name_ver = '{0}@{1}'.format(pkg_name,
                installed_pkgs[pkg_name]['version'])

        # If given an explicit version check the installed version matches.
        if pkg_ver:
            if installed_pkgs[pkg_name].get('version') != pkg_ver:
                pkgs_to_install.append(pkg)
            else:
                pkgs_satisfied.append(installed_name_ver)

            continue
        else:
            pkgs_satisfied.append(installed_name_ver)
            continue

    if __opts__['test']:
        ret['result'] = None

        comment_msg = []
        if pkgs_to_install:
            comment_msg.append('NPM package(s) {0!r} are set to be installed'
                .format(', '.join(pkgs_to_install)))

            ret['changes'] = {'old': [], 'new': pkgs_to_install}

        if pkgs_satisfied:
            comment_msg.append('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
            ret['result'] = True

        ret['comment'] = '. '.join(comment_msg)
        return ret

    if not pkgs_to_install:
        ret['result'] = True
        ret['comment'] = ('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
        return ret

    try:
        cmd_args = {
            'dir': dir,
            'runas': user,
            'registry': registry,
            'env': env,
            'pkgs': pkg_list,
        }

        call = __salt__['npm.install'](**cmd_args)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error installing {0!r}: {1}'.format(
                ', '.join(pkg_list), err)
        return ret

    if call and (isinstance(call, list) or isinstance(call, dict)):
        ret['result'] = True
        ret['changes'] = {'old': [], 'new': pkgs_to_install}
        ret['comment'] = 'Package(s) {0!r} successfully installed'.format(
                ', '.join(pkgs_to_install))
    else:
        ret['result'] = False
        ret['comment'] = 'Could not install package(s) {0!r}'.format(
                ', '.join(pkg_list))

    return ret

def removed(name,
            dir=None,
            user=None):
    '''
    Verify that the given package is not installed.

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir)
    except (CommandExecutionError, CommandNotFoundError) as err:
        ret['result'] = False
        ret['comment'] = 'Error uninstalling {0!r}: {1}'.format(name, err)
        return ret

    if name not in installed_pkgs:
        ret['result'] = True
        ret['comment'] = 'Package {0!r} is not installed'.format(name)
        return ret

    if __opts__['test']:
        ret['result'] = None
        ret['comment'] = 'Package {0!r} is set to be removed'.format(name)
        return ret

    if __salt__['npm.uninstall'](pkg=name, dir=dir, runas=user):
        ret['result'] = True
        ret['changes'][name] = 'Removed'
        ret['comment'] = 'Package {0!r} was successfully removed'.format(name)
    else:
        ret['result'] = False
        ret['comment'] = 'Error removing package {0!r}'.format(name)

    return ret

def bootstrap(name,
              user=None):
    '''
    Bootstraps a node.js application.

    Will execute 'npm install --json' on the specified directory.

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        call = __salt__['npm.install'](dir=name, runas=user, pkg=None)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
        return ret

    if not call:
        ret['result'] = True
        ret['comment'] = 'Directory is already bootstrapped'
        return ret

    # npm.install will return a string if it can't parse a JSON result
    if isinstance(call, str):
        ret['result'] = False
        ret['comment'] = 'Could not bootstrap directory'
    else:
        ret['result'] = True
        ret['changes'] = {name: 'Bootstrapped'}
        ret['comment'] = 'Directory was successfully bootstrapped'

    return ret
<|endoftext|>


<commit_before># -*- coding: utf-8 -*-
'''
Installation of NPM Packages
============================

These states manage the installed packages for node.js using the Node Package
Manager (npm). Note that npm must be installed for these states to be
available, so npm states should include a requisite to a pkg.installed state
for the package which provides npm (simply ``npm`` in most cases). Example:

.. code-block:: yaml

    npm:
      pkg.installed

    yaml:
      npm.installed:
        - require:
          - pkg: npm
'''

# Import salt libs
from __future__ import absolute_import
from salt.exceptions import CommandExecutionError, CommandNotFoundError

# Import 3rd-party libs
import salt.ext.six as six

def __virtual__():
    '''
    Only load if the npm module is available in __salt__
    '''
    return 'npm' if 'npm.list' in __salt__ else False, '\'npm\' binary not found on system'

def installed(name,
              pkgs=None,
              dir=None,
              user=None,
              force_reinstall=False,
              registry=None,
              env=None):
    '''
    Verify that the given package is installed and is at the correct version
    (if specified).

    .. code-block:: yaml

        coffee-script:
          npm.installed:
            - user: someuser

        coffee-script@1.0.1:
          npm.installed: []

    name
        The package to install

        .. versionchanged:: 2014.7.2
            This parameter is no longer lowercased by salt so that
            case-sensitive NPM package names will work.

    pkgs
        A list of packages to install with a single npm invocation; specifying
        this argument will ignore the ``name`` argument

        .. versionadded:: 2014.7.0

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0

    registry
        The NPM registry from which to install the package

        .. versionadded:: 2014.7.0

    env
        A list of environment variables to be set prior to execution. The
        format is the same as the :py:func:`cmd.run <salt.states.cmd.run>`.
        state function.

        .. versionadded:: 2014.7.0

    force_reinstall
        Install the package even if it is already installed
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if pkgs is not None:
        pkg_list = pkgs
    else:
        pkg_list = [name]

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir, runas=user, env=env)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error looking up {0!r}: {1}'.format(name, err)
        return ret
    else:
        installed_pkgs = dict((p, info)
                for p, info in six.iteritems(installed_pkgs))

    pkgs_satisfied = []
    pkgs_to_install = []

    def _pkg_is_installed(pkg, installed_pkgs):
        '''
        Helper function to determine if a package is installed

        This performs more complex comparison than just checking
        keys, such as examining source repos to see if the package
        was installed by a different name from the same repo

        :pkg str: The package to compare
        :installed_pkgs: A dictionary produced by npm list --json
        '''
        if (pkg_name in installed_pkgs and
            'version' in installed_pkgs[pkg_name]):
            return True
        # Check to see if we are trying to install from a URI
        elif '://' in pkg_name:  # TODO Better way?
            for pkg_details in installed_pkgs.values():
                try:
                    pkg_from = pkg_details.get('from', '').split('://')[1]
                    if pkg_name.split('://')[1] == pkg_from:
                        return True
                except IndexError:
                    pass
        return False

    for pkg in pkg_list:
        pkg_name, _, pkg_ver = pkg.partition('@')
        pkg_name = pkg_name.strip()

        if force_reinstall is True:
            pkgs_to_install.append(pkg)
            continue
        if not _pkg_is_installed(pkg, installed_pkgs):
            pkgs_to_install.append(pkg)
            continue

        installed_name_ver = '{0}@{1}'.format(pkg_name,
                installed_pkgs[pkg_name]['version'])

        # If given an explicit version check the installed version matches.
        if pkg_ver:
            if installed_pkgs[pkg_name].get('version') != pkg_ver:
                pkgs_to_install.append(pkg)
            else:
                pkgs_satisfied.append(installed_name_ver)

            continue
        else:
            pkgs_satisfied.append(installed_name_ver)
            continue

    if __opts__['test']:
        ret['result'] = None

        comment_msg = []
        if pkgs_to_install:
            comment_msg.append('NPM package(s) {0!r} are set to be installed'
                .format(', '.join(pkgs_to_install)))

            ret['changes'] = {'old': [], 'new': pkgs_to_install}

        if pkgs_satisfied:
            comment_msg.append('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
            ret['result'] = True

        ret['comment'] = '. '.join(comment_msg)
        return ret

    if not pkgs_to_install:
        ret['result'] = True
        ret['comment'] = ('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
        return ret

    try:
        cmd_args = {
            'dir': dir,
            'runas': user,
            'registry': registry,
            'env': env,
            'pkgs': pkg_list,
        }

        call = __salt__['npm.install'](**cmd_args)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error installing {0!r}: {1}'.format(
                ', '.join(pkg_list), err)
        return ret

    if call and (isinstance(call, list) or isinstance(call, dict)):
        ret['result'] = True
        ret['changes'] = {'old': [], 'new': pkgs_to_install}
        ret['comment'] = 'Package(s) {0!r} successfully installed'.format(
                ', '.join(pkgs_to_install))
    else:
        ret['result'] = False
        ret['comment'] = 'Could not install package(s) {0!r}'.format(
                ', '.join(pkg_list))

    return ret

def removed(name,
            dir=None,
            user=None):
    '''
    Verify that the given package is not installed.

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir)
    except (CommandExecutionError, CommandNotFoundError) as err:
        ret['result'] = False
        ret['comment'] = 'Error uninstalling {0!r}: {1}'.format(name, err)
        return ret

    if name not in installed_pkgs:
        ret['result'] = True
        ret['comment'] = 'Package {0!r} is not installed'.format(name)
        return ret

    if __opts__['test']:
        ret['result'] = None
        ret['comment'] = 'Package {0!r} is set to be removed'.format(name)
        return ret

    if __salt__['npm.uninstall'](pkg=name, dir=dir, runas=user):
        ret['result'] = True
        ret['changes'][name] = 'Removed'
        ret['comment'] = 'Package {0!r} was successfully removed'.format(name)
    else:
        ret['result'] = False
        ret['comment'] = 'Error removing package {0!r}'.format(name)

    return ret

def bootstrap(name,
              user=None,
              silent=True):
    '''
    Bootstraps a node.js application.

    Will execute 'npm install --json' on the specified directory.

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if __opts__['test']:
        try:
            call = __salt__['npm.install'](dir=name, runas=user, pkg=None, silent=silent, dry_run=True)
        except (CommandNotFoundError, CommandExecutionError) as err:
            ret['result'] = False
            ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
            return ret
        ret['result'] = None
        ret['changes'] = {'old': [], 'new': call}
        ret['comment'] = '{0} is set to be bootstrapped'.format(name)
        return ret

    try:
        call = __salt__['npm.install'](dir=name, runas=user, pkg=None, silent=silent)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
        return ret

    if not call:
        ret['result'] = True
        ret['comment'] = 'Directory is already bootstrapped'
        return ret

    # npm.install will return a string if it can't parse a JSON result
    if isinstance(call, str):
        ret['result'] = False
        ret['changes'] = call
        ret['comment'] = 'Could not bootstrap directory'
    else:
        ret['result'] = True
        ret['changes'] = {name: 'Bootstrapped'}
        ret['comment'] = 'Directory was successfully bootstrapped'

    return ret
<commit_msg>Catch differences in git URLs in npm state<commit_after># -*- coding: utf-8 -*-
'''
Installation of NPM Packages
============================

These states manage the installed packages for node.js using the Node Package
Manager (npm). Note that npm must be installed for these states to be
available, so npm states should include a requisite to a pkg.installed state
for the package which provides npm (simply ``npm`` in most cases). Example:

.. code-block:: yaml

    npm:
      pkg.installed

    yaml:
      npm.installed:
        - require:
          - pkg: npm
'''

# Import salt libs
from __future__ import absolute_import
from salt.exceptions import CommandExecutionError, CommandNotFoundError

# Import 3rd-party libs
import salt.ext.six as six

def __virtual__():
    '''
    Only load if the npm module is available in __salt__
    '''
    return 'npm' if 'npm.list' in __salt__ else False, '\'npm\' binary not found on system'

def installed(name,
              pkgs=None,
              dir=None,
              user=None,
              force_reinstall=False,
              registry=None,
              env=None):
    '''
    Verify that the given package is installed and is at the correct version
    (if specified).

    .. code-block:: yaml

        coffee-script:
          npm.installed:
            - user: someuser

        coffee-script@1.0.1:
          npm.installed: []

    name
        The package to install

        .. versionchanged:: 2014.7.2
            This parameter is no longer lowercased by salt so that
            case-sensitive NPM package names will work.

    pkgs
        A list of packages to install with a single npm invocation; specifying
        this argument will ignore the ``name`` argument

        .. versionadded:: 2014.7.0

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0

    registry
        The NPM registry from which to install the package

        .. versionadded:: 2014.7.0

    env
        A list of environment variables to be set prior to execution. The
        format is the same as the :py:func:`cmd.run <salt.states.cmd.run>`.
        state function.

        .. versionadded:: 2014.7.0

    force_reinstall
        Install the package even if it is already installed
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if pkgs is not None:
        pkg_list = pkgs
    else:
        pkg_list = [name]

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir, runas=user, env=env)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error looking up {0!r}: {1}'.format(name, err)
        return ret
    else:
        installed_pkgs = dict((p, info)
                for p, info in six.iteritems(installed_pkgs))

    pkgs_satisfied = []
    pkgs_to_install = []

    def _pkg_is_installed(pkg, installed_pkgs):
        '''
        Helper function to determine if a package is installed

        This performs more complex comparison than just checking
        keys, such as examining source repos to see if the package
        was installed by a different name from the same repo

        :pkg str: The package to compare
        :installed_pkgs: A dictionary produced by npm list --json
        '''
        if (pkg_name in installed_pkgs and
            'version' in installed_pkgs[pkg_name]):
            return True
        # Check to see if we are trying to install from a URI
        elif '://' in pkg_name:  # TODO Better way?
            for pkg_details in installed_pkgs.values():
                try:
                    pkg_from = pkg_details.get('from', '').split('://')[1]
                    # Catch condition where we may have specified package as
                    # git://github.com/foo/bar but packager describes it as
                    # git://github.com/foo/bar.git in the package
                    if not pkg_from.endswith('.git') and pkg_name.startswith('git://'):
                        pkg_from += '.git'
                    if pkg_name.split('://')[1] == pkg_from:
                        return True
                except IndexError:
                    pass
        return False
    for pkg in pkg_list:
        pkg_name, _, pkg_ver = pkg.partition('@')
        pkg_name = pkg_name.strip()

        if force_reinstall is True:
            pkgs_to_install.append(pkg)
            continue
        if not _pkg_is_installed(pkg, installed_pkgs):
            pkgs_to_install.append(pkg)
            continue

        installed_name_ver = '{0}@{1}'.format(pkg_name,
                installed_pkgs[pkg_name]['version'])

        # If given an explicit version check the installed version matches.
        if pkg_ver:
            if installed_pkgs[pkg_name].get('version') != pkg_ver:
                pkgs_to_install.append(pkg)
            else:
                pkgs_satisfied.append(installed_name_ver)

            continue
        else:
            pkgs_satisfied.append(installed_name_ver)
            continue

    if __opts__['test']:
        ret['result'] = None

        comment_msg = []
        if pkgs_to_install:
            comment_msg.append('NPM package(s) {0!r} are set to be installed'
                .format(', '.join(pkgs_to_install)))

            ret['changes'] = {'old': [], 'new': pkgs_to_install}

        if pkgs_satisfied:
            comment_msg.append('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
            ret['result'] = True

        ret['comment'] = '. '.join(comment_msg)
        return ret

    if not pkgs_to_install:
        ret['result'] = True
        ret['comment'] = ('Package(s) {0!r} satisfied by {1}'
                .format(', '.join(pkg_list), ', '.join(pkgs_satisfied)))
        return ret

    try:
        cmd_args = {
            'dir': dir,
            'runas': user,
            'registry': registry,
            'env': env,
            'pkgs': pkg_list,
        }

        call = __salt__['npm.install'](**cmd_args)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error installing {0!r}: {1}'.format(
                ', '.join(pkg_list), err)
        return ret

    if call and (isinstance(call, list) or isinstance(call, dict)):
        ret['result'] = True
        ret['changes'] = {'old': [], 'new': pkgs_to_install}
        ret['comment'] = 'Package(s) {0!r} successfully installed'.format(
                ', '.join(pkgs_to_install))
    else:
        ret['result'] = False
        ret['comment'] = 'Could not install package(s) {0!r}'.format(
                ', '.join(pkg_list))

    return ret

def removed(name,
            dir=None,
            user=None):
    '''
    Verify that the given package is not installed.

    dir
        The target directory in which to install the package, or None for
        global installation

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    try:
        installed_pkgs = __salt__['npm.list'](dir=dir)
    except (CommandExecutionError, CommandNotFoundError) as err:
        ret['result'] = False
        ret['comment'] = 'Error uninstalling {0!r}: {1}'.format(name, err)
        return ret

    if name not in installed_pkgs:
        ret['result'] = True
        ret['comment'] = 'Package {0!r} is not installed'.format(name)
        return ret

    if __opts__['test']:
        ret['result'] = None
        ret['comment'] = 'Package {0!r} is set to be removed'.format(name)
        return ret

    if __salt__['npm.uninstall'](pkg=name, dir=dir, runas=user):
        ret['result'] = True
        ret['changes'][name] = 'Removed'
        ret['comment'] = 'Package {0!r} was successfully removed'.format(name)
    else:
        ret['result'] = False
        ret['comment'] = 'Error removing package {0!r}'.format(name)

    return ret

def bootstrap(name,
              user=None,
              silent=True):
    '''
    Bootstraps a node.js application.

    Will execute 'npm install --json' on the specified directory.

    user
        The user to run NPM with

        .. versionadded:: 0.17.0
    '''
    ret = {'name': name, 'result': None, 'comment': '', 'changes': {}}

    if __opts__['test']:
        try:
            call = __salt__['npm.install'](dir=name, runas=user, pkg=None, silent=silent, dry_run=True)
        except (CommandNotFoundError, CommandExecutionError) as err:
            ret['result'] = False
            ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
            return ret
        ret['result'] = None
        ret['changes'] = {'old': [], 'new': call}
        ret['comment'] = '{0} is set to be bootstrapped'.format(name)
        return ret

    try:
        call = __salt__['npm.install'](dir=name, runas=user, pkg=None, silent=silent)
    except (CommandNotFoundError, CommandExecutionError) as err:
        ret['result'] = False
        ret['comment'] = 'Error Bootstrapping {0!r}: {1}'.format(name, err)
        return ret

    if not call:
        ret['result'] = True
        ret['comment'] = 'Directory is already bootstrapped'
        return ret

    # npm.install will return a string if it can't parse a JSON result
    if isinstance(call, str):
        ret['result'] = False
        ret['changes'] = call
        ret['comment'] = 'Could not bootstrap directory'
    else:
        ret['result'] = True
        ret['changes'] = {name: 'Bootstrapped'}
        ret['comment'] = 'Directory was successfully bootstrapped'

    return ret
<|endoftext|>